From 2b0b8623452cf0198e9ddc31a8926b703a7c129a Mon Sep 17 00:00:00 2001 From: Minhyeok Park Date: Thu, 2 Jan 2025 20:11:14 +0900 Subject: [PATCH] feat: move external metadata to embeded --- bpset_metadata.json | 3149 ----------------- package.json | 5 +- src/BPManager.ts | 89 +- src/WebServer.ts | 19 +- .../alb/ALBHttpDropInvalidHeaderEnabled.ts | 142 +- src/bpsets/alb/ALBWAFEnabled.ts | 102 +- .../alb/ELBCrossZoneLoadBalancingEnabled.ts | 130 +- .../alb/ELBDeletionProtectionEnabled.ts | 130 +- src/bpsets/alb/ELBLoggingEnabled.ts | 140 +- .../apigw/APIGatewayAssociatedWithWAF.ts | 150 +- .../APIGatewayExecutionLoggingEnabled.ts | 146 +- .../apigw/APIGatewayV2AccessLogsEnabled.ts | 148 +- ...APIGatewayV2AuthorizationTypeConfigured.ts | 154 +- .../AutoScalingGroupELBHealthCheckRequired.ts | 118 +- src/bpsets/asg/AutoScalingLaunchTemplate.ts | 134 +- src/bpsets/asg/AutoScalingMultipleAZ.ts | 134 +- .../cloudfront/CloudFrontAccessLogsEnabled.ts | 158 +- .../cloudfront/CloudFrontAssociatedWithWAF.ts | 146 +- .../CloudFrontDefaultRootObjectConfigured.ts | 152 +- .../CloudFrontNoDeprecatedSSLProtocols.ts | 156 +- .../CloudFrontS3OriginAccessControlEnabled.ts | 166 +- .../cloudfront/CloudFrontViewerPolicyHTTPS.ts | 148 +- .../CWLogGroupRetentionPeriodCheck.ts | 132 +- .../CloudWatchAlarmSettingsCheck.ts | 156 +- ...eBuildProjectEnvironmentPrivilegedCheck.ts | 135 +- .../CodeBuildProjectLoggingEnabled.ts | 137 +- .../CodeDeployAutoRollbackMonitorEnabled.ts | 148 +- .../dynamodb/DynamoDBAutoscalingEnabled.ts | 173 +- .../DynamoDBLastBackupRecoveryPointCreated.ts | 163 +- src/bpsets/dynamodb/DynamoDBPITREnabled.ts | 136 +- .../DynamoDBTableDeletionProtectionEnabled.ts | 126 +- .../dynamodb/DynamoDBTableEncryptedKMS.ts | 142 +- .../DynamoDBTableEncryptionEnabled.ts | 128 +- src/bpsets/ec2/EC2EbsEncryptionByDefault.ts | 112 +- src/bpsets/ec2/EC2Imdsv2Check.ts | 108 +- .../EC2InstanceDetailedMonitoringEnabled.ts | 117 +- .../ec2/EC2InstanceManagedBySystemsManager.ts | 106 +- src/bpsets/ec2/EC2InstanceProfileAttached.ts | 125 +- src/bpsets/ec2/EC2NoAmazonKeyPair.ts | 87 +- src/bpsets/ec2/EC2StoppedInstance.ts | 112 +- src/bpsets/ec2/EC2TokenHopLimitCheck.ts | 110 +- src/bpsets/ecr/ECRKmsEncryption1.ts | 165 +- .../ecr/ECRPrivateImageScanningEnabled.ts | 118 +- .../ECRPrivateLifecyclePolicyConfigured.ts | 144 +- .../ecr/ECRPrivateTagImmutabilityEnabled.ts | 119 +- src/bpsets/ecs/ECSAwsVpcNetworkingEnabled.ts | 130 +- src/bpsets/ecs/ECSContainerInsightsEnabled.ts | 122 +- src/bpsets/ecs/ECSContainersNonPrivileged.ts | 140 +- src/bpsets/ecs/ECSContainersReadonlyAccess.ts | 140 +- .../ecs/ECSFargateLatestPlatformVersion.ts | 138 +- .../ecs/ECSTaskDefinitionLogConfiguration.ts | 157 +- .../ecs/ECSTaskDefinitionMemoryHardLimit.ts | 155 +- .../ecs/ECSTaskDefinitionNonRootUser.ts | 154 +- .../efs/EFSAccessPointEnforceRootDirectory.ts | 147 +- .../efs/EFSAccessPointEnforceUserIdentity.ts | 143 +- src/bpsets/efs/EFSAutomaticBackupsEnabled.ts | 127 +- src/bpsets/efs/EFSEncryptedCheck.ts | 131 +- .../efs/EFSMountTargetPublicAccessible.ts | 141 +- src/bpsets/eks/EKSClusterLoggingEnabled.ts | 125 +- src/bpsets/eks/EKSClusterSecretsEncrypted.ts | 137 +- src/bpsets/eks/EKSEndpointNoPublicAccess.ts | 119 +- ...ElastiCacheAutoMinorVersionUpgradeCheck.ts | 103 +- ...tiCacheRedisClusterAutomaticBackupCheck.ts | 119 +- .../ElastiCacheReplGrpAutoFailoverEnabled.ts | 103 +- .../ElastiCacheReplGrpEncryptedAtRest.ts | 93 +- .../ElastiCacheReplGrpEncryptedInTransit.ts | 93 +- .../ElastiCacheSubnetGroupCheck.ts | 130 +- .../IAMPolicyNoStatementsWithAdminAccess.ts | 113 +- .../IAMPolicyNoStatementsWithFullAccess.ts | 150 +- src/bpsets/iam/IAMRoleManagedPolicyCheck.ts | 143 +- src/bpsets/lambda/LambdaDLQCheck.ts | 142 +- .../LambdaFunctionPublicAccessProhibited.ts | 146 +- .../lambda/LambdaFunctionSettingsCheck.ts | 158 +- src/bpsets/lambda/LambdaInsideVPC.ts | 147 +- .../AuroraLastBackupRecoveryPointCreated.ts | 154 +- .../rds/AuroraMySQLBacktrackingEnabled.ts | 121 +- src/bpsets/rds/DBInstanceBackupEnabled.ts | 130 +- ...DSClusterAutoMinorVersionUpgradeEnabled.ts | 119 +- src/bpsets/rds/RDSClusterDefaultAdminCheck.ts | 143 +- .../RDSClusterDeletionProtectionEnabled.ts | 119 +- src/bpsets/rds/RDSClusterEncryptedAtRest.ts | 106 +- .../rds/RDSClusterIAMAuthenticationEnabled.ts | 124 +- src/bpsets/rds/RDSClusterMultiAZEnabled.ts | 128 +- .../rds/RDSDBSecurityGroupNotAllowed.ts | 179 +- .../rds/RDSEnhancedMonitoringEnabled.ts | 132 +- .../rds/RDSInstancePublicAccessCheck.ts | 124 +- src/bpsets/rds/RDSLoggingEnabled.ts | 156 +- src/bpsets/rds/RDSSnapshotEncrypted.ts | 134 +- src/bpsets/s3/S3AccessPointInVpcOnly.ts | 150 +- src/bpsets/s3/S3BucketDefaultLockEnabled.ts | 131 +- .../s3/S3BucketLevelPublicAccessProhibited.ts | 135 +- src/bpsets/s3/S3BucketLoggingEnabled.ts | 164 +- src/bpsets/s3/S3BucketSSLRequestsOnly.ts | 286 +- src/bpsets/s3/S3BucketVersioningEnabled.ts | 135 +- src/bpsets/s3/S3DefaultEncryptionKMS.ts | 163 +- src/bpsets/s3/S3EventNotificationsEnabled.ts | 165 +- .../s3/S3LastBackupRecoveryPointCreated.ts | 123 +- src/bpsets/s3/S3LifecyclePolicyCheck.ts | 171 +- .../SecretsManagerRotationEnabledCheck.ts | 127 +- ...etsManagerScheduledRotationSuccessCheck.ts | 143 +- .../SecretsManagerSecretPeriodicRotation.ts | 132 +- src/bpsets/securityhub/SecurityHubEnabled.ts | 132 +- src/bpsets/sns/SNSEncryptedKMS.ts | 155 +- ...TopicMessageDeliveryNotificationEnabled.ts | 165 +- .../EC2TransitGatewayAutoVPCAttachDisabled.ts | 118 +- src/bpsets/vpc/RestrictedCommonPorts.ts | 122 +- src/bpsets/vpc/RestrictedSSH.ts | 119 +- .../vpc/SubnetAutoAssignPublicIPDisabled.ts | 113 +- .../vpc/VPCDefaultSecurityGroupClosed.ts | 123 +- src/bpsets/vpc/VPCFlowLogsEnabled.ts | 150 +- src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts | 113 +- .../vpc/VPCPeeringDNSResolutionCheck.ts | 120 +- .../vpc/VPCSGOpenOnlyToAuthorizedPorts.ts | 122 +- src/bpsets/waf/WAFv2LoggingEnabled.ts | 105 +- .../waf/WAFv2RuleGroupLoggingEnabled.ts | 93 +- src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts | 113 +- src/bpsets/waf/WAFv2WebACLNotEmpty.ts | 114 +- src/types.d.ts | 31 +- 118 files changed, 11743 insertions(+), 6820 deletions(-) delete mode 100644 bpset_metadata.json diff --git a/bpset_metadata.json b/bpset_metadata.json deleted file mode 100644 index 4dfe74c..0000000 --- a/bpset_metadata.json +++ /dev/null @@ -1,3149 +0,0 @@ -[ - { - "name": "ALBHttpDropInvalidHeaderEnabled", - "description": "Ensures that the ALB is configured to drop invalid HTTP headers.", - "priority": 1, - "priorityReason": "Invalid headers can introduce security vulnerabilities.", - "awsService": "Elastic Load Balancing", - "awsServiceCategory": "Application Load Balancer", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeLoadBalancerAttributes", - "reason": "Retrieve ALB attributes to check for the invalid header configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyLoadBalancerAttributes", - "reason": "Enable the drop invalid headers feature for the ALB." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling this attribute does not break any custom client behavior." - }, - { - "name": "APIGatewayV2AccessLogsEnabled", - "description": "Ensures that access logs are enabled for API Gateway V2 HTTP APIs.", - "priority": 2, - "priorityReason": "Access logs help in monitoring and debugging API traffic issues.", - "awsService": "APIGateway", - "awsServiceCategory": "HTTP API", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logGroupArn", - "description": "The ARN of the CloudWatch Log Group to store access logs.", - "default": "", - "example": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/http-api/logs:*" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetStagesCommand", - "reason": "Retrieve stage information for HTTP APIs to check for access log configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateStageCommand", - "reason": "Enable access logs and configure the CloudWatch Log Group for the stage." - } - ], - "adviseBeforeFixFunction": "Ensure the specified CloudWatch Log Group exists and has proper permissions." - }, - { - "name": "APIGatewayV2AuthorizationTypeConfigured", - "description": "Verifies that all routes in API Gateway V2 HTTP APIs have authorization configured.", - "priority": 1, - "priorityReason": "Authorization protects APIs from unauthorized access.", - "awsService": "APIGateway", - "awsServiceCategory": "HTTP API", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "authorizationType", - "description": "The type of authorization to apply to the API route.", - "default": "AWS_IAM", - "example": "JWT" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetRoutesCommand", - "reason": "Retrieve route information to check for authorization configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateRouteCommand", - "reason": "Update route configuration to include the specified authorization type." - } - ], - "adviseBeforeFixFunction": "Ensure that the chosen authorization method is correctly set up and configured." - }, - { - "name": "APIGatewayAssociatedWithWAF", - "description": "Ensures that the API Gateway stages are associated with a WAF WebACL.", - "priority": 1, - "priorityReason": "WAF provides protection against common web exploits.", - "awsService": "APIGateway", - "awsServiceCategory": "REST API", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "webAclArn", - "description": "The ARN of the WAF WebACL to associate with the API Gateway stage.", - "default": "", - "example": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/ExampleWebACL/12345678-1234-5678-abcd-1234567890ab" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetStageCommand", - "reason": "Retrieve stage information to check if it is associated with a WAF WebACL." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AssociateWebACLCommand", - "reason": "Associate the specified WAF WebACL with the API Gateway stage." - } - ], - "adviseBeforeFixFunction": "Ensure the WAF WebACL is properly configured before associating it with the API Gateway stage." - }, - { - "name": "APIGatewayExecutionLoggingEnabled", - "description": "Ensures that execution logging is enabled for API Gateway stages.", - "priority": 2, - "priorityReason": "Execution logs help in debugging and monitoring API requests.", - "awsService": "APIGateway", - "awsServiceCategory": "REST API", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetStageCommand", - "reason": "Retrieve stage information to check execution logging configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateStageCommand", - "reason": "Enable execution logging for the API Gateway stage." - } - ], - "adviseBeforeFixFunction": "Ensure CloudWatch Logs are set up correctly to receive execution logs." - }, - { - "name": "AutoScalingGroupELBHealthCheckRequired", - "description": "Ensures that Auto Scaling Groups with load balancers or target groups use ELB health checks.", - "priority": 1, - "priorityReason": "ELB health checks ensure that unhealthy instances are replaced automatically.", - "awsService": "AutoScaling", - "awsServiceCategory": "Auto Scaling Group", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAutoScalingGroupsCommand", - "reason": "Retrieve Auto Scaling Group configurations to check health check settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateAutoScalingGroupCommand", - "reason": "Update the health check type of the Auto Scaling Group to ELB." - } - ], - "adviseBeforeFixFunction": "Ensure that the ELB or target group is correctly configured to provide health checks." - }, - { - "name": "AutoScalingMultipleAZ", - "description": "Ensures that Auto Scaling Groups are configured to run in multiple Availability Zones.", - "priority": 2, - "priorityReason": "Multiple AZs improve availability and fault tolerance of the application.", - "awsService": "AutoScaling", - "awsServiceCategory": "Auto Scaling Group", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [ - { - "name": "availabilityZones", - "description": "List of Availability Zones to add to the Auto Scaling Group.", - "default": "", - "example": "['us-east-1a', 'us-east-1b']" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAutoScalingGroupsCommand", - "reason": "Retrieve Auto Scaling Group configurations to check the number of Availability Zones." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateAutoScalingGroupCommand", - "reason": "Add multiple Availability Zones to the Auto Scaling Group." - } - ], - "adviseBeforeFixFunction": "Ensure that the additional Availability Zones have the necessary capacity and resources." - }, - { - "name": "AutoScalingLaunchTemplate", - "description": "Ensures that Auto Scaling Groups use launch templates instead of launch configurations.", - "priority": 3, - "priorityReason": "Launch templates provide better flexibility and are recommended over launch configurations.", - "awsService": "AutoScaling", - "awsServiceCategory": "Auto Scaling Group", - "bestPracticeCategory": "Configuration", - "requiredParametersForFix": [ - { - "name": "launchTemplateId", - "description": "The ID of the launch template to associate with the Auto Scaling Group.", - "default": "", - "example": "lt-0123456789abcdef0" - }, - { - "name": "launchTemplateVersion", - "description": "The version of the launch template to use.", - "default": "$Latest", - "example": "$Latest" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAutoScalingGroupsCommand", - "reason": "Retrieve Auto Scaling Group configurations to check for launch configuration usage." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateAutoScalingGroupCommand", - "reason": "Switch the Auto Scaling Group to use a launch template." - } - ], - "adviseBeforeFixFunction": "Ensure the launch template is properly configured before associating it with the Auto Scaling Group." - }, - { - "name": "CloudFrontAccessLogsEnabled", - "description": "Ensures that access logging is enabled for CloudFront distributions.", - "priority": 2, - "priorityReason": "Access logs help monitor and analyze distribution traffic.", - "awsService": "CloudFront", - "awsServiceCategory": "Content Delivery Network", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "s3BucketName", - "description": "The name of the S3 bucket to store access logs.", - "default": "", - "example": "my-cloudfront-logs-bucket" - }, - { - "name": "s3BucketPrefix", - "description": "The prefix for the access logs in the S3 bucket.", - "default": "", - "example": "CloudFront/logs/" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve the configuration of CloudFront distributions to check logging status." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDistributionCommand", - "reason": "Enable access logging and configure the S3 bucket for the CloudFront distribution." - } - ], - "adviseBeforeFixFunction": "Ensure the S3 bucket exists and has the necessary permissions to receive access logs." - }, - { - "name": "CloudFrontAssociatedWithWAF", - "description": "Ensures that CloudFront distributions are associated with a WAF WebACL.", - "priority": 1, - "priorityReason": "WAF protects the distribution from known vulnerabilities and attacks.", - "awsService": "CloudFront", - "awsServiceCategory": "Content Delivery Network", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "webAclArn", - "description": "The ARN of the WAF WebACL to associate with the CloudFront distribution.", - "default": "", - "example": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/ExampleWebACL/12345678-1234-5678-abcd-1234567890ab" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve the configuration of CloudFront distributions to check for WAF WebACL association." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AssociateWebACLCommand", - "reason": "Associate a WAF WebACL with the CloudFront distribution." - } - ], - "adviseBeforeFixFunction": "Ensure the WAF WebACL is properly configured before associating it." - }, - { - "name": "CloudFrontDefaultRootObjectConfigured", - "description": "Ensures that a default root object is configured for CloudFront distributions.", - "priority": 3, - "priorityReason": "A default root object improves user experience by serving content for root domain requests.", - "awsService": "CloudFront", - "awsServiceCategory": "Content Delivery Network", - "bestPracticeCategory": "Configuration", - "requiredParametersForFix": [ - { - "name": "defaultRootObject", - "description": "The default root object to configure for the CloudFront distribution.", - "default": "index.html", - "example": "index.html" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve the configuration of CloudFront distributions to check for a default root object." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDistributionCommand", - "reason": "Set a default root object for the CloudFront distribution." - } - ], - "adviseBeforeFixFunction": "Ensure the specified root object exists in the origin bucket or server." - }, - { - "name": "CloudFrontNoDeprecatedSSLProtocols", - "description": "Ensures that deprecated SSL protocols like SSLv3 are not enabled for CloudFront distributions.", - "priority": 1, - "priorityReason": "Deprecated SSL protocols pose security risks and should be disabled.", - "awsService": "CloudFront", - "awsServiceCategory": "Content Delivery Network", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve the configuration of CloudFront distributions to check SSL protocols." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDistributionCommand", - "reason": "Update the SSL protocol settings to disable deprecated protocols." - } - ], - "adviseBeforeFixFunction": "Ensure the change aligns with your application's SSL requirements." - }, - { - "name": "CloudFrontViewerPolicyHTTPS", - "description": "Ensures that viewer protocol policies enforce HTTPS for all CloudFront distributions.", - "priority": 1, - "priorityReason": "Enforcing HTTPS ensures secure communication between the client and the distribution.", - "awsService": "CloudFront", - "awsServiceCategory": "Content Delivery Network", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve the configuration of CloudFront distributions to check viewer protocol policies." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDistributionCommand", - "reason": "Update viewer protocol policies to enforce HTTPS for all behaviors." - } - ], - "adviseBeforeFixFunction": "Ensure all origins are configured to accept HTTPS traffic." - }, - { - "name": "CloudWatchAlarmSettingsCheck", - "description": "Ensures that CloudWatch alarms are configured with the required settings.", - "priority": 3, - "priorityReason": "Properly configured alarms ensure timely monitoring and response to system events.", - "awsService": "CloudWatch", - "awsServiceCategory": "Alarms", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "alarmParameters", - "description": "The required parameters to configure the alarm properly.", - "default": "{}", - "example": "{\"MetricName\": \"CPUUtilization\", \"Threshold\": 80, \"ComparisonOperator\": \"GreaterThanOrEqualToThreshold\"}" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAlarmsCommand", - "reason": "Retrieve CloudWatch alarm configurations to check for required parameters." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutMetricAlarmCommand", - "reason": "Update the alarm settings to match the required configuration." - } - ], - "adviseBeforeFixFunction": "Ensure the specified metric and parameters align with the monitoring objectives." - }, - { - "name": "CodeBuildProjectEnvironmentPrivilegedCheck", - "description": "Ensures that CodeBuild projects do not have privileged mode enabled in their environment.", - "priority": 1, - "priorityReason": "Privileged mode can pose a security risk by allowing unrestricted access to resources.", - "awsService": "CodeBuild", - "awsServiceCategory": "Build Projects", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "BatchGetProjectsCommand", - "reason": "Retrieve CodeBuild project configurations to check for privileged mode." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateProjectCommand", - "reason": "Disable privileged mode for the CodeBuild project." - } - ], - "adviseBeforeFixFunction": "Ensure that privileged mode is not required for specific build operations." - }, - { - "name": "CodeBuildProjectLoggingEnabled", - "description": "Ensures that CodeBuild projects have logging enabled to CloudWatch or S3.", - "priority": 2, - "priorityReason": "Logs help monitor and debug the build process.", - "awsService": "CodeBuild", - "awsServiceCategory": "Build Projects", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logGroupArn", - "description": "The ARN of the CloudWatch Log Group to store build logs.", - "default": "", - "example": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/codebuild/logs:*" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "BatchGetProjectsCommand", - "reason": "Retrieve CodeBuild project configurations to check for logging settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateProjectCommand", - "reason": "Enable logging for the CodeBuild project." - } - ], - "adviseBeforeFixFunction": "Ensure the specified CloudWatch Log Group exists and has necessary permissions." - }, - { - "name": "CodeDeployAutoRollbackMonitorEnabled", - "description": "Ensures that CodeDeploy deployment groups have monitoring alarms and auto-rollback enabled.", - "priority": 1, - "priorityReason": "Monitoring alarms and auto-rollback reduce the risk of failed deployments impacting production.", - "awsService": "CodeDeploy", - "awsServiceCategory": "Deployment Groups", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [ - { - "name": "alarmConfiguration", - "description": "Configuration for CloudWatch alarms to monitor during deployments.", - "default": "{}", - "example": "{\"enabled\": true, \"alarms\": [{\"name\": \"HighErrorRate\"}]}" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListDeploymentGroupsCommand", - "reason": "Retrieve deployment groups to check for alarm and rollback configuration." - }, - { - "name": "BatchGetDeploymentGroupsCommand", - "reason": "Fetch detailed configuration of the deployment groups." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDeploymentGroupCommand", - "reason": "Enable monitoring alarms and auto-rollback for the deployment group." - } - ], - "adviseBeforeFixFunction": "Ensure the monitoring alarms are set up and configured properly." - }, - { - "name": "DynamoDBAutoscalingEnabled", - "description": "Ensures that DynamoDB tables have autoscaling enabled for read and write capacity.", - "priority": 1, - "priorityReason": "Autoscaling ensures the table can handle varying workloads without manual intervention.", - "awsService": "DynamoDB", - "awsServiceCategory": "NoSQL Database", - "bestPracticeCategory": "Performance", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTableCommand", - "reason": "Retrieve table information to check billing mode and autoscaling policies." - }, - { - "name": "DescribeScalingPoliciesCommand", - "reason": "Check for existing autoscaling policies for the table." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutScalingPolicyCommand", - "reason": "Create or update autoscaling policies for the table." - } - ], - "adviseBeforeFixFunction": "Ensure the workload patterns are well understood to configure autoscaling appropriately." - }, - { - "name": "DynamoDBLastBackupRecoveryPointCreated", - "description": "Ensures that DynamoDB tables have a recent backup or recovery point created within the last 24 hours.", - "priority": 1, - "priorityReason": "Frequent backups ensure data recovery in case of accidental deletion or corruption.", - "awsService": "DynamoDB", - "awsServiceCategory": "NoSQL Database", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListBackupsCommand", - "reason": "Retrieve the list of backups for the table to check for recent recovery points." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateBackupCommand", - "reason": "Create a new backup for the DynamoDB table." - } - ], - "adviseBeforeFixFunction": "Ensure that backup policies are aligned with organizational data retention requirements." - }, - { - "name": "DynamoDBPITREnabled", - "description": "Ensures that Point-In-Time Recovery (PITR) is enabled for DynamoDB tables.", - "priority": 2, - "priorityReason": "PITR allows recovery to any point in the last 35 days, enhancing resilience against accidental data loss.", - "awsService": "DynamoDB", - "awsServiceCategory": "NoSQL Database", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeContinuousBackupsCommand", - "reason": "Retrieve table information to check if PITR is enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateContinuousBackupsCommand", - "reason": "Enable Point-In-Time Recovery for the table." - } - ], - "adviseBeforeFixFunction": "Ensure PITR is enabled only on tables requiring this feature to manage costs." - }, - { - "name": "DynamoDBTableDeletionProtectionEnabled", - "description": "Ensures that deletion protection is enabled for DynamoDB tables.", - "priority": 1, - "priorityReason": "Deletion protection prevents accidental deletion of critical tables.", - "awsService": "DynamoDB", - "awsServiceCategory": "NoSQL Database", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTableCommand", - "reason": "Retrieve table information to check deletion protection settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateTableCommand", - "reason": "Enable deletion protection for the table." - } - ], - "adviseBeforeFixFunction": "Ensure deletion protection aligns with the operational requirements of the application." - }, - { - "name": "DynamoDBTableEncryptedKMS", - "description": "Ensures that DynamoDB tables are encrypted using KMS keys.", - "priority": 1, - "priorityReason": "KMS encryption protects sensitive data stored in DynamoDB tables.", - "awsService": "DynamoDB", - "awsServiceCategory": "NoSQL Database", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyId", - "description": "The ID of the KMS key to use for encrypting the table.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcdef12-3456-7890-abcd-ef1234567890" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTableCommand", - "reason": "Retrieve table information to check encryption settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateTableCommand", - "reason": "Enable encryption using the specified KMS key." - } - ], - "adviseBeforeFixFunction": "Ensure the specified KMS key is accessible and properly configured." - }, - { - "name": "EC2InstanceProfileAttached", - "description": "Ensures that all EC2 instances have an IAM instance profile attached.", - "priority": 2, - "priorityReason": "IAM instance profiles enable secure access to AWS services from EC2 instances.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "instanceProfileName", - "description": "The name of the IAM instance profile to attach.", - "default": "", - "example": "EC2InstanceProfile" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Check if IAM instance profiles are attached to EC2 instances." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AssociateIamInstanceProfileCommand", - "reason": "Attach an IAM instance profile to the EC2 instance." - } - ], - "adviseBeforeFixFunction": "Ensure the IAM instance profile has the required policies attached." - }, - { - "name": "EC2StoppedInstance", - "description": "Ensures that unused stopped EC2 instances are terminated.", - "priority": 1, - "priorityReason": "Terminating stopped instances reduces costs and frees up resources.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Cost Management", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Identify stopped EC2 instances." - } - ], - "commandUsedInFixFunction": [ - { - "name": "TerminateInstancesCommand", - "reason": "Terminate unused stopped EC2 instances." - } - ], - "adviseBeforeFixFunction": "Ensure that the stopped instances are no longer needed before terminating them." - }, - { - "name": "ECRPrivateImageScanningEnabled", - "description": "Ensures that image scanning on push is enabled for ECR repositories.", - "priority": 1, - "priorityReason": "Enabling image scanning on push helps identify vulnerabilities in container images.", - "awsService": "ECR", - "awsServiceCategory": "Container Registry", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeRepositoriesCommand", - "reason": "Retrieve repository configurations to check if image scanning on push is enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutImageScanningConfigurationCommand", - "reason": "Enable image scanning on push for the repository." - } - ], - "adviseBeforeFixFunction": "Ensure the repository content complies with scanning requirements." - }, - { - "name": "ECRPrivateLifecyclePolicyConfigured", - "description": "Ensures that lifecycle policies are configured for ECR repositories.", - "priority": 2, - "priorityReason": "Lifecycle policies help manage repository storage by automatically removing unneeded images.", - "awsService": "ECR", - "awsServiceCategory": "Container Registry", - "bestPracticeCategory": "Cost Management", - "requiredParametersForFix": [ - { - "name": "lifecyclePolicyText", - "description": "The JSON-formatted lifecycle policy text.", - "default": "", - "example": "{\"rules\": [{\"rulePriority\": 1, \"description\": \"Keep only recent images\", \"selection\": {\"tagStatus\": \"untagged\", \"countType\": \"imageCountMoreThan\", \"countNumber\": 10, \"tagPrefixList\": []}, \"action\": {\"type\": \"expire\"}}]}" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetLifecyclePolicyCommand", - "reason": "Check if a lifecycle policy is configured for the repository." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutLifecyclePolicyCommand", - "reason": "Configure a lifecycle policy for the repository." - } - ], - "adviseBeforeFixFunction": "Ensure the lifecycle policy aligns with retention requirements." - }, - { - "name": "ECRPrivateTagImmutabilityEnabled", - "description": "Ensures that tag immutability is enabled for ECR repositories.", - "priority": 2, - "priorityReason": "Tag immutability prevents overwriting tags, ensuring image stability and integrity.", - "awsService": "ECR", - "awsServiceCategory": "Container Registry", - "bestPracticeCategory": "Configuration", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeRepositoriesCommand", - "reason": "Retrieve repository configurations to check if tag immutability is enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutImageTagMutabilityCommand", - "reason": "Enable tag immutability for the repository." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling tag immutability does not disrupt existing workflows." - }, - { - "name": "ECSContainersNonPrivileged", - "description": "Ensures that ECS containers do not run in privileged mode.", - "priority": 1, - "priorityReason": "Privileged mode can give containers elevated access to the host system, posing a security risk.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve task definition details to check container privilege settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Update the task definition to disable privileged mode." - } - ], - "adviseBeforeFixFunction": "Ensure the containers do not require privileged mode for their operations." - }, - { - "name": "ECSContainerInsightsEnabled", - "description": "Ensures that ECS clusters have Container Insights enabled.", - "priority": 2, - "priorityReason": "Container Insights provides detailed monitoring metrics and logs for ECS clusters and services.", - "awsService": "ECS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeClustersCommand", - "reason": "Retrieve ECS cluster configurations to check for Container Insights settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateClusterSettingsCommand", - "reason": "Enable Container Insights for the ECS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure CloudWatch is configured to receive metrics and logs from Container Insights." - }, - { - "name": "ECSTaskDefinitionLogConfiguration", - "description": "Ensures that ECS task definitions include a log configuration.", - "priority": 1, - "priorityReason": "Log configuration ensures that container logs are sent to a centralized logging service such as CloudWatch.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logDriver", - "description": "The log driver to use for the task definition.", - "default": "awslogs", - "example": "awslogs" - }, - { - "name": "logOptions", - "description": "The options for the log driver, such as log group name and region.", - "default": "{}", - "example": "{\"awslogs-group\": \"/ecs/example\", \"awslogs-region\": \"us-east-1\"}" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve task definition details to check for log configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Update the task definition to include a log configuration." - } - ], - "adviseBeforeFixFunction": "Ensure the log group exists and is configured to receive logs." - }, - { - "name": "ECSTaskDefinitionMemoryHardLimit", - "description": "Ensures that ECS containers specify a memory hard limit in the task definition.", - "priority": 1, - "priorityReason": "Specifying a memory hard limit prevents containers from consuming excessive memory, protecting other processes.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Performance", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve task definition details to check memory limit settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Update the task definition to specify a memory hard limit." - } - ], - "adviseBeforeFixFunction": "Ensure containers are tested with the specified memory limits." - }, - { - "name": "ECSTaskDefinitionNonRootUser", - "description": "Ensures that ECS containers run as a non-root user.", - "priority": 1, - "priorityReason": "Running containers as a non-root user reduces the risk of privilege escalation attacks.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "user", - "description": "The user to run the container as.", - "default": "", - "example": "appuser" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve task definition details to check the container user settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Update the task definition to run the container as a non-root user." - } - ], - "adviseBeforeFixFunction": "Ensure the application can run with non-root permissions." - }, - { - "name": "EFSAccessPointEnforceRootDirectory", - "description": "Ensures that EFS access points enforce a non-root directory for enhanced security.", - "priority": 2, - "priorityReason": "Restricting access to specific directories reduces the risk of data exposure.", - "awsService": "EFS", - "awsServiceCategory": "Access Points", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "path", - "description": "The path to enforce as the root directory for the access point.", - "default": "/data", - "example": "/data" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAccessPointsCommand", - "reason": "Retrieve information about EFS access points to check their root directory configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateAccessPointCommand", - "reason": "Reconfigure the access point to enforce a specific root directory." - } - ], - "adviseBeforeFixFunction": "Ensure that the directory path is properly configured and exists in the file system." - }, - { - "name": "EFSAccessPointEnforceUserIdentity", - "description": "Ensures that EFS access points enforce a user identity for operations.", - "priority": 2, - "priorityReason": "Enforcing a user identity ensures accountability and proper access control.", - "awsService": "EFS", - "awsServiceCategory": "Access Points", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "posixUser", - "description": "The POSIX user configuration to enforce on the access point.", - "default": "{}", - "example": "{\"Uid\": 1001, \"Gid\": 1001, \"SecondaryGids\": [1002]}" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeAccessPointsCommand", - "reason": "Retrieve access point details to check POSIX user settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateAccessPointCommand", - "reason": "Reconfigure the access point to enforce a user identity." - } - ], - "adviseBeforeFixFunction": "Ensure the specified POSIX user is valid and properly configured in the file system." - }, - { - "name": "EFSAutomaticBackupsEnabled", - "description": "Ensures that automatic backups are enabled for EFS file systems.", - "priority": 1, - "priorityReason": "Automatic backups ensure data recovery in case of accidental deletion or corruption.", - "awsService": "EFS", - "awsServiceCategory": "File Systems", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeBackupPolicyCommand", - "reason": "Check the backup policy for EFS file systems." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBackupPolicyCommand", - "reason": "Enable automatic backups for the file system." - } - ], - "adviseBeforeFixFunction": "Verify backup retention policies align with organizational requirements." - }, - { - "name": "EFSEncryptedCheck", - "description": "Ensures that EFS file systems are encrypted at rest.", - "priority": 1, - "priorityReason": "Encryption at rest protects sensitive data stored in EFS file systems.", - "awsService": "EFS", - "awsServiceCategory": "File Systems", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeFileSystemsCommand", - "reason": "Check if the file systems are encrypted." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateFileSystemCommand", - "reason": "Recreate the file system with encryption enabled." - } - ], - "adviseBeforeFixFunction": "Ensure data migration is planned before recreating the file system with encryption." - }, - { - "name": "EFSMountTargetPublicAccessible", - "description": "Ensures that EFS mount targets are not publicly accessible.", - "priority": 1, - "priorityReason": "Restricting public access prevents unauthorized access to file systems.", - "awsService": "EFS", - "awsServiceCategory": "Mount Targets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeMountTargetsCommand", - "reason": "Retrieve information about mount targets to check public accessibility." - }, - { - "name": "DescribeRouteTablesCommand", - "reason": "Check if mount targets are associated with routes exposing them publicly." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyMountTargetSecurityGroupsCommand", - "reason": "Restrict access to the mount target by updating security groups." - } - ], - "adviseBeforeFixFunction": "Ensure the security group rules align with organizational access policies." - }, - { - "name": "EKSClusterLoggingEnabled", - "description": "Ensures that EKS clusters have all logging types enabled.", - "priority": 2, - "priorityReason": "Enabling cluster logging ensures better monitoring, troubleshooting, and compliance.", - "awsService": "EKS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeClusterCommand", - "reason": "Retrieve cluster details to check logging configurations." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateClusterConfigCommand", - "reason": "Enable all available logging types for the EKS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure enabling logging will not impact current cluster performance or cost significantly." - }, - { - "name": "EKSClusterSecretsEncrypted", - "description": "Ensures that EKS clusters use KMS encryption for secrets.", - "priority": 1, - "priorityReason": "Encrypting secrets enhances security and meets compliance requirements.", - "awsService": "EKS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyArn", - "description": "The ARN of the KMS key to use for secrets encryption.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcdef12-3456-7890-abcd-ef1234567890" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeClusterCommand", - "reason": "Check if the cluster has encryption enabled for secrets." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AssociateEncryptionConfigCommand", - "reason": "Add encryption configuration to the EKS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure the specified KMS key is available and properly configured for the cluster." - }, - { - "name": "EKSEndpointNoPublicAccess", - "description": "Ensures that EKS cluster endpoints are not publicly accessible.", - "priority": 1, - "priorityReason": "Restricting public access prevents unauthorized access to the cluster.", - "awsService": "EKS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeClusterCommand", - "reason": "Retrieve cluster endpoint configuration to check public access settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateClusterConfigCommand", - "reason": "Disable public access for the cluster endpoint." - } - ], - "adviseBeforeFixFunction": "Ensure private access is properly configured before disabling public access." - }, - { - "name": "ElastiCacheAutoMinorVersionUpgradeCheck", - "description": "Ensures that Auto Minor Version Upgrade is enabled for ElastiCache clusters.", - "priority": 2, - "priorityReason": "Keeping clusters updated with minor versions ensures they receive the latest security patches and bug fixes.", - "awsService": "ElastiCache", - "awsServiceCategory": "Cache Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeCacheClustersCommand", - "reason": "Retrieve cluster configurations to check Auto Minor Version Upgrade settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyCacheClusterCommand", - "reason": "Enable Auto Minor Version Upgrade for the cluster." - } - ], - "adviseBeforeFixFunction": "Ensure enabling minor version upgrades aligns with application compatibility." - }, - { - "name": "ElastiCacheRedisClusterAutomaticBackupCheck", - "description": "Ensures that automatic backups are enabled for ElastiCache Redis clusters.", - "priority": 1, - "priorityReason": "Automatic backups are crucial for data recovery in case of accidental deletion or corruption.", - "awsService": "ElastiCache", - "awsServiceCategory": "Redis", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [ - { - "name": "snapshotRetentionLimit", - "description": "The number of days to retain backups.", - "default": "7", - "example": "7" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeReplicationGroupsCommand", - "reason": "Check if automatic backups are enabled for replication groups." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyReplicationGroupCommand", - "reason": "Enable automatic backups for the Redis replication group." - } - ], - "adviseBeforeFixFunction": "Verify the snapshot retention policy aligns with organizational recovery requirements." - }, - { - "name": "ElastiCacheSubnetGroupCheck", - "description": "Ensures that ElastiCache clusters are not using the default subnet group.", - "priority": 2, - "priorityReason": "Using a custom subnet group ensures better control over network configurations.", - "awsService": "ElastiCache", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Configuration", - "requiredParametersForFix": [ - { - "name": "subnetGroupName", - "description": "The name of the custom subnet group to use.", - "default": "", - "example": "custom-subnet-group" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeCacheClustersCommand", - "reason": "Retrieve cluster configurations to check subnet group settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyCacheClusterCommand", - "reason": "Update the cluster to use a custom subnet group." - } - ], - "adviseBeforeFixFunction": "Ensure the custom subnet group is properly configured and meets network requirements." - }, - { - "name": "IAMPolicyNoStatementsWithAdminAccess", - "description": "Ensures that IAM policies do not contain statements granting full administrative access.", - "priority": 1, - "priorityReason": "Granting full administrative access violates the principle of least privilege and poses a security risk.", - "awsService": "IAM", - "awsServiceCategory": "Policies", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListPoliciesCommand", - "reason": "Retrieve the list of local IAM policies." - }, - { - "name": "GetPolicyVersionCommand", - "reason": "Retrieve the default version of IAM policies to analyze their statements." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreatePolicyVersionCommand", - "reason": "Create a new policy version with restricted permissions." - }, - { - "name": "SetDefaultPolicyVersionCommand", - "reason": "Set the new policy version as the default." - } - ], - "adviseBeforeFixFunction": "Review the policy requirements to ensure removing administrative access will not disrupt operations." - }, - { - "name": "IAMPolicyNoStatementsWithFullAccess", - "description": "Ensures that IAM policies do not contain statements granting full access to specific services.", - "priority": 1, - "priorityReason": "Granting full access to specific services can lead to unintentional misuse or privilege escalation.", - "awsService": "IAM", - "awsServiceCategory": "Policies", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListPoliciesCommand", - "reason": "Retrieve the list of local IAM policies." - }, - { - "name": "GetPolicyVersionCommand", - "reason": "Retrieve the default version of IAM policies to analyze their statements." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreatePolicyVersionCommand", - "reason": "Create a new policy version with restricted permissions." - }, - { - "name": "SetDefaultPolicyVersionCommand", - "reason": "Set the new policy version as the default." - } - ], - "adviseBeforeFixFunction": "Review the policy requirements to ensure removing full access permissions will not disrupt operations." - }, - { - "name": "IAMRoleManagedPolicyCheck", - "description": "Ensures that managed policies are attached to IAM roles, groups, or users.", - "priority": 2, - "priorityReason": "Attaching managed policies ensures consistent permissions and simplifies policy management.", - "awsService": "IAM", - "awsServiceCategory": "Roles", - "bestPracticeCategory": "Configuration", - "requiredParametersForFix": [ - { - "name": "roleName", - "description": "The name of the IAM role to attach the managed policy to.", - "default": "", - "example": "MyRole" - }, - { - "name": "policyArn", - "description": "The ARN of the managed policy to attach.", - "default": "", - "example": "arn:aws:iam::aws:policy/ReadOnlyAccess" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListPoliciesCommand", - "reason": "Retrieve the list of managed IAM policies." - }, - { - "name": "ListEntitiesForPolicyCommand", - "reason": "Check which entities are attached to each managed policy." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AttachRolePolicyCommand", - "reason": "Attach a managed policy to an IAM role." - } - ], - "adviseBeforeFixFunction": "Ensure the managed policy aligns with the role's intended permissions." - }, - { - "name": "LambdaDLQCheck", - "description": "Ensures that AWS Lambda functions have a Dead Letter Queue (DLQ) configured.", - "priority": 2, - "priorityReason": "Configuring a DLQ ensures that failed Lambda invocations are captured for troubleshooting and analysis.", - "awsService": "Lambda", - "awsServiceCategory": "Functions", - "bestPracticeCategory": "Reliability", - "requiredParametersForFix": [ - { - "name": "dlqArn", - "description": "The ARN of the Dead Letter Queue to configure.", - "default": "", - "example": "arn:aws:sqs:us-east-1:123456789012:MyDLQ" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListFunctionsCommand", - "reason": "Retrieve the list of Lambda functions to check for DLQ configurations." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateFunctionConfigurationCommand", - "reason": "Configure a Dead Letter Queue for the Lambda function." - } - ], - "adviseBeforeFixFunction": "Ensure the DLQ exists and has the appropriate permissions to receive failed messages." - }, - { - "name": "LambdaFunctionPublicAccessProhibited", - "description": "Ensures that AWS Lambda functions are not publicly accessible.", - "priority": 1, - "priorityReason": "Publicly accessible Lambda functions can lead to security vulnerabilities and unauthorized usage.", - "awsService": "Lambda", - "awsServiceCategory": "Functions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetPolicyCommand", - "reason": "Retrieve the Lambda function's resource-based policy to check for public access." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RemovePermissionCommand", - "reason": "Remove any permissions that grant public access to the Lambda function." - } - ], - "adviseBeforeFixFunction": "Verify that removing public access will not disrupt intended functionality." - }, - { - "name": "LambdaFunctionSettingsCheck", - "description": "Ensures that AWS Lambda functions do not use default settings for timeout and memory.", - "priority": 2, - "priorityReason": "Customizing timeout and memory settings optimizes function performance and cost.", - "awsService": "Lambda", - "awsServiceCategory": "Functions", - "bestPracticeCategory": "Performance", - "requiredParametersForFix": [ - { - "name": "timeout", - "description": "The timeout duration (in seconds) for the Lambda function.", - "default": "15", - "example": "15" - }, - { - "name": "memorySize", - "description": "The memory size (in MB) allocated to the Lambda function.", - "default": "256", - "example": "256" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListFunctionsCommand", - "reason": "Retrieve the list of Lambda functions to check their settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateFunctionConfigurationCommand", - "reason": "Update the function's timeout and memory settings." - } - ], - "adviseBeforeFixFunction": "Ensure the updated settings align with the function's performance and cost requirements." - }, - { - "name": "LambdaInsideVPC", - "description": "Ensures that AWS Lambda functions are configured to run inside a VPC.", - "priority": 1, - "priorityReason": "Running Lambda functions inside a VPC provides additional security and control over network traffic.", - "awsService": "Lambda", - "awsServiceCategory": "Functions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "subnetIds", - "description": "The subnet IDs for the Lambda function to use.", - "default": "", - "example": "subnet-12345678,subnet-87654321" - }, - { - "name": "securityGroupIds", - "description": "The security group IDs for the Lambda function to use.", - "default": "", - "example": "sg-12345678,sg-87654321" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListFunctionsCommand", - "reason": "Retrieve the list of Lambda functions to check their VPC configurations." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateFunctionConfigurationCommand", - "reason": "Configure the function to use a VPC." - } - ], - "adviseBeforeFixFunction": "Ensure the specified subnets and security groups are configured correctly and have necessary permissions." - }, - { - "name": "AuroraLastBackupRecoveryPointCreated", - "description": "Ensures that Aurora DB clusters have a recent recovery point created.", - "priority": 1, - "priorityReason": "Recent backups ensure data recovery in case of accidental deletion or corruption.", - "awsService": "RDS", - "awsServiceCategory": "Aurora Clusters", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve the list of Aurora DB clusters." - }, - { - "name": "DescribeDBClusterSnapshotsCommand", - "reason": "Check the most recent recovery point for each cluster." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateDBClusterSnapshotCommand", - "reason": "Create a new recovery point for the Aurora DB cluster." - } - ], - "adviseBeforeFixFunction": "Ensure the DB cluster is in a stable state before creating a snapshot." - }, - { - "name": "AuroraMySQLBacktrackingEnabled", - "description": "Ensures that backtracking is enabled for Aurora MySQL clusters.", - "priority": 2, - "priorityReason": "Backtracking allows quick recovery from accidental data modifications.", - "awsService": "RDS", - "awsServiceCategory": "Aurora Clusters", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [ - { - "name": "backtrackWindow", - "description": "The backtracking window in seconds.", - "default": "86400", - "example": "86400" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve the list of Aurora MySQL clusters and check their backtracking configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Enable backtracking for the Aurora MySQL cluster." - } - ], - "adviseBeforeFixFunction": "Ensure enabling backtracking does not disrupt the current cluster configuration." - }, - { - "name": "DBInstanceBackupEnabled", - "description": "Ensures that backups are enabled for RDS instances.", - "priority": 1, - "priorityReason": "Enabling backups is critical for disaster recovery and data protection.", - "awsService": "RDS", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [ - { - "name": "backupRetentionPeriod", - "description": "The number of days to retain automated backups.", - "default": "7", - "example": "7" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBInstancesCommand", - "reason": "Retrieve the list of RDS instances and check their backup settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBInstanceCommand", - "reason": "Enable automated backups for the RDS instance." - } - ], - "adviseBeforeFixFunction": "Ensure the backup retention policy meets organizational recovery requirements." - }, - { - "name": "RDSClusterIAMAuthenticationEnabled", - "description": "Ensures that IAM authentication is enabled for RDS clusters.", - "priority": 2, - "priorityReason": "IAM authentication simplifies credential management and enhances security.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve the list of RDS clusters and check their IAM authentication settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Enable IAM authentication for the RDS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure IAM roles are properly configured to support database authentication." - }, - { - "name": "RDSClusterDeletionProtectionEnabled", - "description": "Ensures that deletion protection is enabled for RDS clusters.", - "priority": 1, - "priorityReason": "Deletion protection prevents accidental deletion of critical RDS clusters.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve the list of RDS clusters and check their deletion protection settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Enable deletion protection for the RDS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure deletion protection aligns with operational requirements and does not block intentional deletions." - }, - { - "name": "RDSClusterEncryptedAtRest", - "description": "Ensures that RDS clusters are encrypted at rest.", - "priority": 1, - "priorityReason": "Encryption at rest protects sensitive data stored in RDS clusters.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve the list of RDS clusters and check their encryption settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateDBClusterCommand", - "reason": "Recreate the RDS cluster with encryption enabled." - } - ], - "adviseBeforeFixFunction": "Ensure data migration is planned before recreating the RDS cluster with encryption." - }, - { - "name": "S3BucketVersioningEnabled", - "description": "Ensures that S3 bucket versioning is enabled.", - "priority": 1, - "priorityReason": "Enabling versioning helps protect against accidental overwrites and deletions.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Resilience", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketVersioningCommand", - "reason": "Check if versioning is enabled for the S3 bucket." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketVersioningCommand", - "reason": "Enable versioning for the S3 bucket." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling versioning aligns with cost and recovery policies." - }, - { - "name": "S3BucketSSLRequestsOnly", - "description": "Ensures that S3 buckets require SSL for requests.", - "priority": 1, - "priorityReason": "Requiring SSL ensures secure data transmission to and from the bucket.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketPolicyCommand", - "reason": "Retrieve the bucket policy to check for SSL enforcement." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketPolicyCommand", - "reason": "Update or create a bucket policy to enforce SSL requests." - } - ], - "adviseBeforeFixFunction": "Review the impact of enforcing SSL on applications accessing the bucket." - }, - { - "name": "S3BucketLoggingEnabled", - "description": "Ensures that S3 bucket logging is enabled.", - "priority": 2, - "priorityReason": "Bucket logging helps in auditing and monitoring access patterns.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "targetBucket", - "description": "The target bucket for storing access logs.", - "default": "", - "example": "my-log-bucket" - }, - { - "name": "targetPrefix", - "description": "The prefix for log file names.", - "default": "", - "example": "logs/" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketLoggingCommand", - "reason": "Retrieve the bucket logging configuration." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketLoggingCommand", - "reason": "Enable logging for the S3 bucket." - } - ], - "adviseBeforeFixFunction": "Ensure the target bucket exists and has the necessary permissions for logging." - }, - { - "name": "S3BucketDefaultLockEnabled", - "description": "Ensures that S3 Object Lock is enabled by default on buckets.", - "priority": 1, - "priorityReason": "Object Lock protects objects against deletion or modification for a specified period.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "GetObjectLockConfigurationCommand", - "reason": "Check if the S3 bucket has default Object Lock enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateBucketCommand", - "reason": "Recreate the bucket with Object Lock enabled." - } - ], - "adviseBeforeFixFunction": "Recreating a bucket deletes its existing data. Ensure proper data migration is planned." - }, - { - "name": "SecretsManagerRotationEnabledCheck", - "description": "Ensures that secret rotation is enabled for AWS Secrets Manager secrets.", - "priority": 1, - "priorityReason": "Enabling rotation reduces the risk of credential compromise by regularly updating them.", - "awsService": "SecretsManager", - "awsServiceCategory": "Secrets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "rotationLambdaARN", - "description": "The ARN of the Lambda function to handle rotation.", - "default": "", - "example": "arn:aws:lambda:us-east-1:123456789012:function:MyRotationLambda" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListSecretsCommand", - "reason": "Retrieve the list of Secrets Manager secrets." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RotateSecretCommand", - "reason": "Enable rotation for the secret." - } - ], - "adviseBeforeFixFunction": "Ensure that the rotation Lambda function is configured correctly to handle secret updates." - }, - { - "name": "SecretsManagerScheduledRotationSuccessCheck", - "description": "Ensures that secrets with rotation enabled have been rotated successfully within the scheduled time.", - "priority": 1, - "priorityReason": "Ensuring timely rotation protects against stale or compromised credentials.", - "awsService": "SecretsManager", - "awsServiceCategory": "Secrets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListSecretsCommand", - "reason": "Retrieve the list of Secrets Manager secrets." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RotateSecretCommand", - "reason": "Force rotation of the secret to update it." - } - ], - "adviseBeforeFixFunction": "Verify that the rotation Lambda function is active and can handle forced rotation." - }, - { - "name": "SecretsManagerSecretPeriodicRotation", - "description": "Ensures that secrets are rotated periodically, at least every 90 days.", - "priority": 2, - "priorityReason": "Periodic rotation helps mitigate risks of long-lived credentials.", - "awsService": "SecretsManager", - "awsServiceCategory": "Secrets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "rotationDays", - "description": "The number of days after which the secret should be rotated.", - "default": "90", - "example": "90" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListSecretsCommand", - "reason": "Retrieve the list of Secrets Manager secrets." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateSecretCommand", - "reason": "Set the rotation schedule for the secret." - } - ], - "adviseBeforeFixFunction": "Ensure rotation rules align with organizational policies and application dependencies." - }, - { - "name": "SecurityHubEnabled", - "description": "Ensures that AWS Security Hub is enabled for the AWS account.", - "priority": 1, - "priorityReason": "Enabling Security Hub is critical for monitoring and managing security across AWS accounts.", - "awsService": "SecurityHub", - "awsServiceCategory": "Security Monitoring", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeHubCommand", - "reason": "Check if Security Hub is enabled in the account." - } - ], - "commandUsedInFixFunction": [ - { - "name": "EnableSecurityHubCommand", - "reason": "Enable Security Hub for the account." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling Security Hub aligns with organizational policies and cost considerations." - }, - { - "name": "SNSEncryptedKMS", - "description": "Ensures that SNS topics are encrypted using KMS keys.", - "priority": 2, - "priorityReason": "Encrypting SNS topics helps protect sensitive data transmitted via notifications.", - "awsService": "SNS", - "awsServiceCategory": "Topics", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyId", - "description": "The KMS key ID or ARN to encrypt the SNS topic.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListTopicsCommand", - "reason": "Retrieve the list of SNS topics." - }, - { - "name": "GetTopicAttributesCommand", - "reason": "Check the encryption settings of each SNS topic." - } - ], - "commandUsedInFixFunction": [ - { - "name": "SetTopicAttributesCommand", - "reason": "Enable encryption for the SNS topic." - } - ], - "adviseBeforeFixFunction": "Ensure the KMS key is configured correctly and accessible by SNS." - }, - { - "name": "SNSTopicMessageDeliveryNotificationEnabled", - "description": "Ensures that SNS topics are configured to send delivery notifications.", - "priority": 3, - "priorityReason": "Enabling delivery notifications ensures visibility into message delivery status.", - "awsService": "SNS", - "awsServiceCategory": "Topics", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "feedbackRoleArn", - "description": "The ARN of the IAM role to enable delivery notifications.", - "default": "", - "example": "arn:aws:iam::123456789012:role/SNSDeliveryRole" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListTopicsCommand", - "reason": "Retrieve the list of SNS topics." - }, - { - "name": "GetTopicAttributesCommand", - "reason": "Check if delivery notifications are enabled for each topic." - } - ], - "commandUsedInFixFunction": [ - { - "name": "SetTopicAttributesCommand", - "reason": "Enable delivery notifications for the SNS topic." - } - ], - "adviseBeforeFixFunction": "Ensure the IAM role has the appropriate permissions to manage delivery notifications." - }, - { - "name": "EC2TransitGatewayAutoVPCAttachDisabled", - "description": "Ensures that the auto-attach feature of Transit Gateways is disabled.", - "priority": 1, - "priorityReason": "Disabling auto-attach ensures manual control over VPC attachments, enhancing security and governance.", - "awsService": "EC2", - "awsServiceCategory": "Transit Gateways", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTransitGatewaysCommand", - "reason": "Retrieve the list of Transit Gateways and check their auto-attach settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyTransitGatewayCommand", - "reason": "Disable the auto-attach feature for the Transit Gateway." - } - ], - "adviseBeforeFixFunction": "Verify that no active auto-attached VPCs will be impacted by disabling this feature." - }, - { - "name": "RestrictedSSH", - "description": "Ensures that SSH access (port 22) is restricted to trusted sources.", - "priority": 1, - "priorityReason": "Restricting SSH access prevents unauthorized access to resources and strengthens security.", - "awsService": "EC2", - "awsServiceCategory": "Security Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeSecurityGroupRulesCommand", - "reason": "Retrieve security group rules to check for unrestricted SSH access." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RevokeSecurityGroupIngressCommand", - "reason": "Revoke rules allowing unrestricted SSH access." - } - ], - "adviseBeforeFixFunction": "Ensure that legitimate users have alternative access paths before restricting SSH access." - }, - { - "name": "SubnetAutoAssignPublicIPDisabled", - "description": "Ensures that subnets are not configured to automatically assign public IP addresses.", - "priority": 2, - "priorityReason": "Disabling automatic public IP assignment helps prevent unintended exposure of resources to the internet.", - "awsService": "EC2", - "awsServiceCategory": "Subnets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeSubnetsCommand", - "reason": "Retrieve the list of subnets and check their public IP assignment settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifySubnetAttributeCommand", - "reason": "Disable automatic public IP assignment for the subnet." - } - ], - "adviseBeforeFixFunction": "Ensure no essential resources require public IPs before disabling this feature." - }, - { - "name": "VPCFlowLogsEnabled", - "description": "Ensures that flow logs are enabled for all VPCs.", - "priority": 1, - "priorityReason": "Enabling flow logs provides visibility into network traffic and helps with troubleshooting and auditing.", - "awsService": "EC2", - "awsServiceCategory": "VPCs", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logGroupName", - "description": "The name of the CloudWatch log group to store flow logs.", - "default": "", - "example": "VPCFlowLogs" - }, - { - "name": "iamRoleArn", - "description": "The ARN of the IAM role with permissions to publish flow logs to CloudWatch.", - "default": "", - "example": "arn:aws:iam::123456789012:role/FlowLogsRole" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeVpcsCommand", - "reason": "Retrieve the list of VPCs and check if flow logs are enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CreateFlowLogsCommand", - "reason": "Enable flow logs for the VPC." - } - ], - "adviseBeforeFixFunction": "Ensure CloudWatch and IAM role configurations are prepared to handle the new logs." - }, - { - "name": "VPCDefaultSecurityGroupClosed", - "description": "Ensures that default security groups have no inbound or outbound rules.", - "priority": 1, - "priorityReason": "Closing default security groups prevents unintended access and strengthens security.", - "awsService": "EC2", - "awsServiceCategory": "Security Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeSecurityGroupsCommand", - "reason": "Retrieve default security groups and check their rules." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RevokeSecurityGroupIngressCommand", - "reason": "Remove all inbound rules from the default security group." - }, - { - "name": "RevokeSecurityGroupEgressCommand", - "reason": "Remove all outbound rules from the default security group." - } - ], - "adviseBeforeFixFunction": "Ensure no critical dependencies are relying on the default security group rules." - }, - { - "name": "WAFv2LoggingEnabled", - "description": "Ensures that logging is enabled for WAFv2 Web ACLs.", - "priority": 1, - "priorityReason": "Logging provides visibility into WAF actions and helps in auditing and debugging.", - "awsService": "WAFv2", - "awsServiceCategory": "Web ACLs", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logDestinationArn", - "description": "The ARN of the log group or Kinesis Data Firehose for logging.", - "default": "", - "example": "arn:aws:logs:us-east-1:123456789012:log-group:WAFLogs" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetLoggingConfigurationCommand", - "reason": "Check if logging is enabled for WAF Web ACLs." - }, - { - "name": "ListWebACLsCommand", - "reason": "Retrieve the list of Web ACLs." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutLoggingConfigurationCommand", - "reason": "Enable logging for WAF Web ACLs." - } - ], - "adviseBeforeFixFunction": "Ensure the log destination (CloudWatch Logs or Kinesis Data Firehose) is configured correctly." - }, - { - "name": "WAFv2RuleGroupLoggingEnabled", - "description": "Ensures that logging is enabled for WAFv2 Rule Groups.", - "priority": 2, - "priorityReason": "Logging for Rule Groups provides visibility into their actions, helping in auditing and debugging.", - "awsService": "WAFv2", - "awsServiceCategory": "Rule Groups", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetRuleGroupCommand", - "reason": "Retrieve the details of WAF Rule Groups to check their logging configuration." - }, - { - "name": "ListRuleGroupsCommand", - "reason": "Retrieve the list of Rule Groups." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateRuleGroupCommand", - "reason": "Enable logging for the WAF Rule Group." - } - ], - "adviseBeforeFixFunction": "Ensure metrics and logs are enabled for related Rule Groups." - }, - { - "name": "WAFv2RuleGroupNotEmpty", - "description": "Ensures that WAFv2 Rule Groups are not empty.", - "priority": 2, - "priorityReason": "Empty Rule Groups do not provide any protective measures, making them ineffective.", - "awsService": "WAFv2", - "awsServiceCategory": "Rule Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "rules", - "description": "The rules to be added to the Rule Group.", - "default": "", - "example": "[{\"Name\": \"IPBlock\", \"Priority\": 1, \"Statement\": {\"IPSetReferenceStatement\": {\"ARN\": \"arn:aws:wafv2:...\"}}}]" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetRuleGroupCommand", - "reason": "Retrieve details of Rule Groups and check if they contain rules." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateRuleGroupCommand", - "reason": "Add rules to the Rule Group." - } - ], - "adviseBeforeFixFunction": "Ensure the new rules do not conflict with existing configurations." - }, - { - "name": "WAFv2WebACLNotEmpty", - "description": "Ensures that WAFv2 Web ACLs contain at least one rule.", - "priority": 1, - "priorityReason": "Web ACLs without rules do not provide any protection against unwanted traffic.", - "awsService": "WAFv2", - "awsServiceCategory": "Web ACLs", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "rules", - "description": "The rules to be added to the Web ACL.", - "default": "", - "example": "[{\"Name\": \"BlockBadActors\", \"Priority\": 1, \"Statement\": {\"IPSetReferenceStatement\": {\"ARN\": \"arn:aws:wafv2:...\"}}}]" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetWebACLCommand", - "reason": "Retrieve details of Web ACLs to check if they contain rules." - }, - { - "name": "ListWebACLsCommand", - "reason": "Retrieve the list of Web ACLs." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateWebACLCommand", - "reason": "Add rules to the Web ACL." - } - ], - "adviseBeforeFixFunction": "Review the rules to ensure they align with your organization's security policies." - }, - { - "name": "RestrictedCommonPorts", - "description": "Ensures that security groups restrict access to common ports such as HTTP, SSH, MySQL, and others.", - "priority": 1, - "priorityReason": "Restricting access to common ports minimizes the risk of unauthorized access.", - "awsService": "EC2", - "awsServiceCategory": "Security Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeSecurityGroupRulesCommand", - "reason": "Retrieve the security group rules to check for unrestricted access to common ports." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RevokeSecurityGroupIngressCommand", - "reason": "Revoke ingress rules that allow unrestricted access to common ports." - } - ], - "adviseBeforeFixFunction": "Review and confirm which ports need to remain open for critical operations." - }, - { - "name": "VPCNetworkACLUnusedCheck", - "description": "Ensures that unused network ACLs are identified and marked for removal or optimization.", - "priority": 2, - "priorityReason": "Unused network ACLs increase administrative overhead and pose a potential security risk if misconfigured.", - "awsService": "EC2", - "awsServiceCategory": "Network ACLs", - "bestPracticeCategory": "Optimization", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeNetworkAclsCommand", - "reason": "Retrieve the list of network ACLs to check for unused ones." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "Ensure that identified unused ACLs are truly unlinked before removing them." - }, - { - "name": "VPCPeeringDNSResolutionCheck", - "description": "Ensures that VPC peering connections have DNS resolution enabled.", - "priority": 1, - "priorityReason": "Enabling DNS resolution improves connectivity and simplifies resource management across peered VPCs.", - "awsService": "EC2", - "awsServiceCategory": "VPC Peering", - "bestPracticeCategory": "Networking", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeVpcPeeringConnectionsCommand", - "reason": "Retrieve the list of VPC peering connections and their DNS resolution settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyVpcPeeringConnectionOptionsCommand", - "reason": "Enable DNS resolution for the VPC peering connection." - } - ], - "adviseBeforeFixFunction": "Ensure the VPCs involved in the peering connection require DNS resolution." - }, - { - "name": "VPCSGOpenOnlyToAuthorizedPorts", - "description": "Ensures that security groups are only open to authorized ports and IP ranges.", - "priority": 1, - "priorityReason": "Restricting security groups to authorized ports minimizes the risk of exposure to unauthorized access.", - "awsService": "EC2", - "awsServiceCategory": "Security Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "authorizedPorts", - "description": "A list of ports authorized for access.", - "default": "[80, 443]", - "example": "[80, 443]" - } - ], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeSecurityGroupRulesCommand", - "reason": "Retrieve security group rules to identify unauthorized open ports." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RevokeSecurityGroupIngressCommand", - "reason": "Revoke unauthorized ingress rules from security groups." - } - ], - "adviseBeforeFixFunction": "Validate the list of authorized ports to ensure it meets operational requirements." - }, - { - "name": "S3AccessPointInVpcOnly", - "description": "Ensures that S3 access points are restricted to VPCs.", - "priority": 1, - "priorityReason": "Restricting access points to VPCs enhances security by preventing public access.", - "awsService": "S3", - "awsServiceCategory": "Access Points", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "vpcId", - "description": "The ID of the VPC to which the access point should be restricted.", - "default": "", - "example": "vpc-12345678" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListAccessPointsCommand", - "reason": "Retrieve S3 access points to verify VPC-only settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateAccessPointCommand", - "reason": "Restrict the access point to a specific VPC." - } - ], - "adviseBeforeFixFunction": "Ensure the target VPC is configured correctly to support S3 access." - }, - { - "name": "S3BucketLevelPublicAccessProhibited", - "description": "Ensures that public access to S3 buckets is restricted.", - "priority": 1, - "priorityReason": "Restricting public access protects data in the S3 bucket from unauthorized users.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetPublicAccessBlockCommand", - "reason": "Retrieve public access block settings for S3 buckets." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutPublicAccessBlockCommand", - "reason": "Restrict public access to the S3 bucket by enabling public access blocks." - } - ], - "adviseBeforeFixFunction": "Ensure that no applications require public access to the bucket before applying restrictions." - }, - { - "name": "S3DefaultEncryptionKMS", - "description": "Ensures that S3 buckets have default encryption enabled using a KMS key.", - "priority": 1, - "priorityReason": "Default encryption ensures that all objects stored in the bucket are encrypted, protecting sensitive data.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyId", - "description": "The KMS key ID or ARN to enable default encryption.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketEncryptionCommand", - "reason": "Check if default encryption is enabled for the bucket." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketEncryptionCommand", - "reason": "Enable default encryption for the bucket." - } - ], - "adviseBeforeFixFunction": "Ensure the KMS key has the necessary permissions to encrypt and decrypt objects." - }, - { - "name": "S3EventNotificationsEnabled", - "description": "Ensures that S3 buckets have event notifications enabled for Lambda, SQS, or SNS.", - "priority": 2, - "priorityReason": "Event notifications facilitate real-time monitoring and automation, improving operational efficiency.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "eventNotificationConfig", - "description": "The event notification configuration to apply.", - "default": "", - "example": "{ \"LambdaFunctionConfigurations\": [...] }" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketNotificationConfigurationCommand", - "reason": "Retrieve the event notification configuration for the bucket." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketNotificationConfigurationCommand", - "reason": "Enable event notifications for the bucket." - } - ], - "adviseBeforeFixFunction": "Ensure that the configured notification targets (Lambda, SQS, SNS) are ready to handle events." - }, - { - "name": "S3LastBackupRecoveryPointCreated", - "description": "Ensures that S3 buckets have recent recovery points created within the last 24 hours.", - "priority": 1, - "priorityReason": "Regular backups ensure data integrity and recoverability in case of data loss.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Backup", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "ListRecoveryPointsByResourceCommand", - "reason": "Check the recovery points for the S3 bucket." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "Ensure the backup mechanism is configured to create recovery points regularly." - }, - { - "name": "S3LifecyclePolicyCheck", - "description": "Ensures that S3 buckets have lifecycle policies configured for managing object transitions and expirations.", - "priority": 2, - "priorityReason": "Lifecycle policies help optimize storage costs by managing object transitions and deletions.", - "awsService": "S3", - "awsServiceCategory": "Buckets", - "bestPracticeCategory": "Optimization", - "requiredParametersForFix": [ - { - "name": "lifecyclePolicy", - "description": "The lifecycle policy to apply to the bucket.", - "default": "", - "example": "{ \"Rules\": [...] }" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetBucketLifecycleConfigurationCommand", - "reason": "Retrieve the lifecycle configuration for the bucket." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutBucketLifecycleConfigurationCommand", - "reason": "Apply lifecycle policies to the bucket." - } - ], - "adviseBeforeFixFunction": "Review lifecycle rules to ensure they align with data retention policies." - }, - { - "name": "RDSClusterAutoMinorVersionUpgradeEnabled", - "description": "Ensures that RDS clusters have auto minor version upgrades enabled.", - "priority": 2, - "priorityReason": "Auto minor version upgrades ensure that RDS clusters stay up-to-date with the latest security and bug fixes.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Maintenance", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve details of RDS clusters to check auto minor version upgrade settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Enable auto minor version upgrades for the RDS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling auto minor upgrades does not disrupt application compatibility." - }, - { - "name": "RDSClusterDefaultAdminCheck", - "description": "Ensures that RDS clusters do not use default admin usernames like 'admin' or 'postgres'.", - "priority": 1, - "priorityReason": "Using non-default admin usernames reduces the risk of brute-force attacks.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve details of RDS clusters to check admin usernames." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "Consider re-creating clusters with non-default admin usernames to enhance security." - }, - { - "name": "RDSClusterMultiAZEnabled", - "description": "Ensures that RDS clusters are configured for Multi-AZ deployments.", - "priority": 1, - "priorityReason": "Multi-AZ deployments provide high availability and fault tolerance for RDS clusters.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Reliability", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve details of RDS clusters to check Multi-AZ settings." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "Ensure applications can tolerate a potential brief downtime during Multi-AZ deployment configuration." - }, - { - "name": "RDSDBSecurityGroupNotAllowed", - "description": "Ensures that RDS clusters do not use default security groups.", - "priority": 1, - "priorityReason": "Using custom security groups reduces the risk of unintended access to the database.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve details of RDS clusters and their associated security groups." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Assign a custom security group to the RDS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure that the custom security group allows only authorized traffic." - }, - { - "name": "RDSEnhancedMonitoringEnabled", - "description": "Ensures that enhanced monitoring is enabled for RDS instances.", - "priority": 2, - "priorityReason": "Enhanced monitoring provides deeper insights into database performance and resource usage.", - "awsService": "RDS", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "monitoringRoleArn", - "description": "The ARN of the IAM role used for enhanced monitoring.", - "default": "", - "example": "arn:aws:iam::123456789012:role/RDSMonitoringRole" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBInstancesCommand", - "reason": "Retrieve details of RDS instances to check monitoring settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBInstanceCommand", - "reason": "Enable enhanced monitoring for the RDS instance." - } - ], - "adviseBeforeFixFunction": "Ensure the monitoring IAM role is properly configured with the required permissions." - }, - { - "name": "RDSInstancePublicAccessCheck", - "description": "Ensures that RDS instances are not publicly accessible.", - "priority": 1, - "priorityReason": "Restricting public access reduces the risk of unauthorized access to databases.", - "awsService": "RDS", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBInstancesCommand", - "reason": "Retrieve details of RDS instances to check public accessibility." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBInstanceCommand", - "reason": "Disable public accessibility for the RDS instance." - } - ], - "adviseBeforeFixFunction": "Verify that applications accessing the database are within the same VPC or have secure connectivity." - }, - { - "name": "RDSLoggingEnabled", - "description": "Ensures that RDS clusters have logging enabled for supported log types.", - "priority": 1, - "priorityReason": "Enabling logging provides visibility into database activity and assists with compliance and debugging.", - "awsService": "RDS", - "awsServiceCategory": "Clusters", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "logTypes", - "description": "The list of log types to enable for the RDS cluster.", - "default": "", - "example": "[\"audit\", \"error\", \"general\", \"slowquery\"]" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClustersCommand", - "reason": "Retrieve details of RDS clusters to check their logging settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyDBClusterCommand", - "reason": "Enable logging for the RDS cluster." - } - ], - "adviseBeforeFixFunction": "Ensure that the enabled log types align with monitoring and compliance requirements." - }, - { - "name": "RDSSnapshotEncrypted", - "description": "Ensures that RDS snapshots are encrypted.", - "priority": 1, - "priorityReason": "Encrypting snapshots protects sensitive data stored in backups.", - "awsService": "RDS", - "awsServiceCategory": "Snapshots", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyId", - "description": "The KMS key ID or ARN to use for snapshot encryption.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeDBClusterSnapshotsCommand", - "reason": "Retrieve details of RDS snapshots to check their encryption status." - } - ], - "commandUsedInFixFunction": [ - { - "name": "CopyDBClusterSnapshotCommand", - "reason": "Create an encrypted copy of an unencrypted snapshot." - } - ], - "adviseBeforeFixFunction": "Ensure the KMS key is configured with the appropriate permissions for snapshot encryption." - }, - { - "name": "ElastiCacheReplGrpAutoFailoverEnabled", - "description": "Ensures that automatic failover is enabled for ElastiCache replication groups.", - "priority": 1, - "priorityReason": "Automatic failover provides high availability and reduces downtime in case of failures.", - "awsService": "ElastiCache", - "awsServiceCategory": "Replication Groups", - "bestPracticeCategory": "Reliability", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeReplicationGroupsCommand", - "reason": "Retrieve details of ElastiCache replication groups to check their failover settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyReplicationGroupCommand", - "reason": "Enable automatic failover for the replication group." - } - ], - "adviseBeforeFixFunction": "Ensure that the replication group is configured for high availability." - }, - { - "name": "ElastiCacheReplGrpEncryptedAtRest", - "description": "Ensures that ElastiCache replication groups are encrypted at rest.", - "priority": 1, - "priorityReason": "Encrypting data at rest protects it from unauthorized access in storage.", - "awsService": "ElastiCache", - "awsServiceCategory": "Replication Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeReplicationGroupsCommand", - "reason": "Retrieve details of ElastiCache replication groups to check their encryption settings." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "Encryption at rest must be enabled at the time of cluster creation." - }, - { - "name": "ElastiCacheReplGrpEncryptedInTransit", - "description": "Ensures that ElastiCache replication groups are encrypted in transit.", - "priority": 1, - "priorityReason": "Encrypting data in transit protects it from interception during communication.", - "awsService": "ElastiCache", - "awsServiceCategory": "Replication Groups", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeReplicationGroupsCommand", - "reason": "Retrieve details of ElastiCache replication groups to check their in-transit encryption settings." - } - ], - "commandUsedInFixFunction": [], - "adviseBeforeFixFunction": "In-transit encryption must be enabled at the time of cluster creation." - }, - { - "name": "ECSAwsVpcNetworkingEnabled", - "description": "Ensures that ECS task definitions use the awsvpc networking mode.", - "priority": 1, - "priorityReason": "Using awsvpc networking mode ensures that tasks receive their own elastic network interfaces for enhanced security.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Networking", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve details of ECS task definitions to check their network mode." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Re-register the task definition with the awsvpc networking mode." - } - ], - "adviseBeforeFixFunction": "Ensure that the VPC and subnets are configured to support the awsvpc networking mode." - }, - { - "name": "ECSContainersReadonlyAccess", - "description": "Ensures that ECS containers are configured with read-only root file systems.", - "priority": 2, - "priorityReason": "Using read-only root file systems reduces the risk of unauthorized changes to the container's file system.", - "awsService": "ECS", - "awsServiceCategory": "Task Definitions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTaskDefinitionCommand", - "reason": "Retrieve details of ECS task definitions to check container file system permissions." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RegisterTaskDefinitionCommand", - "reason": "Re-register the task definition with read-only root file systems for containers." - } - ], - "adviseBeforeFixFunction": "Verify that the application does not require write access to the container's root file system." - }, - { - "name": "ECSFargateLatestPlatformVersion", - "description": "Ensures that ECS services use the latest Fargate platform version.", - "priority": 2, - "priorityReason": "Using the latest platform version ensures access to the latest features and bug fixes.", - "awsService": "ECS", - "awsServiceCategory": "Services", - "bestPracticeCategory": "Maintenance", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeServicesCommand", - "reason": "Retrieve details of ECS services to check their platform version." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateServiceCommand", - "reason": "Update the service to use the latest Fargate platform version." - } - ], - "adviseBeforeFixFunction": "Ensure that updating the platform version does not disrupt service operations." - }, - { - "name": "ECRKmsEncryption1", - "description": "Ensures that ECR repositories are encrypted using KMS keys.", - "priority": 1, - "priorityReason": "Encrypting ECR repositories with KMS keys protects sensitive data from unauthorized access.", - "awsService": "ECR", - "awsServiceCategory": "Repositories", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "kmsKeyId", - "description": "The KMS key ID or ARN to use for encryption.", - "default": "", - "example": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeRepositoriesCommand", - "reason": "Retrieve details of ECR repositories to check their encryption settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutEncryptionConfigurationCommand", - "reason": "Enable KMS encryption for the ECR repository." - } - ], - "adviseBeforeFixFunction": "Ensure the KMS key is properly configured with permissions to encrypt and decrypt ECR repository data." - }, - { - "name": "EC2EbsEncryptionByDefault", - "description": "Ensures that EBS volumes are encrypted by default.", - "priority": 1, - "priorityReason": "Default encryption ensures all newly created EBS volumes are protected by encryption.", - "awsService": "EC2", - "awsServiceCategory": "EBS", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetEbsEncryptionByDefaultCommand", - "reason": "Check if EBS encryption by default is enabled." - } - ], - "commandUsedInFixFunction": [ - { - "name": "EnableEbsEncryptionByDefaultCommand", - "reason": "Enable EBS encryption by default." - } - ], - "adviseBeforeFixFunction": "Ensure that encryption requirements align with organizational security policies." - }, - { - "name": "EC2Imdsv2Check", - "description": "Ensures that EC2 instances require IMDSv2 for metadata access.", - "priority": 1, - "priorityReason": "Requiring IMDSv2 improves instance metadata security by preventing SSRF attacks.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Retrieve details of EC2 instances to check their metadata options." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyInstanceMetadataOptionsCommand", - "reason": "Enforce IMDSv2 on EC2 instances." - } - ], - "adviseBeforeFixFunction": "Verify that applications using instance metadata are compatible with IMDSv2." - }, - { - "name": "EC2InstanceDetailedMonitoringEnabled", - "description": "Ensures that EC2 instances have detailed monitoring enabled.", - "priority": 2, - "priorityReason": "Detailed monitoring provides granular metrics for resource usage and performance analysis.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Retrieve details of EC2 instances to check their monitoring state." - } - ], - "commandUsedInFixFunction": [ - { - "name": "MonitorInstancesCommand", - "reason": "Enable detailed monitoring on EC2 instances." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling detailed monitoring aligns with operational requirements." - }, - { - "name": "EC2InstanceManagedBySystemsManager", - "description": "Ensures that EC2 instances are managed by AWS Systems Manager.", - "priority": 2, - "priorityReason": "Using Systems Manager simplifies management tasks such as patching, configuration, and automation.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Management", - "requiredParametersForFix": [ - { - "name": "iamRole", - "description": "The IAM role to attach to the instance for Systems Manager.", - "default": "", - "example": "arn:aws:iam::123456789012:role/SSMRole" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstanceInformationCommand", - "reason": "Check if instances are registered with Systems Manager." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AttachIamInstanceProfileCommand", - "reason": "Attach an IAM role that enables Systems Manager to manage the instance." - } - ], - "adviseBeforeFixFunction": "Ensure the IAM role has the necessary permissions for Systems Manager operations." - }, - { - "name": "EC2NoAmazonKeyPair", - "description": "Ensures that EC2 instances do not use Amazon-provided key pairs for authentication.", - "priority": 1, - "priorityReason": "Using custom key pairs ensures that access to EC2 instances is controlled by the organization.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "newKeyPair", - "description": "The custom key pair to use for the EC2 instance.", - "default": "", - "example": "my-custom-keypair" - } - ], - "isFixFunctionUsesDestructiveCommand": true, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Retrieve details of EC2 instances to check their key pair settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "RecreateInstanceWithNewKeyPairCommand", - "reason": "Recreate the instance with a custom key pair." - } - ], - "adviseBeforeFixFunction": "Ensure that the new key pair is securely stored and accessible." - }, - { - "name": "EC2TokenHopLimitCheck", - "description": "Ensures that EC2 instance metadata service has a low token hop limit configured.", - "priority": 2, - "priorityReason": "Reducing the hop limit minimizes the risk of metadata interception in multi-hop scenarios.", - "awsService": "EC2", - "awsServiceCategory": "Instances", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "hopLimit", - "description": "The maximum number of hops allowed for the metadata service.", - "default": "1", - "example": "1" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeInstancesCommand", - "reason": "Retrieve details of EC2 instances to check their metadata service hop limit." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyInstanceMetadataOptionsCommand", - "reason": "Set the hop limit for the instance metadata service." - } - ], - "adviseBeforeFixFunction": "Ensure that the hop limit setting does not interfere with legitimate application behavior." - }, - { - "name": "DynamoDBTableEncryptionEnabled", - "description": "Ensures that DynamoDB tables are encrypted at rest.", - "priority": 1, - "priorityReason": "Encrypting DynamoDB tables protects sensitive data stored in the database.", - "awsService": "DynamoDB", - "awsServiceCategory": "Tables", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeTableCommand", - "reason": "Retrieve details of DynamoDB tables to check their encryption settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateTableCommand", - "reason": "Enable encryption for the DynamoDB table." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling encryption aligns with your organization's data security policies." - }, - { - "name": "CWLogGroupRetentionPeriodCheck", - "description": "Ensures that CloudWatch log groups have a defined retention period.", - "priority": 2, - "priorityReason": "Defining a retention period reduces storage costs and ensures logs are not kept indefinitely.", - "awsService": "CloudWatch", - "awsServiceCategory": "Logs", - "bestPracticeCategory": "Cost Optimization", - "requiredParametersForFix": [ - { - "name": "retentionDays", - "description": "The number of days to retain log data.", - "default": "30", - "example": "7" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeLogGroupsCommand", - "reason": "Retrieve details of CloudWatch log groups to check their retention settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "PutRetentionPolicyCommand", - "reason": "Set the retention period for CloudWatch log groups." - } - ], - "adviseBeforeFixFunction": "Choose a retention period that balances storage costs and compliance requirements." - }, - { - "name": "CloudFrontS3OriginAccessControlEnabled", - "description": "Ensures that CloudFront distributions with S3 origins have origin access control enabled.", - "priority": 1, - "priorityReason": "Using origin access control restricts access to S3 buckets, enhancing security.", - "awsService": "CloudFront", - "awsServiceCategory": "Distributions", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetDistributionCommand", - "reason": "Retrieve CloudFront distribution configurations to check origin access settings." - } - ], - "commandUsedInFixFunction": [ - { - "name": "UpdateDistributionCommand", - "reason": "Enable origin access control for CloudFront distributions." - } - ], - "adviseBeforeFixFunction": "Ensure that enabling origin access control does not disrupt existing functionality." - }, - { - "name": "ALBWAFEnabled", - "description": "Ensures that WAF is associated with ALBs.", - "priority": 1, - "priorityReason": "Associating WAF with ALBs protects against common web attacks.", - "awsService": "Elastic Load Balancing", - "awsServiceCategory": "Application Load Balancer", - "bestPracticeCategory": "Security", - "requiredParametersForFix": [ - { - "name": "webAclArn", - "description": "The ARN of the WAF ACL to associate with the ALB.", - "default": "", - "example": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "GetWebAclForResourceCommand", - "reason": "Check if a WAF is associated with the ALB." - } - ], - "commandUsedInFixFunction": [ - { - "name": "AssociateWebAclCommand", - "reason": "Associate a WAF ACL with the ALB." - } - ], - "adviseBeforeFixFunction": "Ensure the WAF ACL has the appropriate rules for the application's requirements." - }, - { - "name": "ELBCrossZoneLoadBalancingEnabled", - "description": "Ensures that cross-zone load balancing is enabled for load balancers.", - "priority": 2, - "priorityReason": "Cross-zone load balancing distributes traffic evenly across all registered targets.", - "awsService": "Elastic Load Balancing", - "awsServiceCategory": "Load Balancer", - "bestPracticeCategory": "Reliability", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeLoadBalancerAttributesCommand", - "reason": "Check if cross-zone load balancing is enabled for load balancers." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyLoadBalancerAttributesCommand", - "reason": "Enable cross-zone load balancing for load balancers." - } - ], - "adviseBeforeFixFunction": "Ensure enabling cross-zone load balancing aligns with traffic distribution goals." - }, - { - "name": "ELBDeletionProtectionEnabled", - "description": "Ensures that deletion protection is enabled for load balancers.", - "priority": 1, - "priorityReason": "Enabling deletion protection prevents accidental deletion of load balancers.", - "awsService": "Elastic Load Balancing", - "awsServiceCategory": "Load Balancer", - "bestPracticeCategory": "Reliability", - "requiredParametersForFix": [], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeLoadBalancerAttributesCommand", - "reason": "Check if deletion protection is enabled for load balancers." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyLoadBalancerAttributesCommand", - "reason": "Enable deletion protection for load balancers." - } - ], - "adviseBeforeFixFunction": "Verify that deletion protection is necessary for the load balancer's lifecycle management." - }, - { - "name": "ELBLoggingEnabled", - "description": "Ensures that access logs are enabled for load balancers.", - "priority": 1, - "priorityReason": "Enabling access logs helps with debugging and analyzing traffic patterns.", - "awsService": "Elastic Load Balancing", - "awsServiceCategory": "Load Balancer", - "bestPracticeCategory": "Monitoring", - "requiredParametersForFix": [ - { - "name": "s3BucketName", - "description": "The S3 bucket to store access logs.", - "default": "", - "example": "my-logs-bucket" - } - ], - "isFixFunctionUsesDestructiveCommand": false, - "commandUsedInCheckFunction": [ - { - "name": "DescribeLoadBalancerAttributesCommand", - "reason": "Check if access logging is enabled for load balancers." - } - ], - "commandUsedInFixFunction": [ - { - "name": "ModifyLoadBalancerAttributesCommand", - "reason": "Enable access logs for load balancers." - } - ], - "adviseBeforeFixFunction": "Ensure that the specified S3 bucket exists and has permissions to receive access logs." - } -] diff --git a/package.json b/package.json index cb70194..df44ce9 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,7 @@ }, "pkg": { "scripts": "build/**/*", - "assets": [ - "views/**/*", - "bpset_metadata.json" - ], + "assets": "views/**/*", "targets": [ "node22-linuxstatic-x86_64", "node22-linuxstatic-arm64" diff --git a/src/BPManager.ts b/src/BPManager.ts index 6183533..c96e2e1 100644 --- a/src/BPManager.ts +++ b/src/BPManager.ts @@ -1,5 +1,5 @@ -import { BPSet, BPSetMetadata } from "./types"; -import { readdir, readFile } from 'node:fs/promises' +import { BPSet } from "./types"; +import { readdir } from 'node:fs/promises' import path from 'node:path' export class BPManager { @@ -13,12 +13,8 @@ export class BPManager { private readonly bpSets: Record = {} - private readonly bpSetMetadatas: - Record = {} - private constructor() { this.loadBPSets() - this.loadBPSetMetadatas() } private async loadBPSets() { @@ -39,72 +35,20 @@ export class BPManager { } } - private async loadBPSetMetadatas() { - const bpSetMetadatasRaw = await readFile(path.join(__dirname, '../bpset_metadata.json')) - const bpSetMetadatas = JSON.parse(bpSetMetadatasRaw.toString('utf-8')) as BPSetMetadata[] - - for (const [idx, bpSetMetadata] of bpSetMetadatas.entries()) { - this.bpSetMetadatas[bpSetMetadata.name] = { - ...bpSetMetadata, - nonCompliantResources: [], - compliantResources: [], - status:'LOADED', - errorMessage: [], - idx - } - } - } - public runCheckOnce(name: string) { - return this - .bpSets[name].check() - .catch((err) => { - this.bpSetMetadatas[name].status = 'ERROR' - this.bpSetMetadatas[name].errorMessage.push({ - date: new Date(), - message: err - }) - - return undefined - }) - .then((result) => { - if (result === undefined) - return - - this.bpSetMetadatas[name].compliantResources = result.compliantResources - this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources - this.bpSetMetadatas[name].status = 'FINISHED' - }) + return this.bpSets[name].check() } public runCheckAll(finished = (name: string) => {}) { - const checkJobs = - Object - .values(this.bpSetMetadatas) - .map(({ name }) => { - this.bpSetMetadatas[name].status = 'CHECKING' + const checkJobs: Promise[] = [] - return this - .bpSets[name].check() - .catch((err) => { - this.bpSetMetadatas[name].status = 'ERROR' - this.bpSetMetadatas[name].errorMessage.push({ - date: new Date(), - message: err - }) - - return undefined - }) - .then((result) => { - if (result === undefined) - return - - this.bpSetMetadatas[name].compliantResources = result.compliantResources - this.bpSetMetadatas[name].nonCompliantResources = result.nonCompliantResources - this.bpSetMetadatas[name].status = 'FINISHED' - finished(name) - }) - }) + for (const bpset of Object.values(this.bpSets)) + checkJobs.push( + bpset + .check() + .then(() => + finished(bpset.getMetadata().name)) + ) return Promise.all(checkJobs) } @@ -113,20 +57,11 @@ export class BPManager { return this .bpSets[name] .fix( - this.bpSetMetadatas[name].nonCompliantResources, + this.bpSets[name].getStats().nonCompliantResources, requiredParametersForFix ) } - public readonly getBPSet = (name: string) => - this.bpSets[name] - - public readonly getBPSetMetadata = (name: string) => - this.bpSetMetadatas[name] - public readonly getBPSets = () => Object.values(this.bpSets) - - public readonly getBPSetMetadatas = () => - Object.values(this.bpSetMetadatas) } diff --git a/src/WebServer.ts b/src/WebServer.ts index 99fb629..dc8d7c8 100644 --- a/src/WebServer.ts +++ b/src/WebServer.ts @@ -1,6 +1,6 @@ import express, { Request, Response } from 'express' import { BPManager } from './BPManager' -import { BPSetMetadata } from './types' +import { BPSet, BPSetMetadata, BPSetStats } from './types' import { Memorizer } from './Memorizer' import path from 'path' @@ -28,21 +28,24 @@ export class WebServer { } private getMainPage(req: Request, res: Response) { + const hidePass = req.query['hidePass'] === 'true' const bpStatus: { category: string, - metadatas: BPSetMetadata[] + metadatas: (BPSetMetadata&BPSetStats)[] }[] = [] - const bpMetadatas = this.bpManager.getBPSetMetadatas() - const categories = new Set(bpMetadatas.map((v) => v?.awsService)) - const hidePass = req.query['hidePass'] === 'true' + const bpMetadatas = this.bpManager.getBPSets().map((v, idx) => ({ ...v, idx })) + const categories = new Set(bpMetadatas.map((v) => v.getMetadata().awsService)) for (const category of categories) bpStatus.push({ category, - metadatas: bpMetadatas.filter((v) => - v.awsService === category && - (!hidePass || v.nonCompliantResources.length > 0)) + metadatas: + bpMetadatas + .filter((v) => + v.getMetadata().awsService === category && + (!hidePass || v.getStats().nonCompliantResources.length > 0)) + .map((v) => ({ ...v.getMetadata(), ...v.getStats(), idx: v.idx })) }) res.render('index', { diff --git a/src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts b/src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts index 70ac514..6552486 100644 --- a/src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts +++ b/src/bpsets/alb/ALBHttpDropInvalidHeaderEnabled.ts @@ -2,71 +2,129 @@ import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand, DescribeLoadBalancerAttributesCommand, - ModifyLoadBalancerAttributesCommand -} from '@aws-sdk/client-elastic-load-balancing-v2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyLoadBalancerAttributesCommand, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ALBHttpDropInvalidHeaderEnabled implements BPSet { - private readonly client = new ElasticLoadBalancingV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElasticLoadBalancingV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getLoadBalancers = async () => { - const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})) - return response.LoadBalancers || [] - } + const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); + return response.LoadBalancers || []; + }; - private readonly getLoadBalancerAttributes = async ( - loadBalancerArn: string - ) => { + private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { const response = await this.memoClient.send( new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) - ) - return response.Attributes || [] - } + ); + return response.Attributes || []; + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const loadBalancers = await this.getLoadBalancers() + public readonly getMetadata = () => ({ + name: 'ALBHttpDropInvalidHeaderEnabled', + description: 'Ensures that ALBs have invalid HTTP headers dropped.', + priority: 1, + priorityReason: 'Dropping invalid headers enhances security and avoids unexpected behavior.', + awsService: 'Elastic Load Balancing', + awsServiceCategory: 'Application Load Balancer', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeLoadBalancerAttributesCommand', + reason: 'Verify if invalid headers are dropped for ALBs.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyLoadBalancerAttributesCommand', + reason: 'Enable the invalid header drop attribute on ALBs.', + }, + ], + adviseBeforeFixFunction: 'Ensure enabling this setting aligns with your application requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const loadBalancers = await this.getLoadBalancers(); for (const lb of loadBalancers) { - const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!) + const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const isEnabled = attributes.some( - attr => attr.Key === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true' - ) + (attr) => + attr.Key === 'routing.http.drop_invalid_header_fields.enabled' && attr.Value === 'true' + ); if (isEnabled) { - compliantResources.push(lb.LoadBalancerArn!) + compliantResources.push(lb.LoadBalancerArn!); } else { - nonCompliantResources.push(lb.LoadBalancerArn!) + nonCompliantResources.push(lb.LoadBalancerArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const lbArn of nonCompliantResources) { await this.client.send( new ModifyLoadBalancerAttributesCommand({ LoadBalancerArn: lbArn, Attributes: [ - { Key: 'routing.http.drop_invalid_header_fields.enabled', Value: 'true' } - ] + { Key: 'routing.http.drop_invalid_header_fields.enabled', Value: 'true' }, + ], }) - ) + ); } - } + }; } diff --git a/src/bpsets/alb/ALBWAFEnabled.ts b/src/bpsets/alb/ALBWAFEnabled.ts index 81b29d5..9bbf341 100644 --- a/src/bpsets/alb/ALBWAFEnabled.ts +++ b/src/bpsets/alb/ALBWAFEnabled.ts @@ -1,6 +1,6 @@ import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand } from '@aws-sdk/client-elastic-load-balancing-v2' import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2' -import { BPSet } from '../../types' +import { BPSet, BPSetFixFn, BPSetStats } from '../../types' import { Memorizer } from '../../Memorizer' export class ALBWAFEnabled implements BPSet { @@ -13,11 +13,73 @@ export class ALBWAFEnabled implements BPSet { return response.LoadBalancers || [] } - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { + public readonly getMetadata = () => ( + { + name: 'ALBWAFEnabled', + description: 'Ensures that WAF is associated with ALBs.', + priority: 1, + priorityReason: 'Associating WAF with ALBs protects against common web attacks.', + awsService: 'Elastic Load Balancing', + awsServiceCategory: 'Application Load Balancer', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'web-acl-arn', + description: 'The ARN of the WAF ACL to associate with the ALB.', + default: '', + example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example' + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetWebAclForResourceCommand', + reason: 'Check if a WAF is associated with the ALB.' + } + ], + commandUsedInFixFunction: [ + { + name: 'AssociateWebAclCommand', + reason: 'Associate a WAF ACL with the ALB.' + } + ], + adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.' + }) + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [] + } + + public readonly getStats = () => + this.stats + + public readonly clearStats = () => { + this.stats.compliantResources = [] + this.stats.nonCompliantResources = [] + this.stats.status = 'LOADED' + this.stats.errorMessage = [] + } + + public readonly check = async () => { + this.stats.status = 'CHECKING' + + await this.checkImpl() + .then( + () => this.stats.status = 'FINISHED', + (err) => { + this.stats.status = 'ERROR' + this.stats.errorMessage.push({ + date: new Date(), + message: err + } + ) + }) + } + + private readonly checkImpl = async () => { const compliantResources: string[] = [] const nonCompliantResources: string[] = [] const loadBalancers = await this.getLoadBalancers() @@ -33,17 +95,27 @@ export class ALBWAFEnabled implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'web-acl-arn' }] - } + this.stats.compliantResources = compliantResources + this.stats.nonCompliantResources = nonCompliantResources + this.stats.status = 'FINISHED' } - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args) + .then( + () => this.stats.status = 'FINISHED', + (err) => { + this.stats.status = 'ERROR' + this.stats.errorMessage.push({ + date: new Date(), + message: err + } + ) + }) + } + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value if (!webAclArn) { diff --git a/src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts b/src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts index 14e626f..48b68a8 100644 --- a/src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts +++ b/src/bpsets/alb/ELBCrossZoneLoadBalancingEnabled.ts @@ -2,63 +2,127 @@ import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand, DescribeLoadBalancerAttributesCommand, - ModifyLoadBalancerAttributesCommand -} from '@aws-sdk/client-elastic-load-balancing-v2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyLoadBalancerAttributesCommand, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ELBCrossZoneLoadBalancingEnabled implements BPSet { - private readonly client = new ElasticLoadBalancingV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElasticLoadBalancingV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getLoadBalancers = async () => { - const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})) - return response.LoadBalancers || [] - } + const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); + return response.LoadBalancers || []; + }; private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { const response = await this.memoClient.send( new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) - ) - return response.Attributes || [] - } + ); + return response.Attributes || []; + }; + + public readonly getMetadata = () => ({ + name: 'ELBCrossZoneLoadBalancingEnabled', + description: 'Ensures that cross-zone load balancing is enabled for Elastic Load Balancers.', + priority: 2, + priorityReason: 'Cross-zone load balancing helps evenly distribute traffic, improving resilience.', + awsService: 'Elastic Load Balancing', + awsServiceCategory: 'Classic Load Balancer', + bestPracticeCategory: 'Performance', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeLoadBalancerAttributesCommand', + reason: 'Verify if cross-zone load balancing is enabled for ELBs.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyLoadBalancerAttributesCommand', + reason: 'Enable cross-zone load balancing for ELBs.', + }, + ], + adviseBeforeFixFunction: 'Ensure this setting aligns with your traffic distribution requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const loadBalancers = await this.getLoadBalancers() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const loadBalancers = await this.getLoadBalancers(); for (const lb of loadBalancers) { - const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!) + const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const isEnabled = attributes.some( - attr => attr.Key === 'load_balancing.cross_zone.enabled' && attr.Value === 'true' - ) + (attr) => + attr.Key === 'load_balancing.cross_zone.enabled' && attr.Value === 'true' + ); if (isEnabled) { - compliantResources.push(lb.LoadBalancerArn!) + compliantResources.push(lb.LoadBalancerArn!); } else { - nonCompliantResources.push(lb.LoadBalancerArn!) + nonCompliantResources.push(lb.LoadBalancerArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const lbArn of nonCompliantResources) { await this.client.send( new ModifyLoadBalancerAttributesCommand({ LoadBalancerArn: lbArn, - Attributes: [{ Key: 'load_balancing.cross_zone.enabled', Value: 'true' }] + Attributes: [{ Key: 'load_balancing.cross_zone.enabled', Value: 'true' }], }) - ) + ); } - } + }; } diff --git a/src/bpsets/alb/ELBDeletionProtectionEnabled.ts b/src/bpsets/alb/ELBDeletionProtectionEnabled.ts index c566e79..d66afa4 100644 --- a/src/bpsets/alb/ELBDeletionProtectionEnabled.ts +++ b/src/bpsets/alb/ELBDeletionProtectionEnabled.ts @@ -2,63 +2,127 @@ import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand, DescribeLoadBalancerAttributesCommand, - ModifyLoadBalancerAttributesCommand -} from '@aws-sdk/client-elastic-load-balancing-v2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyLoadBalancerAttributesCommand, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ELBDeletionProtectionEnabled implements BPSet { - private readonly client = new ElasticLoadBalancingV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElasticLoadBalancingV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getLoadBalancers = async () => { - const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})) - return response.LoadBalancers || [] - } + const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); + return response.LoadBalancers || []; + }; private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { const response = await this.memoClient.send( new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) - ) - return response.Attributes || [] - } + ); + return response.Attributes || []; + }; + + public readonly getMetadata = () => ({ + name: 'ELBDeletionProtectionEnabled', + description: 'Ensures that deletion protection is enabled for Elastic Load Balancers.', + priority: 1, + priorityReason: 'Deletion protection prevents accidental deletion of critical resources.', + awsService: 'Elastic Load Balancing', + awsServiceCategory: 'Classic Load Balancer', + bestPracticeCategory: 'Resilience', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeLoadBalancerAttributesCommand', + reason: 'Verify if deletion protection is enabled for ELBs.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyLoadBalancerAttributesCommand', + reason: 'Enable deletion protection for ELBs.', + }, + ], + adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with resource management policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const loadBalancers = await this.getLoadBalancers() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const loadBalancers = await this.getLoadBalancers(); for (const lb of loadBalancers) { - const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!) + const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const isEnabled = attributes.some( - attr => attr.Key === 'deletion_protection.enabled' && attr.Value === 'true' - ) + (attr) => + attr.Key === 'deletion_protection.enabled' && attr.Value === 'true' + ); if (isEnabled) { - compliantResources.push(lb.LoadBalancerArn!) + compliantResources.push(lb.LoadBalancerArn!); } else { - nonCompliantResources.push(lb.LoadBalancerArn!) + nonCompliantResources.push(lb.LoadBalancerArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const lbArn of nonCompliantResources) { await this.client.send( new ModifyLoadBalancerAttributesCommand({ LoadBalancerArn: lbArn, - Attributes: [{ Key: 'deletion_protection.enabled', Value: 'true' }] + Attributes: [{ Key: 'deletion_protection.enabled', Value: 'true' }], }) - ) + ); } - } + }; } diff --git a/src/bpsets/alb/ELBLoggingEnabled.ts b/src/bpsets/alb/ELBLoggingEnabled.ts index 5fd0263..b502cc5 100644 --- a/src/bpsets/alb/ELBLoggingEnabled.ts +++ b/src/bpsets/alb/ELBLoggingEnabled.ts @@ -2,61 +2,127 @@ import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand, DescribeLoadBalancerAttributesCommand, - ModifyLoadBalancerAttributesCommand -} from '@aws-sdk/client-elastic-load-balancing-v2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyLoadBalancerAttributesCommand, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ELBLoggingEnabled implements BPSet { - private readonly client = new ElasticLoadBalancingV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElasticLoadBalancingV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getLoadBalancers = async () => { - const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})) - return response.LoadBalancers || [] - } + const response = await this.memoClient.send(new DescribeLoadBalancersCommand({})); + return response.LoadBalancers || []; + }; private readonly getLoadBalancerAttributes = async (loadBalancerArn: string) => { const response = await this.memoClient.send( new DescribeLoadBalancerAttributesCommand({ LoadBalancerArn: loadBalancerArn }) - ) - return response.Attributes || [] - } + ); + return response.Attributes || []; + }; + + public readonly getMetadata = () => ({ + name: 'ELBLoggingEnabled', + description: 'Ensures that access logging is enabled for Elastic Load Balancers.', + priority: 1, + priorityReason: 'Access logging provides critical data for troubleshooting and compliance.', + awsService: 'Elastic Load Balancing', + awsServiceCategory: 'Classic Load Balancer', + bestPracticeCategory: 'Logging and Monitoring', + requiredParametersForFix: [ + { name: 's3-bucket-name', description: 'The S3 bucket for storing access logs.', default: '', example: 'my-log-bucket' }, + { name: 's3-prefix', description: 'The S3 prefix for the access logs.', default: '', example: 'elb/logs/' }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeLoadBalancerAttributesCommand', + reason: 'Verify if access logging is enabled for ELBs.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyLoadBalancerAttributesCommand', + reason: 'Enable access logging for ELBs and set S3 bucket and prefix.', + }, + ], + adviseBeforeFixFunction: 'Ensure the specified S3 bucket and prefix exist and are accessible.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const loadBalancers = await this.getLoadBalancers() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const loadBalancers = await this.getLoadBalancers(); for (const lb of loadBalancers) { - const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!) + const attributes = await this.getLoadBalancerAttributes(lb.LoadBalancerArn!); const isEnabled = attributes.some( - attr => attr.Key === 'access_logs.s3.enabled' && attr.Value === 'true' - ) + (attr) => attr.Key === 'access_logs.s3.enabled' && attr.Value === 'true' + ); if (isEnabled) { - compliantResources.push(lb.LoadBalancerArn!) + compliantResources.push(lb.LoadBalancerArn!); } else { - nonCompliantResources.push(lb.LoadBalancerArn!) + nonCompliantResources.push(lb.LoadBalancerArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 's3-bucket-name' }, { name: 's3-prefix' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const bucketName = requiredParametersForFix.find(param => param.name === 's3-bucket-name')?.value - const bucketPrefix = requiredParametersForFix.find(param => param.name === 's3-prefix')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const bucketName = requiredParametersForFix.find((param) => param.name === 's3-bucket-name')?.value; + const bucketPrefix = requiredParametersForFix.find((param) => param.name === 's3-prefix')?.value; if (!bucketName || !bucketPrefix) { - throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing.") + throw new Error("Required parameters 's3-bucket-name' and/or 's3-prefix' are missing."); } for (const lbArn of nonCompliantResources) { @@ -66,10 +132,10 @@ export class ELBLoggingEnabled implements BPSet { Attributes: [ { Key: 'access_logs.s3.enabled', Value: 'true' }, { Key: 'access_logs.s3.bucket', Value: bucketName }, - { Key: 'access_logs.s3.prefix', Value: bucketPrefix } - ] + { Key: 'access_logs.s3.prefix', Value: bucketPrefix }, + ], }) - ) + ); } - } + }; } diff --git a/src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts b/src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts index 7d66ef9..95fe492 100644 --- a/src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts +++ b/src/bpsets/apigw/APIGatewayAssociatedWithWAF.ts @@ -1,70 +1,146 @@ import { ApiGatewayV2Client, GetApisCommand, - GetStagesCommand -} from '@aws-sdk/client-apigatewayv2' -import { WAFV2Client, GetWebACLForResourceCommand, AssociateWebACLCommand } from '@aws-sdk/client-wafv2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + GetStagesCommand, +} from '@aws-sdk/client-apigatewayv2'; +import { + WAFV2Client, + GetWebACLForResourceCommand, + AssociateWebACLCommand, +} from '@aws-sdk/client-wafv2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class APIGatewayAssociatedWithWAF implements BPSet { - private readonly client = new ApiGatewayV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) - private readonly wafClient = Memorizer.memo(new WAFV2Client({})) + private readonly client = new ApiGatewayV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + private readonly wafClient = Memorizer.memo(new WAFV2Client({})); private readonly getHttpApis = async () => { - const response = await this.memoClient.send(new GetApisCommand({})) - return response.Items || [] - } + const response = await this.memoClient.send(new GetApisCommand({})); + return response.Items || []; + }; private readonly getStages = async (apiId: string) => { - const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })) - return response.Items || [] - } + const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); + return response.Items || []; + }; + + public readonly getMetadata = () => ({ + name: 'APIGatewayAssociatedWithWAF', + description: 'Ensures that API Gateway stages are associated with WAF.', + priority: 2, + priorityReason: 'Associating WAF with API Gateway stages enhances security by protecting against web attacks.', + awsService: 'API Gateway', + awsServiceCategory: 'API Management', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'web-acl-arn', + description: 'The ARN of the WAF ACL to associate with the API Gateway stage.', + default: '', + example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetWebACLForResourceCommand', + reason: 'Verify if a WAF is associated with the API Gateway stage.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'AssociateWebACLCommand', + reason: 'Associate a WAF ACL with the API Gateway stage.', + }, + ], + adviseBeforeFixFunction: 'Ensure the WAF ACL has the appropriate rules for the application\'s requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const apis = await this.getHttpApis() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const apis = await this.getHttpApis(); for (const api of apis) { - const stages = await this.getStages(api.ApiId!) + const stages = await this.getStages(api.ApiId!); for (const stage of stages) { - const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}` - const response = await this.wafClient.send(new GetWebACLForResourceCommand({ ResourceArn: stageArn })) + const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`; + const response = await this.wafClient.send( + new GetWebACLForResourceCommand({ ResourceArn: stageArn }) + ); if (response.WebACL) { - compliantResources.push(stageArn) + compliantResources.push(stageArn); } else { - nonCompliantResources.push(stageArn) + nonCompliantResources.push(stageArn); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'web-acl-arn' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const webAclArn = requiredParametersForFix.find(param => param.name === 'web-acl-arn')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const webAclArn = requiredParametersForFix.find((param) => param.name === 'web-acl-arn')?.value; if (!webAclArn) { - throw new Error("Required parameter 'web-acl-arn' is missing.") + throw new Error("Required parameter 'web-acl-arn' is missing."); } for (const stageArn of nonCompliantResources) { await this.wafClient.send( new AssociateWebACLCommand({ ResourceArn: stageArn, - WebACLArn: webAclArn + WebACLArn: webAclArn, }) - ) + ); } - } + }; } diff --git a/src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts b/src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts index 1d0cdd8..dd59042 100644 --- a/src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts +++ b/src/bpsets/apigw/APIGatewayExecutionLoggingEnabled.ts @@ -2,63 +2,135 @@ import { ApiGatewayV2Client, GetApisCommand, GetStagesCommand, - UpdateStageCommand -} from '@aws-sdk/client-apigatewayv2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateStageCommand, +} from '@aws-sdk/client-apigatewayv2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class APIGatewayExecutionLoggingEnabled implements BPSet { - private readonly client = new ApiGatewayV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ApiGatewayV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getHttpApis = async () => { - const response = await this.memoClient.send(new GetApisCommand({})) - return response.Items || [] - } + const response = await this.memoClient.send(new GetApisCommand({})); + return response.Items || []; + }; private readonly getStages = async (apiId: string) => { - const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })) - return response.Items || [] - } + const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); + return response.Items || []; + }; + + public readonly getMetadata = () => ({ + name: 'APIGatewayExecutionLoggingEnabled', + description: 'Ensures that execution logging is enabled for API Gateway stages.', + priority: 3, + priorityReason: 'Execution logging is critical for monitoring and troubleshooting API Gateway usage.', + awsService: 'API Gateway', + awsServiceCategory: 'API Management', + bestPracticeCategory: 'Logging and Monitoring', + requiredParametersForFix: [ + { + name: 'log-destination-arn', + description: 'The ARN of the CloudWatch log group for storing API Gateway logs.', + default: '', + example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetStagesCommand', + reason: 'Verify if execution logging is enabled for API Gateway stages.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateStageCommand', + reason: 'Enable execution logging for API Gateway stages and set the destination log group.', + }, + ], + adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const apis = await this.getHttpApis() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const apis = await this.getHttpApis(); for (const api of apis) { - const stages = await this.getStages(api.ApiId!) + const stages = await this.getStages(api.ApiId!); for (const stage of stages) { - const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}` - const loggingLevel = stage.AccessLogSettings?.Format + const stageArn = `arn:aws:apigateway:${this.client.config.region}::/apis/${api.ApiId}/stages/${stage.StageName}`; + const loggingLevel = stage.AccessLogSettings?.Format; if (loggingLevel && loggingLevel !== 'OFF') { - compliantResources.push(stageArn) + compliantResources.push(stageArn); } else { - nonCompliantResources.push(stageArn) + nonCompliantResources.push(stageArn); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-destination-arn' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const logDestinationArn = requiredParametersForFix.find( + (param) => param.name === 'log-destination-arn' + )?.value; if (!logDestinationArn) { - throw new Error("Required parameter 'log-destination-arn' is missing.") + throw new Error("Required parameter 'log-destination-arn' is missing."); } for (const stageArn of nonCompliantResources) { - const [apiId, stageName] = stageArn.split('/').slice(-2) + const [apiId, stageName] = stageArn.split('/').slice(-2); await this.client.send( new UpdateStageCommand({ @@ -66,10 +138,10 @@ export class APIGatewayExecutionLoggingEnabled implements BPSet { StageName: stageName, AccessLogSettings: { DestinationArn: logDestinationArn, - Format: '$context.requestId' - } + Format: '$context.requestId', + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts b/src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts index d571b6b..1ee7fc6 100644 --- a/src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts +++ b/src/bpsets/apigw/APIGatewayV2AccessLogsEnabled.ts @@ -2,64 +2,136 @@ import { ApiGatewayV2Client, GetApisCommand, GetStagesCommand, - UpdateStageCommand -} from '@aws-sdk/client-apigatewayv2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateStageCommand, +} from '@aws-sdk/client-apigatewayv2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class APIGatewayV2AccessLogsEnabled implements BPSet { - private readonly client = new ApiGatewayV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ApiGatewayV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getHttpApis = async () => { - const response = await this.memoClient.send(new GetApisCommand({})) - return response.Items || [] - } + const response = await this.memoClient.send(new GetApisCommand({})); + return response.Items || []; + }; private readonly getStages = async (apiId: string) => { - const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })) - return response.Items || [] - } + const response = await this.memoClient.send(new GetStagesCommand({ ApiId: apiId })); + return response.Items || []; + }; + + public readonly getMetadata = () => ({ + name: 'APIGatewayV2AccessLogsEnabled', + description: 'Ensures that access logging is enabled for API Gateway v2 stages.', + priority: 3, + priorityReason: 'Access logging provides critical data for monitoring and troubleshooting API Gateway usage.', + awsService: 'API Gateway', + awsServiceCategory: 'API Management', + bestPracticeCategory: 'Logging and Monitoring', + requiredParametersForFix: [ + { + name: 'log-destination-arn', + description: 'The ARN of the CloudWatch log group for storing API Gateway logs.', + default: '', + example: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/logs', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetStagesCommand', + reason: 'Verify if access logging is enabled for API Gateway stages.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateStageCommand', + reason: 'Enable access logging for API Gateway stages and set the destination log group.', + }, + ], + adviseBeforeFixFunction: 'Ensure the CloudWatch log group exists and has the appropriate permissions.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const apis = await this.getHttpApis() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const apis = await this.getHttpApis(); for (const api of apis) { - const stages = await this.getStages(api.ApiId!) + const stages = await this.getStages(api.ApiId!); for (const stage of stages) { - const stageIdentifier = `${api.Name!} / ${stage.StageName!}` + const stageIdentifier = `${api.Name!} / ${stage.StageName!}`; if (!stage.AccessLogSettings) { - nonCompliantResources.push(stageIdentifier) + nonCompliantResources.push(stageIdentifier); } else { - compliantResources.push(stageIdentifier) + compliantResources.push(stageIdentifier); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-destination-arn' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const logDestinationArn = requiredParametersForFix.find(param => param.name === 'log-destination-arn')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const logDestinationArn = requiredParametersForFix.find( + (param) => param.name === 'log-destination-arn' + )?.value; if (!logDestinationArn) { - throw new Error("Required parameter 'log-destination-arn' is missing.") + throw new Error("Required parameter 'log-destination-arn' is missing."); } for (const resource of nonCompliantResources) { - const [apiName, stageName] = resource.split(' / ') - const api = (await this.getHttpApis()).find(a => a.Name === apiName) + const [apiName, stageName] = resource.split(' / '); + const api = (await this.getHttpApis()).find((a) => a.Name === apiName); - if (!api) continue + if (!api) continue; await this.client.send( new UpdateStageCommand({ @@ -67,10 +139,10 @@ export class APIGatewayV2AccessLogsEnabled implements BPSet { StageName: stageName, AccessLogSettings: { DestinationArn: logDestinationArn, - Format: '$context.requestId' - } + Format: '$context.requestId', + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts b/src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts index 034f0d9..004ea6b 100644 --- a/src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts +++ b/src/bpsets/apigw/APIGatewayV2AuthorizationTypeConfigured.ts @@ -2,77 +2,149 @@ import { ApiGatewayV2Client, GetApisCommand, GetRoutesCommand, - UpdateRouteCommand -} from '@aws-sdk/client-apigatewayv2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateRouteCommand, +} from '@aws-sdk/client-apigatewayv2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class APIGatewayV2AuthorizationTypeConfigured implements BPSet { - private readonly client = new ApiGatewayV2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ApiGatewayV2Client({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getHttpApis = async () => { - const response = await this.memoClient.send(new GetApisCommand({})) - return response.Items || [] - } + const response = await this.memoClient.send(new GetApisCommand({})); + return response.Items || []; + }; private readonly getRoutes = async (apiId: string) => { - const response = await this.memoClient.send(new GetRoutesCommand({ ApiId: apiId })) - return response.Items || [] - } + const response = await this.memoClient.send(new GetRoutesCommand({ ApiId: apiId })); + return response.Items || []; + }; + + public readonly getMetadata = () => ({ + name: 'APIGatewayV2AuthorizationTypeConfigured', + description: 'Ensures that authorization type is configured for API Gateway v2 routes.', + priority: 2, + priorityReason: 'Configuring authorization ensures API security by restricting unauthorized access.', + awsService: 'API Gateway', + awsServiceCategory: 'API Management', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'authorization-type', + description: 'The authorization type to configure for the routes.', + default: '', + example: 'JWT', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetRoutesCommand', + reason: 'Verify if authorization type is configured for API Gateway routes.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateRouteCommand', + reason: 'Set the authorization type for API Gateway routes.', + }, + ], + adviseBeforeFixFunction: 'Ensure the chosen authorization type aligns with application requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const apis = await this.getHttpApis() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const apis = await this.getHttpApis(); for (const api of apis) { - const routes = await this.getRoutes(api.ApiId!) + const routes = await this.getRoutes(api.ApiId!); for (const route of routes) { - const routeIdentifier = `${api.Name!} / ${route.RouteKey!}` + const routeIdentifier = `${api.Name!} / ${route.RouteKey!}`; if (route.AuthorizationType === 'NONE') { - nonCompliantResources.push(routeIdentifier) + nonCompliantResources.push(routeIdentifier); } else { - compliantResources.push(routeIdentifier) + compliantResources.push(routeIdentifier); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'authorization-type' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const authorizationType = requiredParametersForFix.find(param => param.name === 'authorization-type')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const authorizationType = requiredParametersForFix.find( + (param) => param.name === 'authorization-type' + )?.value; if (!authorizationType) { - throw new Error("Required parameter 'authorization-type' is missing.") + throw new Error("Required parameter 'authorization-type' is missing."); } for (const resource of nonCompliantResources) { - const [apiName, routeKey] = resource.split(' / ') - const api = (await this.getHttpApis()).find(a => a.Name === apiName) + const [apiName, routeKey] = resource.split(' / '); + const api = (await this.getHttpApis()).find((a) => a.Name === apiName); - if (!api) continue + if (!api) continue; - const routes = await this.getRoutes(api.ApiId!) - const route = routes.find(r => r.RouteKey === routeKey) + const routes = await this.getRoutes(api.ApiId!); + const route = routes.find((r) => r.RouteKey === routeKey); - if (!route) continue + if (!route) continue; await this.client.send( new UpdateRouteCommand({ ApiId: api.ApiId!, - RouteId: route.RouteId!, // Use RouteId instead of RouteKey - AuthorizationType: authorizationType as any + RouteId: route.RouteId!, + AuthorizationType: authorizationType as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts b/src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts index e05751a..463bffe 100644 --- a/src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts +++ b/src/bpsets/asg/AutoScalingGroupELBHealthCheckRequired.ts @@ -1,48 +1,118 @@ -import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +import { + AutoScalingClient, + DescribeAutoScalingGroupsCommand, + UpdateAutoScalingGroupCommand, +} from '@aws-sdk/client-auto-scaling'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class AutoScalingGroupELBHealthCheckRequired implements BPSet { - private readonly client = new AutoScalingClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new AutoScalingClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAutoScalingGroups = async () => { - const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})) - return response.AutoScalingGroups || [] - } + const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); + return response.AutoScalingGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'AutoScalingGroupELBHealthCheckRequired', + description: 'Ensures that Auto Scaling groups with ELB or Target Groups use ELB health checks.', + priority: 2, + priorityReason: 'ELB health checks ensure accurate instance health monitoring in Auto Scaling groups.', + awsService: 'Auto Scaling', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Resilience', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeAutoScalingGroupsCommand', + reason: 'Retrieve Auto Scaling groups to check health check type.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateAutoScalingGroupCommand', + reason: 'Set the health check type to ELB for Auto Scaling groups.', + }, + ], + adviseBeforeFixFunction: 'Ensure that the Auto Scaling group is associated with a functional ELB or Target Group.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const asgs = await this.getAutoScalingGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const asgs = await this.getAutoScalingGroups(); for (const asg of asgs) { if ( (asg.LoadBalancerNames?.length || asg.TargetGroupARNs?.length) && asg.HealthCheckType !== 'ELB' ) { - nonCompliantResources.push(asg.AutoScalingGroupARN!) + nonCompliantResources.push(asg.AutoScalingGroupARN!); } else { - compliantResources.push(asg.AutoScalingGroupARN!) + compliantResources.push(asg.AutoScalingGroupARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const asgArn of nonCompliantResources) { - const asgName = asgArn.split(':').pop()! + const asgName = asgArn.split(':').pop()!; await this.client.send( new UpdateAutoScalingGroupCommand({ AutoScalingGroupName: asgName, - HealthCheckType: 'ELB' + HealthCheckType: 'ELB', }) - ) + ); } - } + }; } diff --git a/src/bpsets/asg/AutoScalingLaunchTemplate.ts b/src/bpsets/asg/AutoScalingLaunchTemplate.ts index 9834a47..b73ed57 100644 --- a/src/bpsets/asg/AutoScalingLaunchTemplate.ts +++ b/src/bpsets/asg/AutoScalingLaunchTemplate.ts @@ -1,58 +1,130 @@ -import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +import { + AutoScalingClient, + DescribeAutoScalingGroupsCommand, + UpdateAutoScalingGroupCommand, +} from '@aws-sdk/client-auto-scaling'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class AutoScalingLaunchTemplate implements BPSet { - private readonly client = new AutoScalingClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new AutoScalingClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAutoScalingGroups = async () => { - const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})) - return response.AutoScalingGroups || [] - } + const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); + return response.AutoScalingGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'AutoScalingLaunchTemplate', + description: 'Ensures that Auto Scaling groups use a launch template instead of a launch configuration.', + priority: 3, + priorityReason: 'Launch templates provide enhanced capabilities and flexibility compared to launch configurations.', + awsService: 'Auto Scaling', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Management', + requiredParametersForFix: [ + { name: 'launch-template-id', description: 'The ID of the launch template to associate.', default: '', example: 'lt-0abcd1234efgh5678' }, + { name: 'version', description: 'The version of the launch template to use.', default: '$Default', example: '$Default' }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeAutoScalingGroupsCommand', + reason: 'Retrieve Auto Scaling groups to check for launch template usage.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateAutoScalingGroupCommand', + reason: 'Associate the Auto Scaling group with the specified launch template.', + }, + ], + adviseBeforeFixFunction: 'Ensure the launch template is configured properly before associating it with an Auto Scaling group.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const asgs = await this.getAutoScalingGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const asgs = await this.getAutoScalingGroups(); for (const asg of asgs) { if (asg.LaunchConfigurationName) { - nonCompliantResources.push(asg.AutoScalingGroupARN!) + nonCompliantResources.push(asg.AutoScalingGroupARN!); } else { - compliantResources.push(asg.AutoScalingGroupARN!) + compliantResources.push(asg.AutoScalingGroupARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'launch-template-id' }, { name: 'version' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const launchTemplateId = requiredParametersForFix.find(param => param.name === 'launch-template-id')?.value - const version = requiredParametersForFix.find(param => param.name === 'version')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const launchTemplateId = requiredParametersForFix.find( + (param) => param.name === 'launch-template-id' + )?.value; + const version = requiredParametersForFix.find((param) => param.name === 'version')?.value; if (!launchTemplateId || !version) { - throw new Error("Required parameters 'launch-template-id' and/or 'version' are missing.") + throw new Error("Required parameters 'launch-template-id' and/or 'version' are missing."); } for (const asgArn of nonCompliantResources) { - const asgName = asgArn.split(':').pop()! + const asgName = asgArn.split(':').pop()!; await this.client.send( new UpdateAutoScalingGroupCommand({ AutoScalingGroupName: asgName, LaunchTemplate: { LaunchTemplateId: launchTemplateId, - Version: version - } + Version: version, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/asg/AutoScalingMultipleAZ.ts b/src/bpsets/asg/AutoScalingMultipleAZ.ts index 3920346..077fac5 100644 --- a/src/bpsets/asg/AutoScalingMultipleAZ.ts +++ b/src/bpsets/asg/AutoScalingMultipleAZ.ts @@ -1,54 +1,130 @@ -import { AutoScalingClient, DescribeAutoScalingGroupsCommand, UpdateAutoScalingGroupCommand } from '@aws-sdk/client-auto-scaling' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +import { + AutoScalingClient, + DescribeAutoScalingGroupsCommand, + UpdateAutoScalingGroupCommand, +} from '@aws-sdk/client-auto-scaling'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class AutoScalingMultipleAZ implements BPSet { - private readonly client = new AutoScalingClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new AutoScalingClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAutoScalingGroups = async () => { - const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})) - return response.AutoScalingGroups || [] - } + const response = await this.memoClient.send(new DescribeAutoScalingGroupsCommand({})); + return response.AutoScalingGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'AutoScalingMultipleAZ', + description: 'Ensures that Auto Scaling groups are configured to use multiple Availability Zones.', + priority: 2, + priorityReason: 'Using multiple AZs improves fault tolerance and availability for Auto Scaling groups.', + awsService: 'Auto Scaling', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Resilience', + requiredParametersForFix: [ + { + name: 'availability-zones', + description: 'Comma-separated list of Availability Zones to assign to the Auto Scaling group.', + default: '', + example: 'us-east-1a,us-east-1b', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeAutoScalingGroupsCommand', + reason: 'Retrieve Auto Scaling groups to verify their Availability Zone configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateAutoScalingGroupCommand', + reason: 'Update the Auto Scaling group to use the specified Availability Zones.', + }, + ], + adviseBeforeFixFunction: 'Ensure that the specified Availability Zones are correctly configured in your infrastructure.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const asgs = await this.getAutoScalingGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const asgs = await this.getAutoScalingGroups(); for (const asg of asgs) { if (asg.AvailabilityZones?.length! > 1) { - compliantResources.push(asg.AutoScalingGroupARN!) + compliantResources.push(asg.AutoScalingGroupARN!); } else { - nonCompliantResources.push(asg.AutoScalingGroupARN!) + nonCompliantResources.push(asg.AutoScalingGroupARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'availability-zones' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const availabilityZones = requiredParametersForFix.find(param => param.name === 'availability-zones')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const availabilityZones = requiredParametersForFix.find( + (param) => param.name === 'availability-zones' + )?.value; if (!availabilityZones) { - throw new Error("Required parameter 'availability-zones' is missing.") + throw new Error("Required parameter 'availability-zones' is missing."); } for (const asgArn of nonCompliantResources) { - const asgName = asgArn.split(':').pop()! + const asgName = asgArn.split(':').pop()!; await this.client.send( new UpdateAutoScalingGroupCommand({ AutoScalingGroupName: asgName, - AvailabilityZones: availabilityZones.split(',') + AvailabilityZones: availabilityZones.split(','), }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts b/src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts index 669eeaf..7dd30f3 100644 --- a/src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts +++ b/src/bpsets/cloudfront/CloudFrontAccessLogsEnabled.ts @@ -2,66 +2,140 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontAccessLogsEnabled implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontAccessLogsEnabled', + description: 'Ensures that access logging is enabled for CloudFront distributions.', + priority: 1, + priorityReason: 'Access logs are critical for monitoring and troubleshooting CloudFront distributions.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Logging and Monitoring', + requiredParametersForFix: [ + { + name: 'log-bucket-name', + description: 'The S3 bucket name for storing access logs.', + default: '', + example: 'my-cloudfront-logs-bucket.s3.amazonaws.com', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions.', + }, + { + name: 'GetDistributionCommand', + reason: 'Retrieve distribution details to check logging settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Enable logging and update distribution settings.', + }, + ], + adviseBeforeFixFunction: 'Ensure the specified S3 bucket exists and has proper permissions for CloudFront logging.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { - const { distribution: details } = await this.getDistributionDetails(distribution.Id!) - if ( - details.DistributionConfig?.Logging?.Enabled - ) { - compliantResources.push(details.ARN!) + const { distribution: details } = await this.getDistributionDetails(distribution.Id!); + if (details.DistributionConfig?.Logging?.Enabled) { + compliantResources.push(details.ARN!); } else { - nonCompliantResources.push(details.ARN!) + nonCompliantResources.push(details.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-bucket-name' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const logBucketName = requiredParametersForFix.find(param => param.name === 'log-bucket-name')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const logBucketName = requiredParametersForFix.find( + (param) => param.name === 'log-bucket-name' + )?.value; if (!logBucketName) { - throw new Error("Required parameter 'log-bucket-name' is missing.") + throw new Error("Required parameter 'log-bucket-name' is missing."); } for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, @@ -69,17 +143,17 @@ export class CloudFrontAccessLogsEnabled implements BPSet { Enabled: true, Bucket: logBucketName, IncludeCookies: false, - Prefix: '' - } - } + Prefix: '', + }, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any // Include all required properties + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts b/src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts index e5c0f60..72b2213 100644 --- a/src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts +++ b/src/bpsets/cloudfront/CloudFrontAssociatedWithWAF.ts @@ -2,76 +2,148 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontAssociatedWithWAF implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontAssociatedWithWAF', + description: 'Ensures that CloudFront distributions are associated with a WAF.', + priority: 1, + priorityReason: 'Associating WAF with CloudFront distributions enhances security against web attacks.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'web-acl-id', + description: 'The ID of the Web ACL to associate with the CloudFront distribution.', + default: '', + example: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/example', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions to check for WAF association.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Associate the specified WAF with the CloudFront distribution.', + }, + ], + adviseBeforeFixFunction: 'Ensure the Web ACL is configured correctly for the application’s security requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { if (distribution.WebACLId && distribution.WebACLId !== '') { - compliantResources.push(distribution.ARN!) + compliantResources.push(distribution.ARN!); } else { - nonCompliantResources.push(distribution.ARN!) + nonCompliantResources.push(distribution.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'web-acl-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const webAclId = requiredParametersForFix.find(param => param.name === 'web-acl-id')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const webAclId = requiredParametersForFix.find( + (param) => param.name === 'web-acl-id' + )?.value; if (!webAclId) { - throw new Error("Required parameter 'web-acl-id' is missing.") + throw new Error("Required parameter 'web-acl-id' is missing."); } for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, - WebACLId: webAclId - } + WebACLId: webAclId, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any // Include all required properties + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontDefaultRootObjectConfigured.ts b/src/bpsets/cloudfront/CloudFrontDefaultRootObjectConfigured.ts index 0f02464..4eda6d1 100644 --- a/src/bpsets/cloudfront/CloudFrontDefaultRootObjectConfigured.ts +++ b/src/bpsets/cloudfront/CloudFrontDefaultRootObjectConfigured.ts @@ -2,77 +2,153 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontDefaultRootObjectConfigured implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontDefaultRootObjectConfigured', + description: 'Ensures that CloudFront distributions have a default root object configured.', + priority: 3, + priorityReason: 'A default root object ensures users access the correct content when navigating to the distribution domain.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Configuration', + requiredParametersForFix: [ + { + name: 'default-root-object', + description: 'The default root object for the CloudFront distribution.', + default: '', + example: 'index.html', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions to check for a default root object.', + }, + { + name: 'GetDistributionCommand', + reason: 'Retrieve distribution details to verify the default root object setting.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Set the default root object for the CloudFront distribution.', + }, + ], + adviseBeforeFixFunction: 'Ensure the default root object exists in the origin to avoid 404 errors.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { - const { distribution: details } = await this.getDistributionDetails(distribution.Id!) + const { distribution: details } = await this.getDistributionDetails(distribution.Id!); if (details.DistributionConfig?.DefaultRootObject !== '') { - compliantResources.push(details.ARN!) + compliantResources.push(details.ARN!); } else { - nonCompliantResources.push(details.ARN!) + nonCompliantResources.push(details.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'default-root-object' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const defaultRootObject = requiredParametersForFix.find(param => param.name === 'default-root-object')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const defaultRootObject = requiredParametersForFix.find( + (param) => param.name === 'default-root-object' + )?.value; if (!defaultRootObject) { - throw new Error("Required parameter 'default-root-object' is missing.") + throw new Error("Required parameter 'default-root-object' is missing."); } for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, - DefaultRootObject: defaultRootObject - } + DefaultRootObject: defaultRootObject, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts b/src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts index 796adc9..1ad54eb 100644 --- a/src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts +++ b/src/bpsets/cloudfront/CloudFrontNoDeprecatedSSLProtocols.ts @@ -2,66 +2,136 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontNoDeprecatedSSLProtocols implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontNoDeprecatedSSLProtocols', + description: 'Ensures that CloudFront distributions do not use deprecated SSL protocols like SSLv3.', + priority: 2, + priorityReason: 'Deprecated SSL protocols pose significant security risks and should be avoided.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions to check for deprecated SSL protocols.', + }, + { + name: 'GetDistributionCommand', + reason: 'Retrieve distribution details to identify deprecated SSL protocols in origin configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Remove deprecated SSL protocols from the origin configuration of the distribution.', + }, + ], + adviseBeforeFixFunction: 'Ensure the origins are configured to support only secure and modern SSL protocols.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { - const { distribution: details } = await this.getDistributionDetails(distribution.Id!) + const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const hasDeprecatedSSL = details.DistributionConfig?.Origins?.Items?.some( - origin => + (origin) => origin.CustomOriginConfig && origin.CustomOriginConfig.OriginSslProtocols?.Items?.includes('SSLv3') - ) + ); if (hasDeprecatedSSL) { - nonCompliantResources.push(details.ARN!) + nonCompliantResources.push(details.ARN!); } else { - compliantResources.push(details.ARN!) + compliantResources.push(details.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, Origins: { - Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => { + Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => { if (origin.CustomOriginConfig) { return { ...origin, @@ -70,24 +140,24 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet { OriginSslProtocols: { ...origin.CustomOriginConfig.OriginSslProtocols, Items: origin.CustomOriginConfig.OriginSslProtocols?.Items?.filter( - protocol => protocol !== 'SSLv3' - ) - } - } - } + (protocol) => protocol !== 'SSLv3' + ), + }, + }, + }; } - return origin - }) - } - } + return origin; + }), + }, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontS3OriginAccessControlEnabled.ts b/src/bpsets/cloudfront/CloudFrontS3OriginAccessControlEnabled.ts index e303959..c991355 100644 --- a/src/bpsets/cloudfront/CloudFrontS3OriginAccessControlEnabled.ts +++ b/src/bpsets/cloudfront/CloudFrontS3OriginAccessControlEnabled.ts @@ -2,95 +2,169 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontS3OriginAccessControlEnabled implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontS3OriginAccessControlEnabled', + description: 'Ensures that CloudFront distributions with S3 origins have Origin Access Control (OAC) enabled.', + priority: 3, + priorityReason: 'Using Origin Access Control enhances security by ensuring only CloudFront can access the S3 origin.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'origin-access-control-id', + description: 'The ID of the Origin Access Control to associate with the S3 origin.', + default: '', + example: 'oac-0abcd1234efgh5678', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions to check for S3 origins.', + }, + { + name: 'GetDistributionCommand', + reason: 'Retrieve distribution details to verify Origin Access Control configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Enable Origin Access Control for S3 origins in the distribution.', + }, + ], + adviseBeforeFixFunction: 'Ensure the specified Origin Access Control is correctly configured and applied to the S3 bucket.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { - const { distribution: details } = await this.getDistributionDetails(distribution.Id!) + const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const hasNonCompliantOrigin = details.DistributionConfig?.Origins?.Items?.some( - origin => + (origin) => origin.S3OriginConfig && (!origin.OriginAccessControlId || origin.OriginAccessControlId === '') - ) + ); if (hasNonCompliantOrigin) { - nonCompliantResources.push(details.ARN!) + nonCompliantResources.push(details.ARN!); } else { - compliantResources.push(details.ARN!) + compliantResources.push(details.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'origin-access-control-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const originAccessControlId = requiredParametersForFix.find( - param => param.name === 'origin-access-control-id' - )?.value + (param) => param.name === 'origin-access-control-id' + )?.value; if (!originAccessControlId) { - throw new Error("Required parameter 'origin-access-control-id' is missing.") + throw new Error("Required parameter 'origin-access-control-id' is missing."); } for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, Origins: { - Items: distribution.DistributionConfig?.Origins?.Items?.map(origin => { + Items: distribution.DistributionConfig?.Origins?.Items?.map((origin) => { if (origin.S3OriginConfig) { return { ...origin, - OriginAccessControlId: originAccessControlId - } + OriginAccessControlId: originAccessControlId, + }; } - return origin - }) - } - } + return origin; + }), + }, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts b/src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts index 9bbeb06..95d6a75 100644 --- a/src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts +++ b/src/bpsets/cloudfront/CloudFrontViewerPolicyHTTPS.ts @@ -2,83 +2,153 @@ import { CloudFrontClient, ListDistributionsCommand, GetDistributionCommand, - UpdateDistributionCommand -} from '@aws-sdk/client-cloudfront' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDistributionCommand, +} from '@aws-sdk/client-cloudfront'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudFrontViewerPolicyHTTPS implements BPSet { - private readonly client = new CloudFrontClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudFrontClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDistributions = async () => { - const response = await this.memoClient.send(new ListDistributionsCommand({})) - return response.DistributionList?.Items || [] - } + const response = await this.memoClient.send(new ListDistributionsCommand({})); + return response.DistributionList?.Items || []; + }; private readonly getDistributionDetails = async (distributionId: string) => { const response = await this.memoClient.send( new GetDistributionCommand({ Id: distributionId }) - ) + ); return { distribution: response.Distribution!, - etag: response.ETag! - } - } + etag: response.ETag!, + }; + }; + + public readonly getMetadata = () => ({ + name: 'CloudFrontViewerPolicyHTTPS', + description: 'Ensures that CloudFront distributions enforce HTTPS for viewer requests.', + priority: 1, + priorityReason: 'Enforcing HTTPS improves security by ensuring secure communication between viewers and CloudFront.', + awsService: 'CloudFront', + awsServiceCategory: 'CDN', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListDistributionsCommand', + reason: 'List all CloudFront distributions to check viewer protocol policies.', + }, + { + name: 'GetDistributionCommand', + reason: 'Retrieve distribution details to verify viewer protocol policy settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDistributionCommand', + reason: 'Update the viewer protocol policy to enforce HTTPS.', + }, + ], + adviseBeforeFixFunction: 'Ensure all origins and endpoints support HTTPS to prevent connectivity issues.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const distributions = await this.getDistributions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const distributions = await this.getDistributions(); for (const distribution of distributions) { - const { distribution: details } = await this.getDistributionDetails(distribution.Id!) + const { distribution: details } = await this.getDistributionDetails(distribution.Id!); const hasNonCompliantViewerPolicy = details.DistributionConfig?.DefaultCacheBehavior?.ViewerProtocolPolicy === 'allow-all' || details.DistributionConfig?.CacheBehaviors?.Items?.some( - behavior => behavior.ViewerProtocolPolicy === 'allow-all' - ) + (behavior) => behavior.ViewerProtocolPolicy === 'allow-all' + ); if (hasNonCompliantViewerPolicy) { - nonCompliantResources.push(details.ARN!) + nonCompliantResources.push(details.ARN!); } else { - compliantResources.push(details.ARN!) + compliantResources.push(details.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const distributionId = arn.split('/').pop()! - const { distribution, etag } = await this.getDistributionDetails(distributionId) + const distributionId = arn.split('/').pop()!; + const { distribution, etag } = await this.getDistributionDetails(distributionId); const updatedConfig = { ...distribution.DistributionConfig, DefaultCacheBehavior: { ...distribution.DistributionConfig?.DefaultCacheBehavior, - ViewerProtocolPolicy: 'redirect-to-https' + ViewerProtocolPolicy: 'redirect-to-https', }, CacheBehaviors: { - Items: distribution.DistributionConfig?.CacheBehaviors?.Items?.map(behavior => ({ + Items: distribution.DistributionConfig?.CacheBehaviors?.Items?.map((behavior) => ({ ...behavior, - ViewerProtocolPolicy: 'redirect-to-https' - })) - } - } + ViewerProtocolPolicy: 'redirect-to-https', + })), + }, + }; await this.client.send( new UpdateDistributionCommand({ Id: distributionId, IfMatch: etag, - DistributionConfig: updatedConfig as any + DistributionConfig: updatedConfig as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudwatch/CWLogGroupRetentionPeriodCheck.ts b/src/bpsets/cloudwatch/CWLogGroupRetentionPeriodCheck.ts index e3eaead..ec9215f 100644 --- a/src/bpsets/cloudwatch/CWLogGroupRetentionPeriodCheck.ts +++ b/src/bpsets/cloudwatch/CWLogGroupRetentionPeriodCheck.ts @@ -1,60 +1,130 @@ import { CloudWatchLogsClient, DescribeLogGroupsCommand, - PutRetentionPolicyCommand -} from '@aws-sdk/client-cloudwatch-logs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutRetentionPolicyCommand, +} from '@aws-sdk/client-cloudwatch-logs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CWLogGroupRetentionPeriodCheck implements BPSet { - private readonly client = new CloudWatchLogsClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudWatchLogsClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getLogGroups = async () => { - const response = await this.memoClient.send(new DescribeLogGroupsCommand({})) - return response.logGroups || [] - } + const response = await this.memoClient.send(new DescribeLogGroupsCommand({})); + return response.logGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'CWLogGroupRetentionPeriodCheck', + description: 'Ensures all CloudWatch log groups have a retention period set.', + priority: 3, + priorityReason: 'Setting a retention period for log groups helps manage storage costs and compliance.', + awsService: 'CloudWatch Logs', + awsServiceCategory: 'Monitoring', + bestPracticeCategory: 'Configuration', + requiredParametersForFix: [ + { + name: 'retention-period-days', + description: 'Retention period in days to apply to log groups.', + default: '', + example: '30', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeLogGroupsCommand', + reason: 'Retrieve all CloudWatch log groups to verify retention settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutRetentionPolicyCommand', + reason: 'Set the retention period for log groups without a defined retention policy.', + }, + ], + adviseBeforeFixFunction: 'Ensure the specified retention period meets your organizational compliance requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const logGroups = await this.getLogGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const logGroups = await this.getLogGroups(); for (const logGroup of logGroups) { if (logGroup.retentionInDays) { - compliantResources.push(logGroup.logGroupArn!) + compliantResources.push(logGroup.logGroupArn!); } else { - nonCompliantResources.push(logGroup.logGroupArn!) + nonCompliantResources.push(logGroup.logGroupArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'retention-period-days' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const retentionPeriod = requiredParametersForFix.find( - param => param.name === 'retention-period-days' - )?.value + (param) => param.name === 'retention-period-days' + )?.value; if (!retentionPeriod) { - throw new Error("Required parameter 'retention-period-days' is missing.") + throw new Error("Required parameter 'retention-period-days' is missing."); } for (const logGroupArn of nonCompliantResources) { - const logGroupName = logGroupArn.split(':').pop()! + const logGroupName = logGroupArn.split(':').pop()!; await this.client.send( new PutRetentionPolicyCommand({ logGroupName, - retentionInDays: parseInt(retentionPeriod, 10) + retentionInDays: parseInt(retentionPeriod, 10), }) - ) + ); } - } + }; } diff --git a/src/bpsets/cloudwatch/CloudWatchAlarmSettingsCheck.ts b/src/bpsets/cloudwatch/CloudWatchAlarmSettingsCheck.ts index 14be718..3e655b3 100644 --- a/src/bpsets/cloudwatch/CloudWatchAlarmSettingsCheck.ts +++ b/src/bpsets/cloudwatch/CloudWatchAlarmSettingsCheck.ts @@ -1,73 +1,137 @@ import { CloudWatchClient, DescribeAlarmsCommand, - PutMetricAlarmCommand -} from '@aws-sdk/client-cloudwatch' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutMetricAlarmCommand, +} from '@aws-sdk/client-cloudwatch'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CloudWatchAlarmSettingsCheck implements BPSet { - private readonly client = new CloudWatchClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CloudWatchClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAlarms = async () => { - const response = await this.memoClient.send(new DescribeAlarmsCommand({})) - return response.MetricAlarms || [] - } + const response = await this.memoClient.send(new DescribeAlarmsCommand({})); + return response.MetricAlarms || []; + }; + + public readonly getMetadata = () => ({ + name: 'CloudWatchAlarmSettingsCheck', + description: 'Ensures that CloudWatch alarms have the required settings configured.', + priority: 3, + priorityReason: 'Correct alarm settings are essential for effective monitoring and alerting.', + awsService: 'CloudWatch', + awsServiceCategory: 'Monitoring', + bestPracticeCategory: 'Configuration', + requiredParametersForFix: [ + { name: 'metric-name', description: 'The metric name for the alarm.', default: '', example: 'CPUUtilization' }, + { name: 'threshold', description: 'The threshold for the alarm.', default: '', example: '80' }, + { name: 'evaluation-periods', description: 'Number of evaluation periods for the alarm.', default: '', example: '5' }, + { name: 'period', description: 'The period in seconds for the metric evaluation.', default: '', example: '60' }, + { name: 'comparison-operator', description: 'Comparison operator for the threshold.', default: '', example: 'GreaterThanThreshold' }, + { name: 'statistic', description: 'Statistic to apply to the metric.', default: '', example: 'Average' }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeAlarmsCommand', + reason: 'Retrieve all CloudWatch alarms to verify their settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutMetricAlarmCommand', + reason: 'Update or create alarms with the required settings.', + }, + ], + adviseBeforeFixFunction: 'Ensure the required settings are correctly configured for your monitoring needs.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const alarms = await this.getAlarms(); const parameters = { MetricName: '', // Required Threshold: null, EvaluationPeriods: null, Period: null, ComparisonOperator: null, - Statistic: null - } - - const alarms = await this.getAlarms() + Statistic: null, + }; for (const alarm of alarms) { - for (const parameter of Object.keys(parameters).filter(key => (parameters as any)[key] !== null)) { - if (alarm.MetricName !== parameters.MetricName) { - continue - } + let isCompliant = true; - if (alarm[parameter as keyof typeof alarm] !== parameters[parameter as keyof typeof parameters]) { - nonCompliantResources.push(alarm.AlarmArn!) - break + for (const key of Object.keys(parameters).filter((k) => (parameters as any)[k] !== null)) { + if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) { + isCompliant = false; + break; } } - compliantResources.push(alarm.AlarmArn!) + if (isCompliant) { + compliantResources.push(alarm.AlarmArn!); + } else { + nonCompliantResources.push(alarm.AlarmArn!); + } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'metric-name' }, - { name: 'threshold' }, - { name: 'evaluation-periods' }, - { name: 'period' }, - { name: 'comparison-operator' }, - { name: 'statistic' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const requiredSettings = Object.fromEntries( - requiredParametersForFix.map(param => [param.name, param.value]) - ) + requiredParametersForFix.map((param) => [param.name, param.value]) + ); for (const alarmArn of nonCompliantResources) { - const alarmName = alarmArn.split(':').pop()! + const alarmName = alarmArn.split(':').pop()!; await this.client.send( new PutMetricAlarmCommand({ @@ -77,9 +141,9 @@ export class CloudWatchAlarmSettingsCheck implements BPSet { EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10), Period: parseInt(requiredSettings['period'], 10), ComparisonOperator: requiredSettings['comparison-operator'] as any, - Statistic: requiredSettings['statistic'] as any + Statistic: requiredSettings['statistic'] as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/codeseries/CodeBuildProjectEnvironmentPrivilegedCheck.ts b/src/bpsets/codeseries/CodeBuildProjectEnvironmentPrivilegedCheck.ts index 0ccb126..618d9fa 100644 --- a/src/bpsets/codeseries/CodeBuildProjectEnvironmentPrivilegedCheck.ts +++ b/src/bpsets/codeseries/CodeBuildProjectEnvironmentPrivilegedCheck.ts @@ -2,65 +2,136 @@ import { CodeBuildClient, ListProjectsCommand, BatchGetProjectsCommand, - UpdateProjectCommand -} from '@aws-sdk/client-codebuild' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateProjectCommand, +} from '@aws-sdk/client-codebuild'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet { - private readonly client = new CodeBuildClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CodeBuildClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getProjects = async () => { - const projectNames = await this.memoClient.send(new ListProjectsCommand({})) + const projectNames = await this.memoClient.send(new ListProjectsCommand({})); if (!projectNames.projects?.length) { - return [] + return []; } const response = await this.memoClient.send( new BatchGetProjectsCommand({ names: projectNames.projects }) - ) - return response.projects || [] - } + ); + return response.projects || []; + }; + + public readonly getMetadata = () => ({ + name: 'CodeBuildProjectEnvironmentPrivilegedCheck', + description: 'Ensures that AWS CodeBuild projects are not using privileged mode for their environment.', + priority: 3, + priorityReason: 'Privileged mode in CodeBuild environments increases the risk of unauthorized access and actions.', + awsService: 'CodeBuild', + awsServiceCategory: 'Build', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListProjectsCommand', + reason: 'Retrieve all CodeBuild projects to verify environment settings.', + }, + { + name: 'BatchGetProjectsCommand', + reason: 'Fetch detailed configuration for each CodeBuild project.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateProjectCommand', + reason: 'Update the project to disable privileged mode in the environment settings.', + }, + ], + adviseBeforeFixFunction: 'Ensure the privileged mode is not required for any valid use case before applying fixes.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const projects = await this.getProjects() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const projects = await this.getProjects(); for (const project of projects) { if (!project.environment?.privilegedMode) { - compliantResources.push(project.arn!) + compliantResources.push(project.arn!); } else { - nonCompliantResources.push(project.arn!) + nonCompliantResources.push(project.arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { + const projects = await this.getProjects(); - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const projectName = arn.split(':').pop()! - const projects = await this.getProjects() - const projectToFix = projects.find(project => project.arn === arn) + const projectName = arn.split(':').pop()!; + const projectToFix = projects.find((project) => project.arn === arn); if (!projectToFix) { - continue + continue; } await this.client.send( new UpdateProjectCommand({ name: projectName, environment: { - ...projectToFix.environment as any, - privilegedMode: false - } + ...projectToFix.environment, + privilegedMode: false, + } as any, }) - ) + ); } - } + }; } diff --git a/src/bpsets/codeseries/CodeBuildProjectLoggingEnabled.ts b/src/bpsets/codeseries/CodeBuildProjectLoggingEnabled.ts index de2b170..aaad326 100644 --- a/src/bpsets/codeseries/CodeBuildProjectLoggingEnabled.ts +++ b/src/bpsets/codeseries/CodeBuildProjectLoggingEnabled.ts @@ -2,58 +2,129 @@ import { CodeBuildClient, ListProjectsCommand, BatchGetProjectsCommand, - UpdateProjectCommand -} from '@aws-sdk/client-codebuild' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateProjectCommand, +} from '@aws-sdk/client-codebuild'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CodeBuildProjectLoggingEnabled implements BPSet { - private readonly client = new CodeBuildClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CodeBuildClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getProjects = async () => { - const projectNames = await this.memoClient.send(new ListProjectsCommand({})) + const projectNames = await this.memoClient.send(new ListProjectsCommand({})); if (!projectNames.projects?.length) { - return [] + return []; } const response = await this.memoClient.send( new BatchGetProjectsCommand({ names: projectNames.projects }) - ) - return response.projects || [] - } + ); + return response.projects || []; + }; + + public readonly getMetadata = () => ({ + name: 'CodeBuildProjectLoggingEnabled', + description: 'Ensures that logging is enabled for AWS CodeBuild projects.', + priority: 3, + priorityReason: 'Enabling logging allows for monitoring and debugging build processes effectively.', + awsService: 'CodeBuild', + awsServiceCategory: 'Build', + bestPracticeCategory: 'Logging and Monitoring', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListProjectsCommand', + reason: 'Retrieve all CodeBuild projects to verify logging settings.', + }, + { + name: 'BatchGetProjectsCommand', + reason: 'Fetch detailed configuration for each CodeBuild project.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateProjectCommand', + reason: 'Enable logging for projects that have it disabled.', + }, + ], + adviseBeforeFixFunction: 'Ensure the default log group and stream names are suitable for your organization.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const projects = await this.getProjects() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const projects = await this.getProjects(); for (const project of projects) { - const logsConfig = project.logsConfig + const logsConfig = project.logsConfig; if ( logsConfig?.cloudWatchLogs?.status === 'ENABLED' || logsConfig?.s3Logs?.status === 'ENABLED' ) { - compliantResources.push(project.arn!) + compliantResources.push(project.arn!); } else { - nonCompliantResources.push(project.arn!) + nonCompliantResources.push(project.arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { + const projects = await this.getProjects(); - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const projectName = arn.split(':').pop()! - const projects = await this.getProjects() - const projectToFix = projects.find(project => project.arn === arn) + const projectName = arn.split(':').pop()!; + const projectToFix = projects.find((project) => project.arn === arn); if (!projectToFix) { - continue + continue; } await this.client.send( @@ -64,11 +135,11 @@ export class CodeBuildProjectLoggingEnabled implements BPSet { cloudWatchLogs: { status: 'ENABLED', groupName: 'default-cloudwatch-group', - streamName: 'default-stream' - } - } + streamName: 'default-stream', + }, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/codeseries/CodeDeployAutoRollbackMonitorEnabled.ts b/src/bpsets/codeseries/CodeDeployAutoRollbackMonitorEnabled.ts index f821e95..5199f00 100644 --- a/src/bpsets/codeseries/CodeDeployAutoRollbackMonitorEnabled.ts +++ b/src/bpsets/codeseries/CodeDeployAutoRollbackMonitorEnabled.ts @@ -3,70 +3,146 @@ import { ListApplicationsCommand, ListDeploymentGroupsCommand, BatchGetDeploymentGroupsCommand, - UpdateDeploymentGroupCommand -} from '@aws-sdk/client-codedeploy' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateDeploymentGroupCommand, +} from '@aws-sdk/client-codedeploy'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class CodeDeployAutoRollbackMonitorEnabled implements BPSet { - private readonly client = new CodeDeployClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new CodeDeployClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getDeploymentGroups = async () => { - const applications = await this.memoClient.send(new ListApplicationsCommand({})) - const deploymentGroupsInfo = [] + const applications = await this.memoClient.send(new ListApplicationsCommand({})); + const deploymentGroupsInfo = []; for (const application of applications.applications || []) { const deploymentGroups = await this.memoClient.send( new ListDeploymentGroupsCommand({ applicationName: application }) - ) + ); if (!deploymentGroups.deploymentGroups?.length) { - continue + continue; } const batchResponse = await this.memoClient.send( new BatchGetDeploymentGroupsCommand({ applicationName: application, - deploymentGroupNames: deploymentGroups.deploymentGroups + deploymentGroupNames: deploymentGroups.deploymentGroups, }) - ) - deploymentGroupsInfo.push(...(batchResponse.deploymentGroupsInfo || [])) + ); + deploymentGroupsInfo.push(...(batchResponse.deploymentGroupsInfo || [])); } - return deploymentGroupsInfo - } + return deploymentGroupsInfo; + }; + + public readonly getMetadata = () => ({ + name: 'CodeDeployAutoRollbackMonitorEnabled', + description: 'Ensures that auto-rollback and alarm monitoring are enabled for CodeDeploy deployment groups.', + priority: 2, + priorityReason: 'Enabling auto-rollback and alarms helps prevent deployment issues and allows for automated recovery.', + awsService: 'CodeDeploy', + awsServiceCategory: 'Deployment', + bestPracticeCategory: 'Resilience', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListApplicationsCommand', + reason: 'Retrieve all CodeDeploy applications to analyze their deployment groups.', + }, + { + name: 'ListDeploymentGroupsCommand', + reason: 'Fetch deployment groups for each application.', + }, + { + name: 'BatchGetDeploymentGroupsCommand', + reason: 'Get detailed information about deployment groups to check configurations.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateDeploymentGroupCommand', + reason: 'Enable alarm monitoring and auto-rollback configurations for deployment groups.', + }, + ], + adviseBeforeFixFunction: + 'Ensure your alarms and configurations align with organizational standards before enabling auto-rollback and monitoring.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const deploymentGroups = await this.getDeploymentGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const deploymentGroups = await this.getDeploymentGroups(); for (const deploymentGroup of deploymentGroups) { if ( deploymentGroup.alarmConfiguration?.enabled && deploymentGroup.autoRollbackConfiguration?.enabled ) { - compliantResources.push(deploymentGroup.deploymentGroupId!) + compliantResources.push(deploymentGroup.deploymentGroupId!); } else { - nonCompliantResources.push(deploymentGroup.deploymentGroupId!) + nonCompliantResources.push(deploymentGroup.deploymentGroupId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { + const deploymentGroups = await this.getDeploymentGroups(); - public readonly fix = async (nonCompliantResources: string[]) => { for (const groupId of nonCompliantResources) { - const deploymentGroups = await this.getDeploymentGroups() const deploymentGroupToFix = deploymentGroups.find( - group => group.deploymentGroupId === groupId - ) + (group) => group.deploymentGroupId === groupId + ); if (!deploymentGroupToFix) { - continue + continue; } await this.client.send( @@ -75,14 +151,14 @@ export class CodeDeployAutoRollbackMonitorEnabled implements BPSet { currentDeploymentGroupName: deploymentGroupToFix.deploymentGroupName!, alarmConfiguration: { ...deploymentGroupToFix.alarmConfiguration, - enabled: true + enabled: true, }, autoRollbackConfiguration: { ...deploymentGroupToFix.autoRollbackConfiguration, - enabled: true - } + enabled: true, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBAutoscalingEnabled.ts b/src/bpsets/dynamodb/DynamoDBAutoscalingEnabled.ts index c56bcd7..80dfa40 100644 --- a/src/bpsets/dynamodb/DynamoDBAutoscalingEnabled.ts +++ b/src/bpsets/dynamodb/DynamoDBAutoscalingEnabled.ts @@ -1,75 +1,154 @@ import { DynamoDBClient, ListTablesCommand, - DescribeTableCommand -} from '@aws-sdk/client-dynamodb' + DescribeTableCommand, +} from '@aws-sdk/client-dynamodb'; import { ApplicationAutoScalingClient, RegisterScalableTargetCommand, PutScalingPolicyCommand, - DescribeScalingPoliciesCommand -} from '@aws-sdk/client-application-auto-scaling' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeScalingPoliciesCommand, +} from '@aws-sdk/client-application-auto-scaling'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBAutoscalingEnabled implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly autoScalingClient = new ApplicationAutoScalingClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly autoScalingClient = new ApplicationAutoScalingClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBAutoscalingEnabled', + description: 'Ensures DynamoDB tables have autoscaling enabled for both read and write capacity.', + priority: 2, + priorityReason: 'Autoscaling ensures DynamoDB tables dynamically adjust capacity to meet demand, reducing costs and preventing throttling.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Scalability', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'List all DynamoDB tables to check their configurations.', + }, + { + name: 'DescribeTableCommand', + reason: 'Retrieve details of DynamoDB tables to analyze billing mode and autoscaling.', + }, + { + name: 'DescribeScalingPoliciesCommand', + reason: 'Fetch scaling policies for each table to check autoscaling settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterScalableTargetCommand', + reason: 'Register read and write capacity units for autoscaling.', + }, + { + name: 'PutScalingPolicyCommand', + reason: 'Configure target tracking scaling policies for read and write capacity.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the table’s read and write workloads are predictable to configure appropriate scaling limits.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { if (table.BillingModeSummary?.BillingMode === 'PAY_PER_REQUEST') { - compliantResources.push(table.TableArn!) - continue + compliantResources.push(table.TableArn!); + continue; } const scalingPolicies = await this.autoScalingClient.send( new DescribeScalingPoliciesCommand({ ServiceNamespace: 'dynamodb', - ResourceId: `table/${table.TableName}` + ResourceId: `table/${table.TableName}`, }) - ) + ); const scalingPolicyDimensions = scalingPolicies.ScalingPolicies?.map( - policy => policy.ScalableDimension - ) + (policy) => policy.ScalableDimension + ); if ( scalingPolicyDimensions?.includes('dynamodb:table:ReadCapacityUnits') && scalingPolicyDimensions?.includes('dynamodb:table:WriteCapacityUnits') ) { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } else { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const tableName = arn.split('/').pop()! + const tableName = arn.split('/').pop()!; // Register scalable targets for read and write capacity await this.autoScalingClient.send( @@ -78,9 +157,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet { ResourceId: `table/${tableName}`, ScalableDimension: 'dynamodb:table:ReadCapacityUnits', MinCapacity: 1, - MaxCapacity: 100 + MaxCapacity: 100, }) - ) + ); await this.autoScalingClient.send( new RegisterScalableTargetCommand({ @@ -88,9 +167,9 @@ export class DynamoDBAutoscalingEnabled implements BPSet { ResourceId: `table/${tableName}`, ScalableDimension: 'dynamodb:table:WriteCapacityUnits', MinCapacity: 1, - MaxCapacity: 100 + MaxCapacity: 100, }) - ) + ); // Put scaling policies for read and write capacity await this.autoScalingClient.send( @@ -105,11 +184,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet { ScaleInCooldown: 60, ScaleOutCooldown: 60, PredefinedMetricSpecification: { - PredefinedMetricType: 'DynamoDBReadCapacityUtilization' - } - } + PredefinedMetricType: 'DynamoDBReadCapacityUtilization', + }, + }, }) - ) + ); await this.autoScalingClient.send( new PutScalingPolicyCommand({ @@ -123,11 +202,11 @@ export class DynamoDBAutoscalingEnabled implements BPSet { ScaleInCooldown: 60, ScaleOutCooldown: 60, PredefinedMetricSpecification: { - PredefinedMetricType: 'DynamoDBWriteCapacityUtilization' - } - } + PredefinedMetricType: 'DynamoDBWriteCapacityUtilization', + }, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBLastBackupRecoveryPointCreated.ts b/src/bpsets/dynamodb/DynamoDBLastBackupRecoveryPointCreated.ts index 945a976..4d6b719 100644 --- a/src/bpsets/dynamodb/DynamoDBLastBackupRecoveryPointCreated.ts +++ b/src/bpsets/dynamodb/DynamoDBLastBackupRecoveryPointCreated.ts @@ -1,78 +1,167 @@ import { DynamoDBClient, ListTablesCommand, - DescribeTableCommand -} from '@aws-sdk/client-dynamodb' + DescribeTableCommand, +} from '@aws-sdk/client-dynamodb'; import { BackupClient, ListRecoveryPointsByResourceCommand, - StartBackupJobCommand -} from '@aws-sdk/client-backup' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + StartBackupJobCommand, +} from '@aws-sdk/client-backup'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBLastBackupRecoveryPointCreated implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly backupClient = new BackupClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly backupClient = new BackupClient({}); + private readonly stsClient = new STSClient({}); + private readonly memoClient = Memorizer.memo(this.client); + + private accountId: string | undefined; + + private readonly fetchAccountId = async () => { + if (!this.accountId) { + const identity = await this.stsClient.send(new GetCallerIdentityCommand({})); + this.accountId = identity.Account!; + } + return this.accountId; + }; private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBLastBackupRecoveryPointCreated', + description: 'Ensures that DynamoDB tables have a recent recovery point within the last 24 hours.', + priority: 3, + priorityReason: 'Recent backups are critical for data recovery and minimizing data loss.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Backup and Recovery', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'Retrieve the list of DynamoDB tables to check for backups.', + }, + { + name: 'DescribeTableCommand', + reason: 'Fetch details of each DynamoDB table.', + }, + { + name: 'ListRecoveryPointsByResourceCommand', + reason: 'Check recovery points for DynamoDB tables in AWS Backup.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'StartBackupJobCommand', + reason: 'Initiate a backup job for non-compliant DynamoDB tables.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the backup vault and IAM role are properly configured for your backup strategy.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { const recoveryPointsResponse = await this.backupClient.send( new ListRecoveryPointsByResourceCommand({ - ResourceArn: table.TableArn + ResourceArn: table.TableArn, }) - ) - const recoveryPoints = recoveryPointsResponse.RecoveryPoints || [] + ); + const recoveryPoints = recoveryPointsResponse.RecoveryPoints || []; if (recoveryPoints.length === 0) { - nonCompliantResources.push(table.TableArn!) - continue + nonCompliantResources.push(table.TableArn!); + continue; } const latestRecoveryPoint = recoveryPoints - .map(point => new Date(point.CreationDate!)) - .sort((a, b) => b.getTime() - a.getTime())[0] + .map((point) => new Date(point.CreationDate!)) + .sort((a, b) => b.getTime() - a.getTime())[0]; if (new Date().getTime() - latestRecoveryPoint.getTime() > 86400000) { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } else { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { + const accountId = await this.fetchAccountId(); - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { await this.backupClient.send( new StartBackupJobCommand({ ResourceArn: arn, BackupVaultName: 'Default', - IamRoleArn: 'arn:aws:iam::account-id:role/service-role/BackupDefaultServiceRole', + IamRoleArn: `arn:aws:iam::${accountId}:role/service-role/BackupDefaultServiceRole`, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBPITREnabled.ts b/src/bpsets/dynamodb/DynamoDBPITREnabled.ts index 9bce6fb..9679caf 100644 --- a/src/bpsets/dynamodb/DynamoDBPITREnabled.ts +++ b/src/bpsets/dynamodb/DynamoDBPITREnabled.ts @@ -3,68 +3,142 @@ import { ListTablesCommand, DescribeTableCommand, DescribeContinuousBackupsCommand, - UpdateContinuousBackupsCommand -} from '@aws-sdk/client-dynamodb' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateContinuousBackupsCommand, +} from '@aws-sdk/client-dynamodb'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBPITREnabled implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBPITREnabled', + description: 'Ensures that Point-In-Time Recovery (PITR) is enabled for DynamoDB tables.', + priority: 1, + priorityReason: 'PITR provides continuous backups of DynamoDB tables, enabling recovery to any second within the last 35 days.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Backup and Recovery', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'Retrieve the list of DynamoDB tables to verify PITR settings.', + }, + { + name: 'DescribeTableCommand', + reason: 'Fetch details of each DynamoDB table.', + }, + { + name: 'DescribeContinuousBackupsCommand', + reason: 'Check if PITR is enabled for each table.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateContinuousBackupsCommand', + reason: 'Enable PITR for non-compliant DynamoDB tables.', + }, + ], + adviseBeforeFixFunction: 'Ensure enabling PITR aligns with organizational backup policies and compliance requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { const backupStatus = await this.memoClient.send( new DescribeContinuousBackupsCommand({ - TableName: table.TableName! + TableName: table.TableName!, }) - ) + ); if ( backupStatus.ContinuousBackupsDescription?.PointInTimeRecoveryDescription ?.PointInTimeRecoveryStatus === 'ENABLED' ) { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } else { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const tableName = arn.split('/').pop()! + const tableName = arn.split('/').pop()!; await this.client.send( new UpdateContinuousBackupsCommand({ TableName: tableName, PointInTimeRecoverySpecification: { - PointInTimeRecoveryEnabled: true - } + PointInTimeRecoveryEnabled: true, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBTableDeletionProtectionEnabled.ts b/src/bpsets/dynamodb/DynamoDBTableDeletionProtectionEnabled.ts index c35a9c5..bc54a2a 100644 --- a/src/bpsets/dynamodb/DynamoDBTableDeletionProtectionEnabled.ts +++ b/src/bpsets/dynamodb/DynamoDBTableDeletionProtectionEnabled.ts @@ -2,57 +2,127 @@ import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, - UpdateTableCommand -} from '@aws-sdk/client-dynamodb' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateTableCommand, +} from '@aws-sdk/client-dynamodb'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBTableDeletionProtectionEnabled implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBTableDeletionProtectionEnabled', + description: 'Ensures that deletion protection is enabled for DynamoDB tables.', + priority: 2, + priorityReason: 'Deletion protection prevents accidental table deletion, safeguarding critical data.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'Retrieve the list of DynamoDB tables to verify deletion protection.', + }, + { + name: 'DescribeTableCommand', + reason: 'Fetch details of each DynamoDB table, including deletion protection settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateTableCommand', + reason: 'Enable deletion protection for non-compliant DynamoDB tables.', + }, + ], + adviseBeforeFixFunction: 'Ensure enabling deletion protection aligns with operational and compliance requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { if (table.DeletionProtectionEnabled) { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } else { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const tableName = arn.split('/').pop()! + const tableName = arn.split('/').pop()!; await this.client.send( new UpdateTableCommand({ TableName: tableName, - DeletionProtectionEnabled: true + DeletionProtectionEnabled: true, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBTableEncryptedKMS.ts b/src/bpsets/dynamodb/DynamoDBTableEncryptedKMS.ts index 97767f6..0e19579 100644 --- a/src/bpsets/dynamodb/DynamoDBTableEncryptedKMS.ts +++ b/src/bpsets/dynamodb/DynamoDBTableEncryptedKMS.ts @@ -2,62 +2,136 @@ import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, - UpdateTableCommand -} from '@aws-sdk/client-dynamodb' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateTableCommand, +} from '@aws-sdk/client-dynamodb'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBTableEncryptedKMS implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBTableEncryptedKMS', + description: 'Ensures that DynamoDB tables are encrypted with AWS KMS.', + priority: 2, + priorityReason: 'Encrypting DynamoDB tables with KMS enhances data security and meets compliance requirements.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'The ID of the KMS key used to encrypt the DynamoDB table.', + default: '', + example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.', + }, + { + name: 'DescribeTableCommand', + reason: 'Fetch details of each DynamoDB table, including SSE configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateTableCommand', + reason: 'Enable KMS encryption for non-compliant DynamoDB tables.', + }, + ], + adviseBeforeFixFunction: 'Ensure the specified KMS key is accessible and meets your security policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { if ( table.SSEDescription?.Status === 'ENABLED' && table.SSEDescription?.SSEType === 'KMS' ) { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } else { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'kms-key-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const arn of nonCompliantResources) { - const tableName = arn.split('/').pop()! + const tableName = arn.split('/').pop()!; await this.client.send( new UpdateTableCommand({ @@ -65,10 +139,10 @@ export class DynamoDBTableEncryptedKMS implements BPSet { SSESpecification: { Enabled: true, SSEType: 'KMS', - KMSMasterKeyId: kmsKeyId - } + KMSMasterKeyId: kmsKeyId, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/dynamodb/DynamoDBTableEncryptionEnabled.ts b/src/bpsets/dynamodb/DynamoDBTableEncryptionEnabled.ts index f8f317d..196465a 100644 --- a/src/bpsets/dynamodb/DynamoDBTableEncryptionEnabled.ts +++ b/src/bpsets/dynamodb/DynamoDBTableEncryptionEnabled.ts @@ -2,59 +2,129 @@ import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, - UpdateTableCommand -} from '@aws-sdk/client-dynamodb' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateTableCommand, +} from '@aws-sdk/client-dynamodb'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DynamoDBTableEncryptionEnabled implements BPSet { - private readonly client = new DynamoDBClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new DynamoDBClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTables = async () => { - const tableNames = await this.memoClient.send(new ListTablesCommand({})) - const tables = [] + const tableNames = await this.memoClient.send(new ListTablesCommand({})); + const tables = []; for (const tableName of tableNames.TableNames || []) { const tableDetails = await this.memoClient.send( new DescribeTableCommand({ TableName: tableName }) - ) - tables.push(tableDetails.Table!) + ); + tables.push(tableDetails.Table!); } - return tables - } + return tables; + }; + + public readonly getMetadata = () => ({ + name: 'DynamoDBTableEncryptionEnabled', + description: 'Ensures that DynamoDB tables have server-side encryption enabled.', + priority: 3, + priorityReason: 'Enabling server-side encryption ensures data security and compliance with organizational policies.', + awsService: 'DynamoDB', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTablesCommand', + reason: 'Retrieve the list of DynamoDB tables to verify encryption settings.', + }, + { + name: 'DescribeTableCommand', + reason: 'Fetch details of each DynamoDB table, including encryption settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateTableCommand', + reason: 'Enable server-side encryption for non-compliant DynamoDB tables.', + }, + ], + adviseBeforeFixFunction: 'Ensure enabling encryption aligns with organizational data security policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const tables = await this.getTables() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const tables = await this.getTables(); for (const table of tables) { if (table.SSEDescription?.Status === 'ENABLED') { - compliantResources.push(table.TableArn!) + compliantResources.push(table.TableArn!); } else { - nonCompliantResources.push(table.TableArn!) + nonCompliantResources.push(table.TableArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const tableName = arn.split('/').pop()! + const tableName = arn.split('/').pop()!; await this.client.send( new UpdateTableCommand({ TableName: tableName, SSESpecification: { - Enabled: true - } + Enabled: true, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ec2/EC2EbsEncryptionByDefault.ts b/src/bpsets/ec2/EC2EbsEncryptionByDefault.ts index 09854e6..d1a6c45 100644 --- a/src/bpsets/ec2/EC2EbsEncryptionByDefault.ts +++ b/src/bpsets/ec2/EC2EbsEncryptionByDefault.ts @@ -1,36 +1,102 @@ import { EC2Client, DescribeVolumesCommand, - EnableEbsEncryptionByDefaultCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + GetEbsEncryptionByDefaultCommand, + EnableEbsEncryptionByDefaultCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2EbsEncryptionByDefault implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2EbsEncryptionByDefault', + description: 'Ensures that EBS encryption is enabled by default for all volumes in the AWS account.', + priority: 1, + priorityReason: 'Enabling EBS encryption by default ensures data at rest is encrypted, enhancing security and compliance.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetEbsEncryptionByDefaultCommand', + reason: 'Verify if EBS encryption by default is enabled in the AWS account.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'EnableEbsEncryptionByDefaultCommand', + reason: 'Enable EBS encryption by default for the account.', + }, + ], + adviseBeforeFixFunction: + 'Ensure enabling EBS encryption by default aligns with your organization’s security policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeVolumesCommand({})) + this.stats.status = 'CHECKING'; - for (const volume of response.Volumes || []) { - if (volume.Encrypted) { - compliantResources.push(volume.VolumeId!) - } else { - nonCompliantResources.push(volume.VolumeId!) + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const response = await this.client.send(new GetEbsEncryptionByDefaultCommand({})); + if (response.EbsEncryptionByDefault) { + compliantResources.push('EBS Encryption By Default'); + } else { + nonCompliantResources.push('EBS Encryption By Default'); } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async () => { - await this.client.send(new EnableEbsEncryptionByDefaultCommand({})) - } + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async () => { + await this.client.send(new EnableEbsEncryptionByDefaultCommand({})); + }; } diff --git a/src/bpsets/ec2/EC2Imdsv2Check.ts b/src/bpsets/ec2/EC2Imdsv2Check.ts index c291325..962288c 100644 --- a/src/bpsets/ec2/EC2Imdsv2Check.ts +++ b/src/bpsets/ec2/EC2Imdsv2Check.ts @@ -1,45 +1,111 @@ import { DescribeInstancesCommand, EC2Client, - ModifyInstanceMetadataOptionsCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyInstanceMetadataOptionsCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2Imdsv2Check implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2Imdsv2Check', + description: 'Ensures that EC2 instances enforce the use of IMDSv2 for enhanced metadata security.', + priority: 1, + priorityReason: 'Requiring IMDSv2 improves the security of instance metadata by mitigating SSRF attacks.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve all EC2 instances and check their metadata options.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyInstanceMetadataOptionsCommand', + reason: 'Update EC2 instance metadata options to enforce IMDSv2.', + }, + ], + adviseBeforeFixFunction: 'Ensure modifying metadata options aligns with operational policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (instance.MetadataOptions?.HttpTokens === 'required') { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } else { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const instanceId of nonCompliantResources) { await this.client.send( new ModifyInstanceMetadataOptionsCommand({ InstanceId: instanceId, - HttpTokens: 'required' + HttpTokens: 'required', }) - ) + ); } - } + }; } diff --git a/src/bpsets/ec2/EC2InstanceDetailedMonitoringEnabled.ts b/src/bpsets/ec2/EC2InstanceDetailedMonitoringEnabled.ts index 9387767..c81bb45 100644 --- a/src/bpsets/ec2/EC2InstanceDetailedMonitoringEnabled.ts +++ b/src/bpsets/ec2/EC2InstanceDetailedMonitoringEnabled.ts @@ -1,42 +1,111 @@ import { DescribeInstancesCommand, EC2Client, - MonitorInstancesCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + MonitorInstancesCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2InstanceDetailedMonitoringEnabled implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2InstanceDetailedMonitoringEnabled', + description: 'Ensures that EC2 instances have detailed monitoring enabled.', + priority: 2, + priorityReason: 'Detailed monitoring provides enhanced visibility into instance performance metrics.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Monitoring and Logging', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve the list of all EC2 instances and their monitoring state.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'MonitorInstancesCommand', + reason: 'Enable detailed monitoring for non-compliant EC2 instances.', + }, + ], + adviseBeforeFixFunction: + 'Ensure enabling detailed monitoring aligns with organizational policies and cost considerations.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (instance.Monitoring?.State === 'enabled') { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } else { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { - await this.client.send( - new MonitorInstancesCommand({ - InstanceIds: nonCompliantResources - }) - ) - } + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { + if (nonCompliantResources.length > 0) { + await this.client.send( + new MonitorInstancesCommand({ + InstanceIds: nonCompliantResources, + }) + ); + } + }; } diff --git a/src/bpsets/ec2/EC2InstanceManagedBySystemsManager.ts b/src/bpsets/ec2/EC2InstanceManagedBySystemsManager.ts index 1930beb..eb929fa 100644 --- a/src/bpsets/ec2/EC2InstanceManagedBySystemsManager.ts +++ b/src/bpsets/ec2/EC2InstanceManagedBySystemsManager.ts @@ -1,48 +1,104 @@ import { EC2Client, - DescribeInstancesCommand -} from '@aws-sdk/client-ec2' -import { SSMClient, DescribeInstanceInformationCommand } from '@aws-sdk/client-ssm' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeInstancesCommand, +} from '@aws-sdk/client-ec2'; +import { + SSMClient, + DescribeInstanceInformationCommand, +} from '@aws-sdk/client-ssm'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2InstanceManagedBySystemsManager implements BPSet { - private readonly client = new EC2Client({}) - private readonly ssmClient = new SSMClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly ssmClient = new SSMClient({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2InstanceManagedBySystemsManager', + description: 'Ensures that EC2 instances are managed by AWS Systems Manager.', + priority: 2, + priorityReason: 'Management through Systems Manager ensures efficient and secure configuration and operation of EC2 instances.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Management and Governance', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInFixFunction: [], + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve the list of all EC2 instances.', + }, + { + name: 'DescribeInstanceInformationCommand', + reason: 'Retrieve information about instances managed by Systems Manager.', + }, + ], + adviseBeforeFixFunction: + 'Ensure Systems Manager Agent (SSM Agent) is installed and configured properly on non-compliant instances.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); const ssmResponse = await this.ssmClient.send( new DescribeInstanceInformationCommand({}) - ) + ); const managedInstanceIds = ssmResponse.InstanceInformationList?.map( - info => info.InstanceId - ) + (info) => info.InstanceId + ); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (managedInstanceIds?.includes(instance.InstanceId!)) { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } else { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async () => { + public readonly fix: BPSetFixFn = async () => { throw new Error( 'Fix logic for EC2InstanceManagedBySystemsManager is not directly applicable. Systems Manager Agent setup requires manual intervention.' - ) - } + ); + }; } diff --git a/src/bpsets/ec2/EC2InstanceProfileAttached.ts b/src/bpsets/ec2/EC2InstanceProfileAttached.ts index 99ff713..793ffa5 100644 --- a/src/bpsets/ec2/EC2InstanceProfileAttached.ts +++ b/src/bpsets/ec2/EC2InstanceProfileAttached.ts @@ -1,56 +1,127 @@ import { EC2Client, DescribeInstancesCommand, - AssociateIamInstanceProfileCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + AssociateIamInstanceProfileCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2InstanceProfileAttached implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2InstanceProfileAttached', + description: 'Ensures that all EC2 instances have an IAM instance profile attached.', + priority: 2, + priorityReason: 'Attaching an IAM instance profile enables instances to securely interact with AWS services.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'iam-instance-profile', + description: 'The name of the IAM instance profile to attach.', + default: '', + example: 'EC2InstanceProfile', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve all EC2 instances and their associated IAM instance profiles.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'AssociateIamInstanceProfileCommand', + reason: 'Attach an IAM instance profile to non-compliant EC2 instances.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the specified IAM instance profile exists and aligns with your access control policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (instance.IamInstanceProfile) { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } else { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'iam-instance-profile' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const iamInstanceProfile = requiredParametersForFix.find( - param => param.name === 'iam-instance-profile' - )?.value + (param) => param.name === 'iam-instance-profile' + )?.value; if (!iamInstanceProfile) { - throw new Error("Required parameter 'iam-instance-profile' is missing.") + throw new Error("Required parameter 'iam-instance-profile' is missing."); } for (const instanceId of nonCompliantResources) { await this.client.send( new AssociateIamInstanceProfileCommand({ InstanceId: instanceId, - IamInstanceProfile: { Name: iamInstanceProfile } + IamInstanceProfile: { Name: iamInstanceProfile }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ec2/EC2NoAmazonKeyPair.ts b/src/bpsets/ec2/EC2NoAmazonKeyPair.ts index 9e92d89..f2b5d36 100644 --- a/src/bpsets/ec2/EC2NoAmazonKeyPair.ts +++ b/src/bpsets/ec2/EC2NoAmazonKeyPair.ts @@ -1,39 +1,88 @@ import { EC2Client, - DescribeInstancesCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeInstancesCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2NoAmazonKeyPair implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2NoAmazonKeyPair', + description: 'Ensures that EC2 instances are not using an Amazon Key Pair.', + priority: 3, + priorityReason: 'Amazon Key Pairs pose a potential security risk if not properly managed.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve all EC2 instances and verify if an Amazon Key Pair is used.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: + 'Ensure instances are launched without a Key Pair or configure SSH access using alternative mechanisms like Systems Manager Session Manager.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (instance.KeyName) { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } else { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async () => { throw new Error( 'Fix logic for EC2NoAmazonKeyPair is not applicable. Key pairs must be removed manually or during instance creation.' - ) - } + ); + }; } diff --git a/src/bpsets/ec2/EC2StoppedInstance.ts b/src/bpsets/ec2/EC2StoppedInstance.ts index 5dc6336..95cb261 100644 --- a/src/bpsets/ec2/EC2StoppedInstance.ts +++ b/src/bpsets/ec2/EC2StoppedInstance.ts @@ -1,46 +1,114 @@ import { EC2Client, DescribeInstancesCommand, - TerminateInstancesCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + TerminateInstancesCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2StoppedInstance implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2StoppedInstance', + description: 'Ensures that stopped EC2 instances are identified and terminated if necessary.', + priority: 3, + priorityReason: + 'Stopped instances can incur costs for storage and IP addresses without being in use.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Cost Optimization', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve EC2 instances to check their state.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'TerminateInstancesCommand', + reason: 'Terminate stopped EC2 instances to prevent unnecessary costs.', + }, + ], + adviseBeforeFixFunction: + 'Ensure terminated instances do not contain any critical data or configurations before proceeding.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { if (instance.State?.Name === 'stopped') { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } else { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { if (nonCompliantResources.length === 0) { - return // No stopped instances to terminate + return; // No stopped instances to terminate } await this.client.send( new TerminateInstancesCommand({ - InstanceIds: nonCompliantResources + InstanceIds: nonCompliantResources, }) - ) - } + ); + }; } diff --git a/src/bpsets/ec2/EC2TokenHopLimitCheck.ts b/src/bpsets/ec2/EC2TokenHopLimitCheck.ts index 6528ae2..d9db6fe 100644 --- a/src/bpsets/ec2/EC2TokenHopLimitCheck.ts +++ b/src/bpsets/ec2/EC2TokenHopLimitCheck.ts @@ -1,19 +1,77 @@ import { EC2Client, DescribeInstancesCommand, - ModifyInstanceMetadataOptionsCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyInstanceMetadataOptionsCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2TokenHopLimitCheck implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + public readonly getMetadata = () => ({ + name: 'EC2TokenHopLimitCheck', + description: 'Ensures that EC2 instances have a Metadata Options HttpPutResponseHopLimit of 1.', + priority: 3, + priorityReason: + 'Setting the HttpPutResponseHopLimit to 1 ensures secure access to the instance metadata.', + awsService: 'EC2', + awsServiceCategory: 'Compute', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeInstancesCommand', + reason: 'Retrieve EC2 instances and check the metadata options for HttpPutResponseHopLimit.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyInstanceMetadataOptionsCommand', + reason: 'Update the HttpPutResponseHopLimit to enforce secure metadata access.', + }, + ], + adviseBeforeFixFunction: + 'Ensure modifying instance metadata options aligns with your operational policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const response = await this.memoClient.send(new DescribeInstancesCommand({})) + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const response = await this.memoClient.send(new DescribeInstancesCommand({})); for (const reservation of response.Reservations || []) { for (const instance of reservation.Instances || []) { @@ -21,28 +79,38 @@ export class EC2TokenHopLimitCheck implements BPSet { instance.MetadataOptions?.HttpPutResponseHopLimit && instance.MetadataOptions.HttpPutResponseHopLimit < 2 ) { - compliantResources.push(instance.InstanceId!) + compliantResources.push(instance.InstanceId!); } else { - nonCompliantResources.push(instance.InstanceId!) + nonCompliantResources.push(instance.InstanceId!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const instanceId of nonCompliantResources) { await this.client.send( new ModifyInstanceMetadataOptionsCommand({ InstanceId: instanceId, - HttpPutResponseHopLimit: 1 + HttpPutResponseHopLimit: 1, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecr/ECRKmsEncryption1.ts b/src/bpsets/ecr/ECRKmsEncryption1.ts index 9a2e204..2022b59 100644 --- a/src/bpsets/ecr/ECRKmsEncryption1.ts +++ b/src/bpsets/ecr/ECRKmsEncryption1.ts @@ -5,75 +5,162 @@ import { ListImagesCommand, BatchGetImageCommand, PutImageCommand, - DeleteRepositoryCommand -} from '@aws-sdk/client-ecr' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DeleteRepositoryCommand, +} from '@aws-sdk/client-ecr'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECRKmsEncryption1 implements BPSet { - private readonly client = new ECRClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECRClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getRepositories = async () => { - const response = await this.memoClient.send(new DescribeRepositoriesCommand({})) - return response.repositories || [] - } + const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); + return response.repositories || []; + }; + + public readonly getMetadata = () => ({ + name: 'ECRKmsEncryption1', + description: 'Ensures ECR repositories are encrypted using AWS KMS.', + priority: 2, + priorityReason: 'Encrypting ECR repositories with KMS enhances data security and meets compliance requirements.', + awsService: 'ECR', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'The ID of the KMS key used to encrypt the ECR repository.', + default: '', + example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv', + }, + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeRepositoriesCommand', + reason: 'Retrieve the list of ECR repositories to verify encryption settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'CreateRepositoryCommand', + reason: 'Create a new repository with KMS encryption.', + }, + { + name: 'ListImagesCommand', + reason: 'List all images in the existing repository for migration.', + }, + { + name: 'BatchGetImageCommand', + reason: 'Retrieve image manifests for migration to the new repository.', + }, + { + name: 'PutImageCommand', + reason: 'Push images to the newly created repository.', + }, + { + name: 'DeleteRepositoryCommand', + reason: 'Delete the old repository after migration.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the specified KMS key is accessible, and deleting the old repository aligns with operational policies.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const repositories = await this.getRepositories() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const repositories = await this.getRepositories(); for (const repository of repositories) { if (repository.encryptionConfiguration?.encryptionType === 'KMS') { - compliantResources.push(repository.repositoryArn!) + compliantResources.push(repository.repositoryArn!); } else { - nonCompliantResources.push(repository.repositoryArn!) + nonCompliantResources.push(repository.repositoryArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'kms-key-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const arn of nonCompliantResources) { - const repositoryName = arn.split('/').pop()! + const repositoryName = arn.split('/').pop()!; // Create a new repository with KMS encryption - const newRepositoryName = `${repositoryName}-kms` + const newRepositoryName = `${repositoryName}-kms`; await this.client.send( new CreateRepositoryCommand({ repositoryName: newRepositoryName, encryptionConfiguration: { encryptionType: 'KMS', - kmsKey: kmsKeyId - } + kmsKey: kmsKeyId, + }, }) - ) + ); // Get all images in the existing repository const listImagesResponse = await this.client.send( new ListImagesCommand({ repositoryName }) - ) - const imageIds = listImagesResponse.imageIds || [] + ); + const imageIds = listImagesResponse.imageIds || []; if (imageIds.length > 0) { const batchGetImageResponse = await this.client.send( new BatchGetImageCommand({ repositoryName, imageIds }) - ) + ); // Push images to the new repository for (const image of batchGetImageResponse.images || []) { @@ -81,9 +168,9 @@ export class ECRKmsEncryption1 implements BPSet { new PutImageCommand({ repositoryName: newRepositoryName, imageManifest: image.imageManifest, - imageTag: image.imageId?.imageTag + imageTag: image.imageId?.imageTag, }) - ) + ); } } @@ -91,9 +178,9 @@ export class ECRKmsEncryption1 implements BPSet { await this.client.send( new DeleteRepositoryCommand({ repositoryName, - force: true + force: true, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecr/ECRPrivateImageScanningEnabled.ts b/src/bpsets/ecr/ECRPrivateImageScanningEnabled.ts index 464d8d3..a0eef77 100644 --- a/src/bpsets/ecr/ECRPrivateImageScanningEnabled.ts +++ b/src/bpsets/ecr/ECRPrivateImageScanningEnabled.ts @@ -1,50 +1,118 @@ import { ECRClient, DescribeRepositoriesCommand, - PutImageScanningConfigurationCommand -} from '@aws-sdk/client-ecr' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutImageScanningConfigurationCommand, +} from '@aws-sdk/client-ecr'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECRPrivateImageScanningEnabled implements BPSet { - private readonly client = new ECRClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECRClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getRepositories = async () => { - const response = await this.memoClient.send(new DescribeRepositoriesCommand({})) - return response.repositories || [] - } + const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); + return response.repositories || []; + }; + + public readonly getMetadata = () => ({ + name: 'ECRPrivateImageScanningEnabled', + description: 'Ensures that image scanning on push is enabled for private ECR repositories.', + priority: 2, + priorityReason: + 'Enabling image scanning on push helps identify vulnerabilities in container images during upload.', + awsService: 'ECR', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeRepositoriesCommand', + reason: 'Retrieve all ECR repositories and check their image scanning configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutImageScanningConfigurationCommand', + reason: 'Enable image scanning on push for non-compliant repositories.', + }, + ], + adviseBeforeFixFunction: + 'Ensure enabling image scanning aligns with organizational policies and does not affect performance.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const repositories = await this.getRepositories() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const repositories = await this.getRepositories(); for (const repository of repositories) { if (repository.imageScanningConfiguration?.scanOnPush) { - compliantResources.push(repository.repositoryArn!) + compliantResources.push(repository.repositoryArn!); } else { - nonCompliantResources.push(repository.repositoryArn!) + nonCompliantResources.push(repository.repositoryArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const repositoryName = arn.split('/').pop()! + const repositoryName = arn.split('/').pop()!; await this.client.send( new PutImageScanningConfigurationCommand({ repositoryName, - imageScanningConfiguration: { scanOnPush: true } + imageScanningConfiguration: { scanOnPush: true }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecr/ECRPrivateLifecyclePolicyConfigured.ts b/src/bpsets/ecr/ECRPrivateLifecyclePolicyConfigured.ts index 59e5bcf..457bc6e 100644 --- a/src/bpsets/ecr/ECRPrivateLifecyclePolicyConfigured.ts +++ b/src/bpsets/ecr/ECRPrivateLifecyclePolicyConfigured.ts @@ -2,71 +2,147 @@ import { ECRClient, DescribeRepositoriesCommand, PutLifecyclePolicyCommand, - GetLifecyclePolicyCommand -} from '@aws-sdk/client-ecr' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + GetLifecyclePolicyCommand, +} from '@aws-sdk/client-ecr'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECRPrivateLifecyclePolicyConfigured implements BPSet { - private readonly client = new ECRClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECRClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getRepositories = async () => { - const response = await this.memoClient.send(new DescribeRepositoriesCommand({})) - return response.repositories || [] - } + const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); + return response.repositories || []; + }; + + public readonly getMetadata = () => ({ + name: 'ECRPrivateLifecyclePolicyConfigured', + description: 'Ensures that private ECR repositories have lifecycle policies configured.', + priority: 3, + priorityReason: + 'Lifecycle policies reduce unnecessary costs by managing image retention and cleanup in ECR repositories.', + awsService: 'ECR', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Cost Optimization', + requiredParametersForFix: [ + { + name: 'lifecycle-policy', + description: 'The JSON-formatted lifecycle policy text to apply to the repositories.', + default: '', + example: '{"rules":[{"rulePriority":1,"description":"Expire untagged images older than 30 days","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countNumber":30,"countUnit":"days"},"action":{"type":"expire"}}]}', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeRepositoriesCommand', + reason: 'Retrieve all ECR repositories to check their lifecycle policy status.', + }, + { + name: 'GetLifecyclePolicyCommand', + reason: 'Verify if a lifecycle policy exists for each repository.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutLifecyclePolicyCommand', + reason: 'Apply a lifecycle policy to non-compliant ECR repositories.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the provided lifecycle policy is well-tested and does not inadvertently remove critical images.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const repositories = await this.getRepositories() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const repositories = await this.getRepositories(); for (const repository of repositories) { try { await this.client.send( new GetLifecyclePolicyCommand({ registryId: repository.registryId, - repositoryName: repository.repositoryName + repositoryName: repository.repositoryName, }) - ) - compliantResources.push(repository.repositoryArn!) + ); + compliantResources.push(repository.repositoryArn!); } catch (error: any) { if (error.name === 'LifecyclePolicyNotFoundException') { - nonCompliantResources.push(repository.repositoryArn!) + nonCompliantResources.push(repository.repositoryArn!); } else { - throw error + throw error; } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'lifecycle-policy' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const lifecyclePolicy = requiredParametersForFix.find( - param => param.name === 'lifecycle-policy' - )?.value + (param) => param.name === 'lifecycle-policy' + )?.value; if (!lifecyclePolicy) { - throw new Error("Required parameter 'lifecycle-policy' is missing.") + throw new Error("Required parameter 'lifecycle-policy' is missing."); } for (const arn of nonCompliantResources) { - const repositoryName = arn.split('/').pop()! + const repositoryName = arn.split('/').pop()!; await this.client.send( new PutLifecyclePolicyCommand({ repositoryName, - lifecyclePolicyText: lifecyclePolicy + lifecyclePolicyText: lifecyclePolicy, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecr/ECRPrivateTagImmutabilityEnabled.ts b/src/bpsets/ecr/ECRPrivateTagImmutabilityEnabled.ts index b7f5b9d..76e1672 100644 --- a/src/bpsets/ecr/ECRPrivateTagImmutabilityEnabled.ts +++ b/src/bpsets/ecr/ECRPrivateTagImmutabilityEnabled.ts @@ -1,50 +1,119 @@ import { ECRClient, DescribeRepositoriesCommand, - PutImageTagMutabilityCommand -} from '@aws-sdk/client-ecr' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutImageTagMutabilityCommand, +} from '@aws-sdk/client-ecr'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECRPrivateTagImmutabilityEnabled implements BPSet { - private readonly client = new ECRClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECRClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getRepositories = async () => { - const response = await this.memoClient.send(new DescribeRepositoriesCommand({})) - return response.repositories || [] - } + const response = await this.memoClient.send(new DescribeRepositoriesCommand({})); + return response.repositories || []; + }; + + public readonly getMetadata = () => ({ + name: 'ECRPrivateTagImmutabilityEnabled', + description: 'Ensures that private ECR repositories have tag immutability enabled.', + priority: 3, + priorityReason: + 'Enabling tag immutability prevents accidental overwrites of image tags, ensuring integrity and reproducibility.', + awsService: 'ECR', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeRepositoriesCommand', + reason: 'Retrieve all ECR repositories to check their tag mutability setting.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutImageTagMutabilityCommand', + reason: 'Set image tag immutability for non-compliant repositories.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that enabling tag immutability aligns with your development and deployment processes.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const repositories = await this.getRepositories() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const repositories = await this.getRepositories(); for (const repository of repositories) { if (repository.imageTagMutability === 'IMMUTABLE') { - compliantResources.push(repository.repositoryArn!) + compliantResources.push(repository.repositoryArn!); } else { - nonCompliantResources.push(repository.repositoryArn!) + nonCompliantResources.push(repository.repositoryArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const repositoryName = arn.split('/').pop()! + const repositoryName = arn.split('/').pop()!; await this.client.send( new PutImageTagMutabilityCommand({ repositoryName, - imageTagMutability: 'IMMUTABLE' + imageTagMutability: 'IMMUTABLE', }) - ) + ); } - } + }; } + diff --git a/src/bpsets/ecs/ECSAwsVpcNetworkingEnabled.ts b/src/bpsets/ecs/ECSAwsVpcNetworkingEnabled.ts index f090edc..9024651 100644 --- a/src/bpsets/ecs/ECSAwsVpcNetworkingEnabled.ts +++ b/src/bpsets/ecs/ECSAwsVpcNetworkingEnabled.ts @@ -2,55 +2,127 @@ import { ECSClient, ListTaskDefinitionsCommand, DescribeTaskDefinitionCommand, - RegisterTaskDefinitionCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RegisterTaskDefinitionCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSAwsVpcNetworkingEnabled implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSAwsVpcNetworkingEnabled', + description: 'Ensures that ECS task definitions are configured to use the awsvpc network mode.', + priority: 3, + priorityReason: + 'Using the awsvpc network mode provides enhanced security and networking capabilities.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Describe details of each ECS task definition.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with awsvpc network mode.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the awsvpc network mode is supported by your ECS setup and compatible with your workloads.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { if (taskDefinition.networkMode === 'awsvpc') { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } else { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; await this.client.send( new RegisterTaskDefinitionCommand({ @@ -59,9 +131,9 @@ export class ECSAwsVpcNetworkingEnabled implements BPSet { networkMode: 'awsvpc', requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSContainerInsightsEnabled.ts b/src/bpsets/ecs/ECSContainerInsightsEnabled.ts index a1af608..edc429c 100644 --- a/src/bpsets/ecs/ECSContainerInsightsEnabled.ts +++ b/src/bpsets/ecs/ECSContainerInsightsEnabled.ts @@ -1,51 +1,121 @@ import { ECSClient, DescribeClustersCommand, - UpdateClusterSettingsCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateClusterSettingsCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSContainerInsightsEnabled implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const response = await this.memoClient.send(new DescribeClustersCommand({ include: ['SETTINGS'] })) - return response.clusters || [] - } + const response = await this.memoClient.send( + new DescribeClustersCommand({ include: ['SETTINGS'] }) + ); + return response.clusters || []; + }; + + public readonly getMetadata = () => ({ + name: 'ECSContainerInsightsEnabled', + description: 'Ensures that ECS clusters have Container Insights enabled.', + priority: 3, + priorityReason: + 'Enabling Container Insights provides enhanced monitoring and diagnostics for ECS clusters.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Monitoring', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeClustersCommand', + reason: 'Retrieve ECS clusters and their settings to check if Container Insights is enabled.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateClusterSettingsCommand', + reason: 'Enable Container Insights for non-compliant ECS clusters.', + }, + ], + adviseBeforeFixFunction: + 'Ensure enabling Container Insights aligns with your monitoring strategy and does not incur unexpected costs.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { const containerInsightsSetting = cluster.settings?.find( - setting => setting.name === 'containerInsights' - ) + (setting) => setting.name === 'containerInsights' + ); if (containerInsightsSetting?.value === 'enabled') { - compliantResources.push(cluster.clusterArn!) + compliantResources.push(cluster.clusterArn!); } else { - nonCompliantResources.push(cluster.clusterArn!) + nonCompliantResources.push(cluster.clusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { await this.client.send( new UpdateClusterSettingsCommand({ cluster: arn, - settings: [{ name: 'containerInsights', value: 'enabled' }] + settings: [{ name: 'containerInsights', value: 'enabled' }], }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSContainersNonPrivileged.ts b/src/bpsets/ecs/ECSContainersNonPrivileged.ts index 7289f86..12d85c2 100644 --- a/src/bpsets/ecs/ECSContainersNonPrivileged.ts +++ b/src/bpsets/ecs/ECSContainersNonPrivileged.ts @@ -2,65 +2,137 @@ import { ECSClient, DescribeTaskDefinitionCommand, RegisterTaskDefinitionCommand, - ListTaskDefinitionsCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ListTaskDefinitionsCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSContainersNonPrivileged implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSContainersNonPrivileged', + description: 'Ensures that containers in ECS task definitions are not running in privileged mode.', + priority: 1, + priorityReason: + 'Running containers in privileged mode poses significant security risks.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Check the container configurations in ECS task definitions.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with privileged mode disabled.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that containers do not rely on privileged mode for their functionality.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { const privilegedContainers = taskDefinition.containerDefinitions?.filter( - container => container.privileged - ) + (container) => container.privileged + ); if (privilegedContainers?.length) { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } else { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( - container => ({ + (container) => ({ ...container, - privileged: false + privileged: false, }) - ) + ); await this.client.send( new RegisterTaskDefinitionCommand({ @@ -69,9 +141,9 @@ export class ECSContainersNonPrivileged implements BPSet { networkMode: taskDefinition.taskDefinition?.networkMode, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSContainersReadonlyAccess.ts b/src/bpsets/ecs/ECSContainersReadonlyAccess.ts index 641f11e..230f0b6 100644 --- a/src/bpsets/ecs/ECSContainersReadonlyAccess.ts +++ b/src/bpsets/ecs/ECSContainersReadonlyAccess.ts @@ -2,65 +2,137 @@ import { ECSClient, DescribeTaskDefinitionCommand, RegisterTaskDefinitionCommand, - ListTaskDefinitionsCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ListTaskDefinitionsCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSContainersReadonlyAccess implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSContainersReadonlyAccess', + description: 'Ensures that containers in ECS task definitions have readonly root filesystems enabled.', + priority: 1, + priorityReason: + 'Readonly root filesystems prevent unauthorized changes to the container filesystem, enhancing security.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Check the container configurations in ECS task definitions.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with readonly root filesystems enabled.', + }, + ], + adviseBeforeFixFunction: + 'Ensure enabling readonly root filesystems does not interfere with the functionality of your containers.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { const notReadonlyContainers = taskDefinition.containerDefinitions?.filter( - container => !container.readonlyRootFilesystem - ) + (container) => !container.readonlyRootFilesystem + ); if (notReadonlyContainers?.length) { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } else { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( - container => ({ + (container) => ({ ...container, - readonlyRootFilesystem: true + readonlyRootFilesystem: true, }) - ) + ); await this.client.send( new RegisterTaskDefinitionCommand({ @@ -69,9 +141,9 @@ export class ECSContainersReadonlyAccess implements BPSet { networkMode: taskDefinition.taskDefinition?.networkMode, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSFargateLatestPlatformVersion.ts b/src/bpsets/ecs/ECSFargateLatestPlatformVersion.ts index e48a13f..1c3466a 100644 --- a/src/bpsets/ecs/ECSFargateLatestPlatformVersion.ts +++ b/src/bpsets/ecs/ECSFargateLatestPlatformVersion.ts @@ -3,68 +3,144 @@ import { ListClustersCommand, ListServicesCommand, DescribeServicesCommand, - UpdateServiceCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateServiceCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSFargateLatestPlatformVersion implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getServices = async () => { - const clustersResponse = await this.memoClient.send(new ListClustersCommand({})) - const clusterArns = clustersResponse.clusterArns || [] - const services: { clusterArn: string; serviceArn: string }[] = [] + const clustersResponse = await this.memoClient.send(new ListClustersCommand({})); + const clusterArns = clustersResponse.clusterArns || []; + const services: { clusterArn: string; serviceArn: string }[] = []; for (const clusterArn of clusterArns) { const servicesResponse = await this.memoClient.send( new ListServicesCommand({ cluster: clusterArn }) - ) + ); for (const serviceArn of servicesResponse.serviceArns || []) { - services.push({ clusterArn, serviceArn }) + services.push({ clusterArn, serviceArn }); } } - return services - } + return services; + }; + + public readonly getMetadata = () => ({ + name: 'ECSFargateLatestPlatformVersion', + description: 'Ensures ECS Fargate services are using the latest platform version.', + priority: 3, + priorityReason: + 'Using the latest platform version ensures access to the latest features, updates, and security patches.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Performance and Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListClustersCommand', + reason: 'Retrieve ECS clusters to identify associated services.', + }, + { + name: 'ListServicesCommand', + reason: 'Retrieve services associated with each ECS cluster.', + }, + { + name: 'DescribeServicesCommand', + reason: 'Check the platform version of each ECS service.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateServiceCommand', + reason: 'Update ECS services to use the latest platform version.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that updating to the latest platform version aligns with your workload requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const services = await this.getServices() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const services = await this.getServices(); for (const { clusterArn, serviceArn } of services) { const serviceResponse = await this.memoClient.send( new DescribeServicesCommand({ cluster: clusterArn, services: [serviceArn] }) - ) + ); - const service = serviceResponse.services?.[0] + const service = serviceResponse.services?.[0]; if (service?.platformVersion === 'LATEST') { - compliantResources.push(service.serviceArn!) + compliantResources.push(service.serviceArn!); } else { - nonCompliantResources.push(service?.serviceArn!) + nonCompliantResources.push(service?.serviceArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async (nonCompliantResources) => { for (const serviceArn of nonCompliantResources) { - const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0] + const clusterArn = serviceArn.split(':cluster/')[1].split(':service/')[0]; await this.client.send( new UpdateServiceCommand({ cluster: clusterArn, service: serviceArn, - platformVersion: 'LATEST' + platformVersion: 'LATEST', }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSTaskDefinitionLogConfiguration.ts b/src/bpsets/ecs/ECSTaskDefinitionLogConfiguration.ts index d96fd13..ab085ca 100644 --- a/src/bpsets/ecs/ECSTaskDefinitionLogConfiguration.ts +++ b/src/bpsets/ecs/ECSTaskDefinitionLogConfiguration.ts @@ -2,76 +2,155 @@ import { ECSClient, ListTaskDefinitionsCommand, DescribeTaskDefinitionCommand, - RegisterTaskDefinitionCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RegisterTaskDefinitionCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSTaskDefinitionLogConfiguration implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSTaskDefinitionLogConfiguration', + description: 'Ensures that ECS task definitions have log configuration enabled.', + priority: 1, + priorityReason: + 'Enabling log configuration is critical for monitoring and debugging container workloads.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Monitoring', + requiredParametersForFix: [ + { + name: 'log-configuration', + description: 'The log configuration to apply to non-compliant task definitions.', + default: '{}', + example: '{"logDriver": "awslogs", "options": {"awslogs-group": "/ecs/logs"}}', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Check the container configurations in ECS task definitions.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with updated log configuration.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the provided log configuration aligns with your logging strategy and infrastructure.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { const logDisabledContainers = taskDefinition.containerDefinitions?.filter( - container => !container.logConfiguration - ) + (container) => !container.logConfiguration + ); if (logDisabledContainers?.length) { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } else { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-configuration' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix ) => { const logConfiguration = requiredParametersForFix.find( - param => param.name === 'log-configuration' - )?.value + (param) => param.name === 'log-configuration' + )?.value; if (!logConfiguration) { - throw new Error("Required parameter 'log-configuration' is missing.") + throw new Error("Required parameter 'log-configuration' is missing."); } for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( - container => ({ + (container) => ({ ...container, - logConfiguration: JSON.parse(logConfiguration) + logConfiguration: JSON.parse(logConfiguration), }) - ) + ); await this.client.send( new RegisterTaskDefinitionCommand({ @@ -80,9 +159,9 @@ export class ECSTaskDefinitionLogConfiguration implements BPSet { networkMode: taskDefinition.taskDefinition?.networkMode, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSTaskDefinitionMemoryHardLimit.ts b/src/bpsets/ecs/ECSTaskDefinitionMemoryHardLimit.ts index b2d83cb..e1c18f4 100644 --- a/src/bpsets/ecs/ECSTaskDefinitionMemoryHardLimit.ts +++ b/src/bpsets/ecs/ECSTaskDefinitionMemoryHardLimit.ts @@ -2,65 +2,152 @@ import { ECSClient, ListTaskDefinitionsCommand, DescribeTaskDefinitionCommand, - RegisterTaskDefinitionCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RegisterTaskDefinitionCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSTaskDefinitionMemoryHardLimit implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSTaskDefinitionMemoryHardLimit', + description: 'Ensures all containers in ECS task definitions have a memory hard limit set.', + priority: 1, + priorityReason: + 'Setting memory hard limits is essential to prevent resource contention and ensure container isolation.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Resource Management', + requiredParametersForFix: [ + { + name: 'default-memory-limit', + description: 'The default memory hard limit to set for containers without a memory limit.', + default: '512', + example: '1024', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Check container configurations in ECS task definitions for memory limits.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with updated memory limits.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the default memory limit aligns with your container resource requirements.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { const containersWithoutMemoryLimit = taskDefinition.containerDefinitions?.filter( - container => !container.memory - ) + (container) => !container.memory + ); if (containersWithoutMemoryLimit?.length) { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } else { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix + ) => { + const defaultMemoryLimit = parseInt( + requiredParametersForFix.find((param) => param.name === 'default-memory-limit')?.value || '512', + 10 + ); - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( - container => ({ + (container) => ({ ...container, - memory: container.memory || 512 // Default hard limit memory value + memory: container.memory || defaultMemoryLimit, }) - ) + ); await this.client.send( new RegisterTaskDefinitionCommand({ @@ -69,9 +156,9 @@ export class ECSTaskDefinitionMemoryHardLimit implements BPSet { networkMode: taskDefinition.taskDefinition?.networkMode, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/ecs/ECSTaskDefinitionNonRootUser.ts b/src/bpsets/ecs/ECSTaskDefinitionNonRootUser.ts index 1b81207..cffcf3a 100644 --- a/src/bpsets/ecs/ECSTaskDefinitionNonRootUser.ts +++ b/src/bpsets/ecs/ECSTaskDefinitionNonRootUser.ts @@ -2,65 +2,151 @@ import { ECSClient, ListTaskDefinitionsCommand, DescribeTaskDefinitionCommand, - RegisterTaskDefinitionCommand -} from '@aws-sdk/client-ecs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RegisterTaskDefinitionCommand, +} from '@aws-sdk/client-ecs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ECSTaskDefinitionNonRootUser implements BPSet { - private readonly client = new ECSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ECSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getTaskDefinitions = async () => { const taskDefinitionArns = await this.memoClient.send( new ListTaskDefinitionsCommand({ status: 'ACTIVE' }) - ) - const taskDefinitions = [] + ); + const taskDefinitions = []; for (const arn of taskDefinitionArns.taskDefinitionArns || []) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - taskDefinitions.push(taskDefinition.taskDefinition!) + ); + taskDefinitions.push(taskDefinition.taskDefinition!); } - return taskDefinitions - } + return taskDefinitions; + }; + + public readonly getMetadata = () => ({ + name: 'ECSTaskDefinitionNonRootUser', + description: 'Ensures all ECS containers in task definitions run as non-root users.', + priority: 1, + priorityReason: + 'Running containers as non-root users improves security by reducing the potential impact of compromised containers.', + awsService: 'ECS', + awsServiceCategory: 'Container', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'default-non-root-user', + description: 'The default non-root user to assign for containers without a specified user.', + default: 'ecs-user', + example: 'app-user', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTaskDefinitionsCommand', + reason: 'Retrieve all active ECS task definitions.', + }, + { + name: 'DescribeTaskDefinitionCommand', + reason: 'Check container configurations in ECS task definitions for user settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RegisterTaskDefinitionCommand', + reason: 'Re-register ECS task definitions with non-root user configurations.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the default non-root user has sufficient permissions to execute the container workload.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const taskDefinitions = await this.getTaskDefinitions() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const taskDefinitions = await this.getTaskDefinitions(); for (const taskDefinition of taskDefinitions) { const privilegedContainers = taskDefinition.containerDefinitions?.filter( - container => !container.user || container.user === 'root' - ) + (container) => !container.user || container.user === 'root' + ); if (privilegedContainers?.length) { - nonCompliantResources.push(taskDefinition.taskDefinitionArn!) + nonCompliantResources.push(taskDefinition.taskDefinitionArn!); } else { - compliantResources.push(taskDefinition.taskDefinitionArn!) + compliantResources.push(taskDefinition.taskDefinitionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix + ) => { + const defaultNonRootUser = requiredParametersForFix.find( + (param) => param.name === 'default-non-root-user' + )?.value || 'ecs-user'; - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { const taskDefinition = await this.memoClient.send( new DescribeTaskDefinitionCommand({ taskDefinition: arn }) - ) - const family = taskDefinition.taskDefinition?.family + ); + const family = taskDefinition.taskDefinition?.family; const updatedContainers = taskDefinition.taskDefinition?.containerDefinitions?.map( - container => ({ + (container) => ({ ...container, - user: container.user || 'ecs-user' // Default non-root user + user: container.user || defaultNonRootUser, }) - ) + ); await this.client.send( new RegisterTaskDefinitionCommand({ @@ -69,9 +155,9 @@ export class ECSTaskDefinitionNonRootUser implements BPSet { networkMode: taskDefinition.taskDefinition?.networkMode, requiresCompatibilities: taskDefinition.taskDefinition?.requiresCompatibilities, cpu: taskDefinition.taskDefinition?.cpu, - memory: taskDefinition.taskDefinition?.memory + memory: taskDefinition.taskDefinition?.memory, }) - ) + ); } - } + }; } diff --git a/src/bpsets/efs/EFSAccessPointEnforceRootDirectory.ts b/src/bpsets/efs/EFSAccessPointEnforceRootDirectory.ts index 77bf5e5..b2e9436 100644 --- a/src/bpsets/efs/EFSAccessPointEnforceRootDirectory.ts +++ b/src/bpsets/efs/EFSAccessPointEnforceRootDirectory.ts @@ -2,70 +2,149 @@ import { EFSClient, DescribeAccessPointsCommand, DeleteAccessPointCommand, - CreateAccessPointCommand -} from '@aws-sdk/client-efs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + CreateAccessPointCommand, +} from '@aws-sdk/client-efs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EFSAccessPointEnforceRootDirectory implements BPSet { - private readonly client = new EFSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EFSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAccessPoints = async () => { - const response = await this.memoClient.send(new DescribeAccessPointsCommand({})) - return response.AccessPoints || [] - } + const response = await this.memoClient.send(new DescribeAccessPointsCommand({})); + return response.AccessPoints || []; + }; + + public readonly getMetadata = () => ({ + name: 'EFSAccessPointEnforceRootDirectory', + description: 'Ensures that EFS Access Points enforce a specific root directory.', + priority: 1, + priorityReason: + 'Enforcing a consistent root directory path for EFS Access Points ensures proper access control and organization.', + awsService: 'EFS', + awsServiceCategory: 'File System', + bestPracticeCategory: 'Resource Configuration', + requiredParametersForFix: [ + { + name: 'root-directory-path', + description: 'The root directory path to enforce for EFS Access Points.', + default: '/', + example: '/data', + }, + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeAccessPointsCommand', + reason: 'Retrieve all existing EFS Access Points and their configurations.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'DeleteAccessPointCommand', + reason: 'Delete non-compliant EFS Access Points.', + }, + { + name: 'CreateAccessPointCommand', + reason: 'Recreate EFS Access Points with the enforced root directory.', + }, + ], + adviseBeforeFixFunction: + 'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const accessPoints = await this.getAccessPoints() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const accessPoints = await this.getAccessPoints(); for (const accessPoint of accessPoints) { - if (accessPoint.RootDirectory?.Path !== '/') { - compliantResources.push(accessPoint.AccessPointArn!) + if (accessPoint.RootDirectory?.Path === '/') { + compliantResources.push(accessPoint.AccessPointArn!); } else { - nonCompliantResources.push(accessPoint.AccessPointArn!) + nonCompliantResources.push(accessPoint.AccessPointArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'root-directory-path' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix ) => { const rootDirectoryPath = requiredParametersForFix.find( - param => param.name === 'root-directory-path' - )?.value + (param) => param.name === 'root-directory-path' + )?.value; if (!rootDirectoryPath) { - throw new Error("Required parameter 'root-directory-path' is missing.") + throw new Error("Required parameter 'root-directory-path' is missing."); } for (const arn of nonCompliantResources) { - const accessPointId = arn.split('/').pop()! - const fileSystemId = arn.split(':file-system/')[1].split('/')[0] + const accessPointId = arn.split('/').pop()!; + const fileSystemId = arn.split(':file-system/')[1].split('/')[0]; // Delete the existing access point await this.client.send( new DeleteAccessPointCommand({ - AccessPointId: accessPointId + AccessPointId: accessPointId, }) - ) + ); // Recreate the access point with the desired root directory await this.client.send( new CreateAccessPointCommand({ FileSystemId: fileSystemId, - RootDirectory: { Path: rootDirectoryPath } + RootDirectory: { Path: rootDirectoryPath }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/efs/EFSAccessPointEnforceUserIdentity.ts b/src/bpsets/efs/EFSAccessPointEnforceUserIdentity.ts index 2fe7803..94e69b1 100644 --- a/src/bpsets/efs/EFSAccessPointEnforceUserIdentity.ts +++ b/src/bpsets/efs/EFSAccessPointEnforceUserIdentity.ts @@ -2,68 +2,147 @@ import { EFSClient, DescribeAccessPointsCommand, DeleteAccessPointCommand, - CreateAccessPointCommand -} from '@aws-sdk/client-efs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + CreateAccessPointCommand, +} from '@aws-sdk/client-efs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EFSAccessPointEnforceUserIdentity implements BPSet { - private readonly client = new EFSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EFSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getAccessPoints = async () => { - const response = await this.memoClient.send(new DescribeAccessPointsCommand({})) - return response.AccessPoints || [] - } + const response = await this.memoClient.send(new DescribeAccessPointsCommand({})); + return response.AccessPoints || []; + }; + + public readonly getMetadata = () => ({ + name: 'EFSAccessPointEnforceUserIdentity', + description: 'Ensures that EFS Access Points enforce a specific PosixUser identity.', + priority: 1, + priorityReason: + 'Setting a specific PosixUser identity for EFS Access Points ensures controlled access and proper security.', + awsService: 'EFS', + awsServiceCategory: 'File System', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'posix-user', + description: 'The PosixUser configuration to enforce for EFS Access Points.', + default: '{"Uid": "1000", "Gid": "1000"}', + example: '{"Uid": "1234", "Gid": "1234"}', + }, + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeAccessPointsCommand', + reason: 'Retrieve all existing EFS Access Points and their configurations.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'DeleteAccessPointCommand', + reason: 'Delete non-compliant EFS Access Points.', + }, + { + name: 'CreateAccessPointCommand', + reason: 'Recreate EFS Access Points with the enforced PosixUser.', + }, + ], + adviseBeforeFixFunction: + 'Ensure no active workloads are using the access points before applying fixes as it involves deletion and recreation.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const accessPoints = await this.getAccessPoints() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const accessPoints = await this.getAccessPoints(); for (const accessPoint of accessPoints) { if (accessPoint.PosixUser) { - compliantResources.push(accessPoint.AccessPointArn!) + compliantResources.push(accessPoint.AccessPointArn!); } else { - nonCompliantResources.push(accessPoint.AccessPointArn!) + nonCompliantResources.push(accessPoint.AccessPointArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'posix-user' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix ) => { - const posixUser = requiredParametersForFix.find(param => param.name === 'posix-user')?.value + const posixUser = requiredParametersForFix.find((param) => param.name === 'posix-user')?.value; if (!posixUser) { - throw new Error("Required parameter 'posix-user' is missing.") + throw new Error("Required parameter 'posix-user' is missing."); } for (const arn of nonCompliantResources) { - const accessPointId = arn.split('/').pop()! - const fileSystemId = arn.split(':file-system/')[1].split('/')[0] + const accessPointId = arn.split('/').pop()!; + const fileSystemId = arn.split(':file-system/')[1].split('/')[0]; // Delete the existing access point await this.client.send( new DeleteAccessPointCommand({ - AccessPointId: accessPointId + AccessPointId: accessPointId, }) - ) + ); // Recreate the access point with the desired PosixUser await this.client.send( new CreateAccessPointCommand({ FileSystemId: fileSystemId, - PosixUser: JSON.parse(posixUser) + PosixUser: JSON.parse(posixUser), }) - ) + ); } - } + }; } diff --git a/src/bpsets/efs/EFSAutomaticBackupsEnabled.ts b/src/bpsets/efs/EFSAutomaticBackupsEnabled.ts index b24cdc3..2432476 100644 --- a/src/bpsets/efs/EFSAutomaticBackupsEnabled.ts +++ b/src/bpsets/efs/EFSAutomaticBackupsEnabled.ts @@ -2,54 +2,129 @@ import { EFSClient, DescribeFileSystemsCommand, PutBackupPolicyCommand, - DescribeBackupPolicyCommand -} from '@aws-sdk/client-efs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeBackupPolicyCommand, +} from '@aws-sdk/client-efs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EFSAutomaticBackupsEnabled implements BPSet { - private readonly client = new EFSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EFSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getFileSystems = async () => { - const response = await this.memoClient.send(new DescribeFileSystemsCommand({})) - return response.FileSystems || [] - } + const response = await this.memoClient.send(new DescribeFileSystemsCommand({})); + return response.FileSystems || []; + }; + + public readonly getMetadata = () => ({ + name: 'EFSAutomaticBackupsEnabled', + description: 'Ensures that EFS file systems have automatic backups enabled.', + priority: 1, + priorityReason: + 'Enabling automatic backups helps protect against data loss and supports recovery from unintended modifications or deletions.', + awsService: 'EFS', + awsServiceCategory: 'File System', + bestPracticeCategory: 'Backup and Recovery', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeFileSystemsCommand', + reason: 'Retrieve the list of EFS file systems.', + }, + { + name: 'DescribeBackupPolicyCommand', + reason: 'Check if a backup policy is enabled for the file system.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBackupPolicyCommand', + reason: 'Enable automatic backups for the file system.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that enabling backups aligns with the organization’s cost and recovery objectives.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const fileSystems = await this.getFileSystems() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const fileSystems = await this.getFileSystems(); for (const fileSystem of fileSystems) { const response = await this.client.send( new DescribeBackupPolicyCommand({ FileSystemId: fileSystem.FileSystemId! }) - ) + ); if (response.BackupPolicy?.Status === 'ENABLED') { - compliantResources.push(fileSystem.FileSystemArn!) + compliantResources.push(fileSystem.FileSystemArn!); } else { - nonCompliantResources.push(fileSystem.FileSystemArn!) + nonCompliantResources.push(fileSystem.FileSystemArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix + ) => { for (const arn of nonCompliantResources) { - const fileSystemId = arn.split('/').pop()! + const fileSystemId = arn.split('/').pop()!; await this.client.send( new PutBackupPolicyCommand({ FileSystemId: fileSystemId, - BackupPolicy: { Status: 'ENABLED' } + BackupPolicy: { Status: 'ENABLED' }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/efs/EFSEncryptedCheck.ts b/src/bpsets/efs/EFSEncryptedCheck.ts index 5c35292..5b66ff5 100644 --- a/src/bpsets/efs/EFSEncryptedCheck.ts +++ b/src/bpsets/efs/EFSEncryptedCheck.ts @@ -2,53 +2,128 @@ import { EFSClient, DescribeFileSystemsCommand, CreateFileSystemCommand, - DeleteFileSystemCommand -} from '@aws-sdk/client-efs' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DeleteFileSystemCommand, +} from '@aws-sdk/client-efs'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EFSEncryptedCheck implements BPSet { - private readonly client = new EFSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EFSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getFileSystems = async () => { - const response = await this.memoClient.send(new DescribeFileSystemsCommand({})) - return response.FileSystems || [] - } + const response = await this.memoClient.send(new DescribeFileSystemsCommand({})); + return response.FileSystems || []; + }; + + public readonly getMetadata = () => ({ + name: 'EFSEncryptedCheck', + description: 'Ensures that all EFS file systems are encrypted.', + priority: 1, + priorityReason: + 'Encrypting EFS file systems helps ensure data protection and compliance with security best practices.', + awsService: 'EFS', + awsServiceCategory: 'File System', + bestPracticeCategory: 'Encryption', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeFileSystemsCommand', + reason: 'Retrieve all existing EFS file systems and their encryption status.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'DeleteFileSystemCommand', + reason: 'Delete non-compliant EFS file systems.', + }, + { + name: 'CreateFileSystemCommand', + reason: 'Recreate EFS file systems with encryption enabled.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that backups are taken and data migration plans are in place, as the fix involves deletion and recreation of file systems.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const fileSystems = await this.getFileSystems() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const fileSystems = await this.getFileSystems(); for (const fileSystem of fileSystems) { if (fileSystem.Encrypted) { - compliantResources.push(fileSystem.FileSystemArn!) + compliantResources.push(fileSystem.FileSystemArn!); } else { - nonCompliantResources.push(fileSystem.FileSystemArn!) + nonCompliantResources.push(fileSystem.FileSystemArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (...args) => { + await this.fixImpl(...args).then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + public readonly fixImpl: BPSetFixFn = async ( + nonCompliantResources, + requiredParametersForFix + ) => { for (const arn of nonCompliantResources) { - const fileSystemId = arn.split('/').pop()! + const fileSystemId = arn.split('/').pop()!; const fileSystem = await this.memoClient.send( new DescribeFileSystemsCommand({ FileSystemId: fileSystemId }) - ) + ); // Delete the non-compliant file system await this.client.send( new DeleteFileSystemCommand({ - FileSystemId: fileSystemId + FileSystemId: fileSystemId, }) - ) + ); // Recreate the file system with encryption enabled await this.client.send( @@ -56,9 +131,9 @@ export class EFSEncryptedCheck implements BPSet { Encrypted: true, PerformanceMode: fileSystem.FileSystems?.[0]?.PerformanceMode, ThroughputMode: fileSystem.FileSystems?.[0]?.ThroughputMode, - ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps + ProvisionedThroughputInMibps: fileSystem.FileSystems?.[0]?.ProvisionedThroughputInMibps, }) - ) + ); } - } + }; } diff --git a/src/bpsets/efs/EFSMountTargetPublicAccessible.ts b/src/bpsets/efs/EFSMountTargetPublicAccessible.ts index 04b8d87..76b8c6d 100644 --- a/src/bpsets/efs/EFSMountTargetPublicAccessible.ts +++ b/src/bpsets/efs/EFSMountTargetPublicAccessible.ts @@ -1,71 +1,132 @@ import { EFSClient, DescribeFileSystemsCommand, - DescribeMountTargetsCommand -} from '@aws-sdk/client-efs' -import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeMountTargetsCommand, +} from '@aws-sdk/client-efs'; +import { EC2Client, DescribeRouteTablesCommand } from '@aws-sdk/client-ec2'; +import { BPSet, BPSetFixFn, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EFSMountTargetPublicAccessible implements BPSet { - private readonly efsClient = new EFSClient({}) - private readonly ec2Client = new EC2Client({}) - private readonly memoEFSClient = Memorizer.memo(this.efsClient) - private readonly memoEC2Client = Memorizer.memo(this.ec2Client) + private readonly efsClient = new EFSClient({}); + private readonly ec2Client = new EC2Client({}); + private readonly memoEFSClient = Memorizer.memo(this.efsClient); + private readonly memoEC2Client = Memorizer.memo(this.ec2Client); private readonly getFileSystems = async () => { - const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({})) - return response.FileSystems || [] - } + const response = await this.memoEFSClient.send(new DescribeFileSystemsCommand({})); + return response.FileSystems || []; + }; private readonly getRoutesForSubnet = async (subnetId: string) => { const response = await this.memoEC2Client.send( new DescribeRouteTablesCommand({ - Filters: [{ Name: 'association.subnet-id', Values: [subnetId] }] + Filters: [{ Name: 'association.subnet-id', Values: [subnetId] }], }) - ) - return response.RouteTables?.[0]?.Routes || [] - } + ); + return response.RouteTables?.[0]?.Routes || []; + }; + + public readonly getMetadata = () => ({ + name: 'EFSMountTargetPublicAccessible', + description: 'Checks if EFS mount targets are publicly accessible.', + priority: 2, + priorityReason: + 'Publicly accessible EFS mount targets may pose a security risk by exposing data to unintended access.', + awsService: 'EFS', + awsServiceCategory: 'File System', + bestPracticeCategory: 'Network Configuration', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeFileSystemsCommand', + reason: 'Retrieve the list of EFS file systems.', + }, + { + name: 'DescribeMountTargetsCommand', + reason: 'Retrieve the list of mount targets for a file system.', + }, + { + name: 'DescribeRouteTablesCommand', + reason: 'Check route tables associated with the mount target subnets.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: + 'Ensure that the network configurations are reviewed carefully to avoid breaking application connectivity.', + }); + + private readonly stats: BPSetStats = { + nonCompliantResources: [], + compliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const fileSystems = await this.getFileSystems() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const fileSystems = await this.getFileSystems(); for (const fileSystem of fileSystems) { const mountTargets = await this.memoEFSClient.send( new DescribeMountTargetsCommand({ FileSystemId: fileSystem.FileSystemId! }) - ) + ); + + let isNonCompliant = false; for (const mountTarget of mountTargets.MountTargets || []) { - const routes = await this.getRoutesForSubnet(mountTarget.SubnetId!) + const routes = await this.getRoutesForSubnet(mountTarget.SubnetId!); - for (const route of routes) { - if ( - route.DestinationCidrBlock === '0.0.0.0/0' && - route.GatewayId?.startsWith('igw-') - ) { - nonCompliantResources.push(fileSystem.FileSystemArn!) - break - } + if ( + routes.some( + (route) => + route.DestinationCidrBlock === '0.0.0.0/0' && route.GatewayId?.startsWith('igw-') + ) + ) { + nonCompliantResources.push(fileSystem.FileSystemArn!); + isNonCompliant = true; + break; } } - if (!nonCompliantResources.includes(fileSystem.FileSystemArn!)) { - compliantResources.push(fileSystem.FileSystemArn!) + if (!isNonCompliant) { + compliantResources.push(fileSystem.FileSystemArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async () => { throw new Error( 'Fixing public accessibility for mount targets requires manual network reconfiguration.' - ) - } + ); + }; } diff --git a/src/bpsets/eks/EKSClusterLoggingEnabled.ts b/src/bpsets/eks/EKSClusterLoggingEnabled.ts index c523f02..7ade5aa 100644 --- a/src/bpsets/eks/EKSClusterLoggingEnabled.ts +++ b/src/bpsets/eks/EKSClusterLoggingEnabled.ts @@ -2,52 +2,111 @@ import { EKSClient, ListClustersCommand, DescribeClusterCommand, - UpdateClusterConfigCommand -} from '@aws-sdk/client-eks' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateClusterConfigCommand, +} from '@aws-sdk/client-eks'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EKSClusterLoggingEnabled implements BPSet { - private readonly client = new EKSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EKSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})) - const clusterNames = clusterNamesResponse.clusters || [] - const clusters = [] + const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); + const clusterNames = clusterNamesResponse.clusters || []; + const clusters = []; for (const clusterName of clusterNames) { const cluster = await this.memoClient.send( new DescribeClusterCommand({ name: clusterName }) - ) - clusters.push(cluster.cluster!) + ); + clusters.push(cluster.cluster!); } - return clusters - } + return clusters; + }; + + public readonly getMetadata = () => ({ + name: 'EKSClusterLoggingEnabled', + description: 'Ensures that all EKS clusters have full logging enabled.', + priority: 1, + priorityReason: + 'Cluster logging is essential for monitoring, debugging, and auditing purposes.', + awsService: 'EKS', + awsServiceCategory: 'Kubernetes Service', + bestPracticeCategory: 'Observability', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListClustersCommand', + reason: 'Retrieve the list of EKS clusters.', + }, + { + name: 'DescribeClusterCommand', + reason: 'Fetch details about the EKS cluster, including logging configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateClusterConfigCommand', + reason: 'Enable all logging types for the EKS cluster.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that enabling full logging does not generate excessive costs or logs.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { - const clusterLogging = cluster.logging?.clusterLogging?.[0] + const clusterLogging = cluster.logging?.clusterLogging?.[0]; if (clusterLogging?.enabled && clusterLogging.types?.length === 5) { - compliantResources.push(cluster.arn!) + compliantResources.push(cluster.arn!); } else { - nonCompliantResources.push(cluster.arn!) + nonCompliantResources.push(cluster.arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const clusterName = arn.split(':cluster/')[1] + const clusterName = arn.split(':cluster/')[1]; await this.client.send( new UpdateClusterConfigCommand({ @@ -56,12 +115,12 @@ export class EKSClusterLoggingEnabled implements BPSet { clusterLogging: [ { enabled: true, - types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler'] - } - ] - } + types: ['api', 'audit', 'authenticator', 'controllerManager', 'scheduler'], + }, + ], + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/eks/EKSClusterSecretsEncrypted.ts b/src/bpsets/eks/EKSClusterSecretsEncrypted.ts index e48217d..cb0c421 100644 --- a/src/bpsets/eks/EKSClusterSecretsEncrypted.ts +++ b/src/bpsets/eks/EKSClusterSecretsEncrypted.ts @@ -2,61 +2,124 @@ import { EKSClient, ListClustersCommand, DescribeClusterCommand, - AssociateEncryptionConfigCommand -} from '@aws-sdk/client-eks' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + AssociateEncryptionConfigCommand, +} from '@aws-sdk/client-eks'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EKSClusterSecretsEncrypted implements BPSet { - private readonly client = new EKSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EKSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})) - const clusterNames = clusterNamesResponse.clusters || [] - const clusters = [] + const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); + const clusterNames = clusterNamesResponse.clusters || []; + const clusters = []; for (const clusterName of clusterNames) { const cluster = await this.memoClient.send( new DescribeClusterCommand({ name: clusterName }) - ) - clusters.push(cluster.cluster!) + ); + clusters.push(cluster.cluster!); } - return clusters - } + return clusters; + }; + + public readonly getMetadata = () => ({ + name: 'EKSClusterSecretsEncrypted', + description: 'Ensures that all EKS clusters have secrets encrypted with a KMS key.', + priority: 1, + priorityReason: + 'Encrypting secrets ensures the security and compliance of sensitive data in EKS clusters.', + awsService: 'EKS', + awsServiceCategory: 'Kubernetes Service', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'The KMS key ARN to enable encryption for EKS secrets.', + default: '', + example: 'arn:aws:kms:us-east-1:123456789012:key/example-key-id', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListClustersCommand', + reason: 'Retrieve the list of EKS clusters.', + }, + { + name: 'DescribeClusterCommand', + reason: 'Fetch details about the EKS cluster, including encryption configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'AssociateEncryptionConfigCommand', + reason: 'Enable encryption for EKS secrets using the provided KMS key.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the specified KMS key is accessible to the EKS service and cluster.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { - const encryptionConfig = cluster.encryptionConfig?.[0] + const encryptionConfig = cluster.encryptionConfig?.[0]; if (encryptionConfig?.resources?.includes('secrets')) { - compliantResources.push(cluster.arn!) + compliantResources.push(cluster.arn!); } else { - nonCompliantResources.push(cluster.arn!) + nonCompliantResources.push(cluster.arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'kms-key-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { + const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const arn of nonCompliantResources) { - const clusterName = arn.split(':cluster/')[1] + const clusterName = arn.split(':cluster/')[1]; await this.client.send( new AssociateEncryptionConfigCommand({ @@ -64,11 +127,11 @@ export class EKSClusterSecretsEncrypted implements BPSet { encryptionConfig: [ { resources: ['secrets'], - provider: { keyArn: kmsKeyId } - } - ] + provider: { keyArn: kmsKeyId }, + }, + ], }) - ) + ); } - } + }; } diff --git a/src/bpsets/eks/EKSEndpointNoPublicAccess.ts b/src/bpsets/eks/EKSEndpointNoPublicAccess.ts index 433d730..da9d06f 100644 --- a/src/bpsets/eks/EKSEndpointNoPublicAccess.ts +++ b/src/bpsets/eks/EKSEndpointNoPublicAccess.ts @@ -2,61 +2,118 @@ import { EKSClient, ListClustersCommand, DescribeClusterCommand, - UpdateClusterConfigCommand -} from '@aws-sdk/client-eks' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateClusterConfigCommand, +} from '@aws-sdk/client-eks'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EKSEndpointNoPublicAccess implements BPSet { - private readonly client = new EKSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EKSClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})) - const clusterNames = clusterNamesResponse.clusters || [] - const clusters = [] + const clusterNamesResponse = await this.memoClient.send(new ListClustersCommand({})); + const clusterNames = clusterNamesResponse.clusters || []; + const clusters = []; for (const clusterName of clusterNames) { const cluster = await this.memoClient.send( new DescribeClusterCommand({ name: clusterName }) - ) - clusters.push(cluster.cluster!) + ); + clusters.push(cluster.cluster!); } - return clusters - } + return clusters; + }; + + public readonly getMetadata = () => ({ + name: 'EKSEndpointNoPublicAccess', + description: 'Ensures EKS cluster endpoint does not have public access enabled.', + priority: 1, + priorityReason: 'Disabling public access to the cluster endpoint enhances security by limiting exposure to public networks.', + awsService: 'EKS', + awsServiceCategory: 'Kubernetes Service', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListClustersCommand', + reason: 'Retrieves the list of EKS clusters.', + }, + { + name: 'DescribeClusterCommand', + reason: 'Fetches detailed configuration of each EKS cluster.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateClusterConfigCommand', + reason: 'Updates the EKS cluster configuration to disable public endpoint access.', + }, + ], + adviseBeforeFixFunction: 'Ensure the private endpoint is properly configured and accessible before disabling public access.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { - const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess + const endpointPublicAccess = cluster.resourcesVpcConfig?.endpointPublicAccess; if (endpointPublicAccess) { - nonCompliantResources.push(cluster.arn!) + nonCompliantResources.push(cluster.arn!); } else { - compliantResources.push(cluster.arn!) + compliantResources.push(cluster.arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const clusterName = arn.split(':cluster/')[1] + const clusterName = arn.split(':cluster/')[1]; await this.client.send( new UpdateClusterConfigCommand({ name: clusterName, resourcesVpcConfig: { - endpointPublicAccess: false - } + endpointPublicAccess: false, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/elasticache/ElastiCacheAutoMinorVersionUpgradeCheck.ts b/src/bpsets/elasticache/ElastiCacheAutoMinorVersionUpgradeCheck.ts index 98c10ea..3a92fcd 100644 --- a/src/bpsets/elasticache/ElastiCacheAutoMinorVersionUpgradeCheck.ts +++ b/src/bpsets/elasticache/ElastiCacheAutoMinorVersionUpgradeCheck.ts @@ -1,49 +1,102 @@ import { ElastiCacheClient, DescribeCacheClustersCommand, - ModifyCacheClusterCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyCacheClusterCommand, +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheAutoMinorVersionUpgradeCheck implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const response = await this.memoClient.send(new DescribeCacheClustersCommand({})) - return response.CacheClusters || [] - } + const response = await this.memoClient.send(new DescribeCacheClustersCommand({})); + return response.CacheClusters || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheAutoMinorVersionUpgradeCheck', + description: 'Ensures that ElastiCache clusters have auto minor version upgrade enabled.', + priority: 2, + priorityReason: 'Auto minor version upgrades help ensure clusters stay up-to-date with the latest security and bug fixes.', + awsService: 'ElastiCache', + awsServiceCategory: 'Cache Service', + bestPracticeCategory: 'Reliability', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeCacheClustersCommand', + reason: 'Fetches the list and configurations of ElastiCache clusters.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyCacheClusterCommand', + reason: 'Enables auto minor version upgrade on ElastiCache clusters.', + }, + ], + adviseBeforeFixFunction: 'Ensure application compatibility with updated ElastiCache versions before enabling this setting.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { if (cluster.AutoMinorVersionUpgrade) { - compliantResources.push(cluster.ARN!) + compliantResources.push(cluster.ARN!); } else { - nonCompliantResources.push(cluster.ARN!) + nonCompliantResources.push(cluster.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster:')[1] + const clusterId = arn.split(':cluster:')[1]; await this.client.send( new ModifyCacheClusterCommand({ CacheClusterId: clusterId, - AutoMinorVersionUpgrade: true + AutoMinorVersionUpgrade: true, }) - ) + ); } - } + }; } diff --git a/src/bpsets/elasticache/ElastiCacheRedisClusterAutomaticBackupCheck.ts b/src/bpsets/elasticache/ElastiCacheRedisClusterAutomaticBackupCheck.ts index c05e7c7..149e2d2 100644 --- a/src/bpsets/elasticache/ElastiCacheRedisClusterAutomaticBackupCheck.ts +++ b/src/bpsets/elasticache/ElastiCacheRedisClusterAutomaticBackupCheck.ts @@ -1,60 +1,117 @@ import { ElastiCacheClient, DescribeReplicationGroupsCommand, - ModifyReplicationGroupCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyReplicationGroupCommand, +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheRedisClusterAutomaticBackupCheck implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getReplicationGroups = async () => { - const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})) - return response.ReplicationGroups || [] - } + const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); + return response.ReplicationGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheRedisClusterAutomaticBackupCheck', + description: 'Ensures that Redis clusters in ElastiCache have automatic backups enabled.', + priority: 2, + priorityReason: 'Automatic backups are crucial for disaster recovery and data safety.', + awsService: 'ElastiCache', + awsServiceCategory: 'Cache Service', + bestPracticeCategory: 'Reliability', + requiredParametersForFix: [ + { + name: 'snapshot-retention-period', + description: 'Number of days to retain automatic snapshots.', + default: '7', + example: '7', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeReplicationGroupsCommand', + reason: 'Fetches details of replication groups to verify backup settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyReplicationGroupCommand', + reason: 'Enables automatic snapshots and sets the retention period for Redis clusters.', + }, + ], + adviseBeforeFixFunction: 'Ensure that enabling snapshots does not conflict with operational or compliance requirements.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const replicationGroups = await this.getReplicationGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const replicationGroups = await this.getReplicationGroups(); for (const group of replicationGroups) { if (group.SnapshottingClusterId) { - compliantResources.push(group.ARN!) + compliantResources.push(group.ARN!); } else { - nonCompliantResources.push(group.ARN!) + nonCompliantResources.push(group.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'snapshot-retention-period' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { + public readonly fix: BPSetFixFn = async (nonCompliantResources, requiredParametersForFix) => { const retentionPeriod = requiredParametersForFix.find( - param => param.name === 'snapshot-retention-period' - )?.value + (param) => param.name === 'snapshot-retention-period' + )?.value; if (!retentionPeriod) { - throw new Error("Required parameter 'snapshot-retention-period' is missing.") + throw new Error("Required parameter 'snapshot-retention-period' is missing."); } for (const arn of nonCompliantResources) { - const groupId = arn.split(':replication-group:')[1] + const groupId = arn.split(':replication-group:')[1]; await this.client.send( new ModifyReplicationGroupCommand({ ReplicationGroupId: groupId, - SnapshotRetentionLimit: parseInt(retentionPeriod, 10) + SnapshotRetentionLimit: parseInt(retentionPeriod, 10), }) - ) + ); } - } + }; } diff --git a/src/bpsets/elasticache/ElastiCacheReplGrpAutoFailoverEnabled.ts b/src/bpsets/elasticache/ElastiCacheReplGrpAutoFailoverEnabled.ts index 9158734..c0c121d 100644 --- a/src/bpsets/elasticache/ElastiCacheReplGrpAutoFailoverEnabled.ts +++ b/src/bpsets/elasticache/ElastiCacheReplGrpAutoFailoverEnabled.ts @@ -1,49 +1,102 @@ import { ElastiCacheClient, DescribeReplicationGroupsCommand, - ModifyReplicationGroupCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyReplicationGroupCommand, +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats, BPSetFixFn } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheReplGrpAutoFailoverEnabled implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getReplicationGroups = async () => { - const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})) - return response.ReplicationGroups || [] - } + const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); + return response.ReplicationGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheReplGrpAutoFailoverEnabled', + description: 'Ensures that automatic failover is enabled for ElastiCache replication groups.', + priority: 1, + priorityReason: 'Automatic failover is critical for high availability and reliability of ElastiCache clusters.', + awsService: 'ElastiCache', + awsServiceCategory: 'Cache Service', + bestPracticeCategory: 'Availability', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeReplicationGroupsCommand', + reason: 'Fetches replication group details to verify automatic failover settings.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyReplicationGroupCommand', + reason: 'Enables automatic failover for replication groups.', + }, + ], + adviseBeforeFixFunction: 'Ensure the environment supports multi-AZ configurations before enabling automatic failover.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const replicationGroups = await this.getReplicationGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const replicationGroups = await this.getReplicationGroups(); for (const group of replicationGroups) { if (group.AutomaticFailover === 'enabled') { - compliantResources.push(group.ARN!) + compliantResources.push(group.ARN!); } else { - nonCompliantResources.push(group.ARN!) + nonCompliantResources.push(group.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix: BPSetFixFn = async (nonCompliantResources) => { for (const arn of nonCompliantResources) { - const groupId = arn.split(':replication-group:')[1] + const groupId = arn.split(':replication-group:')[1]; await this.client.send( new ModifyReplicationGroupCommand({ ReplicationGroupId: groupId, - AutomaticFailoverEnabled: true + AutomaticFailoverEnabled: true, }) - ) + ); } - } + }; } diff --git a/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedAtRest.ts b/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedAtRest.ts index fe5b444..ba9ac5d 100644 --- a/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedAtRest.ts +++ b/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedAtRest.ts @@ -1,43 +1,90 @@ import { ElastiCacheClient, DescribeReplicationGroupsCommand, - ModifyReplicationGroupCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheReplGrpEncryptedAtRest implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getReplicationGroups = async () => { - const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})) - return response.ReplicationGroups || [] - } + const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); + return response.ReplicationGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheReplGrpEncryptedAtRest', + description: 'Ensures that ElastiCache replication groups are encrypted at rest.', + priority: 1, + priorityReason: 'Encryption at rest is crucial for protecting data in compliance with security standards.', + awsService: 'ElastiCache', + awsServiceCategory: 'Cache Service', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeReplicationGroupsCommand', + reason: 'Fetches replication group details to verify encryption settings.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: 'Recreation of the replication group is required for encryption. Ensure data backups are available.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const replicationGroups = await this.getReplicationGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const replicationGroups = await this.getReplicationGroups(); for (const group of replicationGroups) { if (group.AtRestEncryptionEnabled) { - compliantResources.push(group.ARN!) + compliantResources.push(group.ARN!); } else { - nonCompliantResources.push(group.ARN!) + nonCompliantResources.push(group.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async () => { throw new Error( 'Fixing encryption at rest for replication groups requires recreation. Please create a new replication group with AtRestEncryptionEnabled set to true.' - ) - } + ); + }; } diff --git a/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedInTransit.ts b/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedInTransit.ts index 572ab37..1df6a75 100644 --- a/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedInTransit.ts +++ b/src/bpsets/elasticache/ElastiCacheReplGrpEncryptedInTransit.ts @@ -1,43 +1,90 @@ import { ElastiCacheClient, DescribeReplicationGroupsCommand, - ModifyReplicationGroupCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheReplGrpEncryptedInTransit implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getReplicationGroups = async () => { - const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})) - return response.ReplicationGroups || [] - } + const response = await this.memoClient.send(new DescribeReplicationGroupsCommand({})); + return response.ReplicationGroups || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheReplGrpEncryptedInTransit', + description: 'Ensures that ElastiCache replication groups have in-transit encryption enabled.', + priority: 1, + priorityReason: 'In-transit encryption is essential for securing data during transmission.', + awsService: 'ElastiCache', + awsServiceCategory: 'Cache Service', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeReplicationGroupsCommand', + reason: 'Fetches replication group details to verify in-transit encryption settings.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: 'Recreation of the replication group is required for enabling in-transit encryption. Ensure data backups are available.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const replicationGroups = await this.getReplicationGroups() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const replicationGroups = await this.getReplicationGroups(); for (const group of replicationGroups) { if (group.TransitEncryptionEnabled) { - compliantResources.push(group.ARN!) + compliantResources.push(group.ARN!); } else { - nonCompliantResources.push(group.ARN!) + nonCompliantResources.push(group.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async () => { throw new Error( 'Fixing in-transit encryption for replication groups requires recreation. Please create a new replication group with TransitEncryptionEnabled set to true.' - ) - } + ); + }; } diff --git a/src/bpsets/elasticache/ElastiCacheSubnetGroupCheck.ts b/src/bpsets/elasticache/ElastiCacheSubnetGroupCheck.ts index d91754a..68b3dbc 100644 --- a/src/bpsets/elasticache/ElastiCacheSubnetGroupCheck.ts +++ b/src/bpsets/elasticache/ElastiCacheSubnetGroupCheck.ts @@ -3,68 +3,132 @@ import { DescribeCacheClustersCommand, DeleteCacheClusterCommand, CreateCacheClusterCommand -} from '@aws-sdk/client-elasticache' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-elasticache'; +import { BPSet, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class ElastiCacheSubnetGroupCheck implements BPSet { - private readonly client = new ElastiCacheClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new ElastiCacheClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getClusters = async () => { - const response = await this.memoClient.send(new DescribeCacheClustersCommand({})) - return response.CacheClusters || [] - } + const response = await this.memoClient.send(new DescribeCacheClustersCommand({})); + return response.CacheClusters || []; + }; + + public readonly getMetadata = () => ({ + name: 'ElastiCacheSubnetGroupCheck', + description: 'Ensures ElastiCache clusters are not using the default subnet group.', + priority: 2, + priorityReason: 'Using the default subnet group is not recommended for production workloads.', + awsService: 'ElastiCache', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Networking', + requiredParametersForFix: [ + { + name: 'subnet-group-name', + description: 'The name of the desired subnet group to associate with the cluster.', + default: '', + example: 'custom-subnet-group', + } + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeCacheClustersCommand', + reason: 'Fetches the details of all ElastiCache clusters to check their subnet group.', + } + ], + commandUsedInFixFunction: [ + { + name: 'DeleteCacheClusterCommand', + reason: 'Deletes non-compliant ElastiCache clusters.', + }, + { + name: 'CreateCacheClusterCommand', + reason: 'Recreates ElastiCache clusters with the desired subnet group.', + } + ], + adviseBeforeFixFunction: 'Ensure data backups are available before fixing as clusters will be deleted and recreated.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const clusters = await this.getClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const clusters = await this.getClusters(); for (const cluster of clusters) { if (cluster.CacheSubnetGroupName !== 'default') { - compliantResources.push(cluster.ARN!) + compliantResources.push(cluster.ARN!); } else { - nonCompliantResources.push(cluster.ARN!) + nonCompliantResources.push(cluster.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'subnet-group-name' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] ) => { const subnetGroupName = requiredParametersForFix.find( - param => param.name === 'subnet-group-name' - )?.value + (param) => param.name === 'subnet-group-name' + )?.value; if (!subnetGroupName) { - throw new Error("Required parameter 'subnet-group-name' is missing.") + throw new Error("Required parameter 'subnet-group-name' is missing."); } for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster:')[1] + const clusterId = arn.split(':cluster:')[1]; const cluster = await this.memoClient.send( new DescribeCacheClustersCommand({ CacheClusterId: clusterId }) - ) - const clusterDetails = cluster.CacheClusters?.[0] + ); + const clusterDetails = cluster.CacheClusters?.[0]; if (!clusterDetails) { - continue + continue; } // Delete the non-compliant cluster await this.client.send( new DeleteCacheClusterCommand({ - CacheClusterId: clusterId + CacheClusterId: clusterId, }) - ) + ); // Recreate the cluster with the desired subnet group await this.client.send( @@ -74,11 +138,13 @@ export class ElastiCacheSubnetGroupCheck implements BPSet { CacheNodeType: clusterDetails.CacheNodeType!, NumCacheNodes: clusterDetails.NumCacheNodes!, CacheSubnetGroupName: subnetGroupName, - SecurityGroupIds: clusterDetails.SecurityGroups?.map(group => group.SecurityGroupId) as string[], + SecurityGroupIds: clusterDetails.SecurityGroups?.map( + (group) => group.SecurityGroupId + ) as string[], PreferredMaintenanceWindow: clusterDetails.PreferredMaintenanceWindow, - EngineVersion: clusterDetails.EngineVersion + EngineVersion: clusterDetails.EngineVersion, }) - ) + ); } - } + }; } diff --git a/src/bpsets/iam/IAMPolicyNoStatementsWithAdminAccess.ts b/src/bpsets/iam/IAMPolicyNoStatementsWithAdminAccess.ts index 97429c3..f2156fc 100644 --- a/src/bpsets/iam/IAMPolicyNoStatementsWithAdminAccess.ts +++ b/src/bpsets/iam/IAMPolicyNoStatementsWithAdminAccess.ts @@ -3,38 +3,98 @@ import { ListPoliciesCommand, GetPolicyVersionCommand, DeletePolicyCommand -} from '@aws-sdk/client-iam' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-iam'; +import { BPSet, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class IAMPolicyNoStatementsWithAdminAccess implements BPSet { - private readonly client = new IAMClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new IAMClient({}); + private readonly memoClient = Memorizer.memo(this.client); private readonly getPolicies = async () => { - const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' })) - return response.Policies || [] - } + const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' })); + return response.Policies || []; + }; private readonly getPolicyDefaultVersions = async (policyArn: string, versionId: string) => { const response = await this.memoClient.send( new GetPolicyVersionCommand({ PolicyArn: policyArn, VersionId: versionId }) - ) - return response.PolicyVersion! - } + ); + return response.PolicyVersion!; + }; + + public readonly getMetadata = () => ({ + name: 'IAMPolicyNoStatementsWithAdminAccess', + description: 'Ensures IAM policies do not contain statements granting full administrative access.', + priority: 1, + priorityReason: 'Granting full administrative access can lead to security vulnerabilities.', + awsService: 'IAM', + awsServiceCategory: 'Security, Identity, & Compliance', + bestPracticeCategory: 'IAM', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'ListPoliciesCommand', + reason: 'Fetches all local IAM policies.', + }, + { + name: 'GetPolicyVersionCommand', + reason: 'Retrieves the default version of each policy.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'DeletePolicyCommand', + reason: 'Deletes non-compliant IAM policies.', + }, + ], + adviseBeforeFixFunction: 'Deleting policies is irreversible. Verify policies before applying fixes.', + }); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const policies = await this.getPolicies() + this.stats.status = 'CHECKING'; + + await this.checkImpl().then( + () => (this.stats.status = 'FINISHED'), + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const policies = await this.getPolicies(); for (const policy of policies) { - const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!) + const policyVersion = await this.getPolicyDefaultVersions(policy.Arn!, policy.DefaultVersionId!); - const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)) // Parse Document JSON string + const policyDocument = JSON.parse(JSON.stringify(policyVersion.Document)); // Parse Document JSON string const statements = Array.isArray(policyDocument.Statement) ? policyDocument.Statement - : [policyDocument.Statement] + : [policyDocument.Statement]; for (const statement of statements) { if ( @@ -42,26 +102,23 @@ export class IAMPolicyNoStatementsWithAdminAccess implements BPSet { statement?.Resource === '*' && statement?.Effect === 'Allow' ) { - nonCompliantResources.push(policy.Arn!) - break + nonCompliantResources.push(policy.Arn!); + break; } } if (!nonCompliantResources.includes(policy.Arn!)) { - compliantResources.push(policy.Arn!) + compliantResources.push(policy.Arn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - await this.client.send(new DeletePolicyCommand({ PolicyArn: arn })) + await this.client.send(new DeletePolicyCommand({ PolicyArn: arn })); } - } + }; } diff --git a/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts b/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts index 1683871..ba537ef 100644 --- a/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts +++ b/src/bpsets/iam/IAMPolicyNoStatementsWithFullAccess.ts @@ -1,23 +1,90 @@ -import { IAMClient, ListPoliciesCommand, GetPolicyVersionCommand } from "@aws-sdk/client-iam"; -import { BPSet } from "../../types"; +import { + IAMClient, + ListPoliciesCommand, + GetPolicyVersionCommand, + CreatePolicyVersionCommand, + DeletePolicyVersionCommand, +} from "@aws-sdk/client-iam"; +import { BPSet, BPSetMetadata, BPSetStats } from "../../types"; import { Memorizer } from "../../Memorizer"; export class IAMPolicyNoStatementsWithFullAccess implements BPSet { private readonly client = new IAMClient({}); private readonly memoClient = Memorizer.memo(this.client); + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: "LOADED", + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: "IAMPolicyNoStatementsWithFullAccess", + description: "Ensures IAM policies do not have statements granting full access.", + priority: 2, + priorityReason: "Granting full access poses a significant security risk.", + awsService: "IAM", + awsServiceCategory: "Access Management", + bestPracticeCategory: "Security", + requiredParametersForFix: [ + { + name: "policy-revision-strategy", + description: "Strategy to revise policies (e.g., remove, restrict actions)", + default: "remove", + example: "remove", + }, + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { name: "ListPoliciesCommand", reason: "Fetch all customer-managed policies." }, + { name: "GetPolicyVersionCommand", reason: "Retrieve the default version of the policy." }, + ], + commandUsedInFixFunction: [ + { name: "CreatePolicyVersionCommand", reason: "Create a new policy version." }, + { name: "DeletePolicyVersionCommand", reason: "Delete outdated policy versions." }, + ], + adviseBeforeFixFunction: + "Ensure revised policies meet the organization's security and access requirements.", + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = "LOADED"; + this.stats.errorMessage = []; + }; + public readonly check = async () => { + this.stats.status = "CHECKING"; + + await this.checkImpl() + .then( + () => { + this.stats.status = "FINISHED"; + }, + (err) => { + this.stats.status = "ERROR"; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { const compliantResources: string[] = []; const nonCompliantResources: string[] = []; - // Fetch all customer-managed IAM policies const policiesResponse = await this.memoClient.send( new ListPoliciesCommand({ Scope: "Local" }) ); const policies = policiesResponse.Policies || []; for (const policy of policies) { - // Get the default version of the policy const policyVersionResponse = await this.memoClient.send( new GetPolicyVersionCommand({ PolicyArn: policy.Arn!, @@ -29,7 +96,6 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet { decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string) ); - // Check statements for full access const hasFullAccess = policyDocument.Statement.some((statement: any) => { if (statement.Effect === "Deny") return false; const actions = Array.isArray(statement.Action) @@ -45,23 +111,77 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [], - }; + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] ) => { - for (const policyArn of nonCompliantResources) { - // Add logic to remove or modify the statements with full access - // Note: Updating an IAM policy requires creating a new version and setting it as default - console.error( - `Fix operation is not implemented for policy ${policyArn}. Manual intervention is required.` + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then( + () => { + this.stats.status = "FINISHED"; + }, + (err) => { + this.stats.status = "ERROR"; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } ); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const strategy = requiredParametersForFix.find( + (param) => param.name === "policy-revision-strategy" + )?.value; + + if (!strategy) { + throw new Error("Required parameter 'policy-revision-strategy' is missing."); + } + + for (const policyArn of nonCompliantResources) { + const policyVersionResponse = await this.memoClient.send( + new GetPolicyVersionCommand({ + PolicyArn: policyArn, + VersionId: "v1", + }) + ); + + const policyDocument = JSON.parse( + decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string) + ); + + policyDocument.Statement = policyDocument.Statement.filter((statement: any) => { + if (statement.Effect === "Deny") return true; + const actions = Array.isArray(statement.Action) + ? statement.Action + : [statement.Action]; + return !actions.some((action: string) => action.endsWith(":*")); + }); + + const createVersionResponse = await this.client.send( + new CreatePolicyVersionCommand({ + PolicyArn: policyArn, + PolicyDocument: JSON.stringify(policyDocument), + SetAsDefault: true, + }) + ); + + if (createVersionResponse.PolicyVersion?.VersionId) { + await this.client.send( + new DeletePolicyVersionCommand({ + PolicyArn: policyArn, + VersionId: policyVersionResponse.PolicyVersion!.VersionId, + }) + ); + } } }; } diff --git a/src/bpsets/iam/IAMRoleManagedPolicyCheck.ts b/src/bpsets/iam/IAMRoleManagedPolicyCheck.ts index ee1943e..1cbeab4 100644 --- a/src/bpsets/iam/IAMRoleManagedPolicyCheck.ts +++ b/src/bpsets/iam/IAMRoleManagedPolicyCheck.ts @@ -1,56 +1,115 @@ import { IAMClient, ListPoliciesCommand, - ListEntitiesForPolicyCommand -} from '@aws-sdk/client-iam' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ListEntitiesForPolicyCommand, +} from '@aws-sdk/client-iam'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class IAMRoleManagedPolicyCheck implements BPSet { - private readonly client = new IAMClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new IAMClient({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'IAMRoleManagedPolicyCheck', + description: 'Checks whether managed IAM policies are attached to any entities (roles, users, or groups).', + priority: 3, + priorityReason: 'Orphaned managed policies may indicate unused resources that can be removed for better security and resource management.', + awsService: 'IAM', + awsServiceCategory: 'Access Management', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListPoliciesCommand', + reason: 'Retrieve all customer-managed IAM policies.', + }, + { + name: 'ListEntitiesForPolicyCommand', + reason: 'Check if policies are attached to any users, roles, or groups.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: 'Ensure orphaned policies are no longer required before removing them.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then( + () => { + this.stats.status = 'FINISHED'; + }, + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const policies = await this.getPolicies(); + + for (const policy of policies) { + const { attached } = await this.checkEntitiesForPolicy(policy.Arn!); + + if (attached) { + compliantResources.push(policy.Arn!); + } else { + nonCompliantResources.push(policy.Arn!); + } + } + + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix = async () => { + throw new Error( + 'Fixing orphaned managed policies requires manual review and removal. Ensure these policies are no longer needed.' + ); + }; private readonly getPolicies = async () => { - const response = await this.memoClient.send(new ListPoliciesCommand({ Scope: 'Local' })) - return response.Policies || [] - } + const response = await this.memoClient.send( + new ListPoliciesCommand({ Scope: 'Local' }) + ); + return response.Policies || []; + }; private readonly checkEntitiesForPolicy = async (policyArn: string) => { const response = await this.memoClient.send( new ListEntitiesForPolicyCommand({ PolicyArn: policyArn }) - ) + ); return { attached: Boolean( - response.PolicyGroups?.length || response.PolicyUsers?.length || response.PolicyRoles?.length - ) - } - } - - public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const policies = await this.getPolicies() - - for (const policy of policies) { - const { attached } = await this.checkEntitiesForPolicy(policy.Arn!) - - if (attached) { - compliantResources.push(policy.Arn!) - } else { - nonCompliantResources.push(policy.Arn!) - } - } - - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } - - public readonly fix = async (nonCompliantResources: string[]) => { - throw new Error( - 'Fixing orphaned managed policies requires manual review and removal. Ensure these policies are no longer needed.' - ) - } + response.PolicyGroups?.length || + response.PolicyUsers?.length || + response.PolicyRoles?.length + ), + }; + }; } diff --git a/src/bpsets/lambda/LambdaDLQCheck.ts b/src/bpsets/lambda/LambdaDLQCheck.ts index a822f2c..7c60ef2 100644 --- a/src/bpsets/lambda/LambdaDLQCheck.ts +++ b/src/bpsets/lambda/LambdaDLQCheck.ts @@ -1,58 +1,136 @@ -import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class LambdaDLQCheck implements BPSet { - private readonly client = new LambdaClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new LambdaClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getFunctions = async () => { - const response = await this.memoClient.send(new ListFunctionsCommand({})) - return response.Functions || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const functions = await this.getFunctions() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'LambdaDLQCheck', + description: 'Ensures that Lambda functions have a configured Dead Letter Queue (DLQ).', + priority: 2, + priorityReason: 'A DLQ is critical for handling failed events in Lambda, enhancing reliability.', + awsService: 'Lambda', + awsServiceCategory: 'Serverless', + bestPracticeCategory: 'Reliability', + requiredParametersForFix: [ + { + name: 'dlq-arn', + description: 'The ARN of the Dead Letter Queue to associate with the Lambda function.', + default: '', + example: 'arn:aws:sqs:us-east-1:123456789012:example-queue', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListFunctionsCommand', + reason: 'Retrieve all Lambda functions in the account.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateFunctionConfigurationCommand', + reason: 'Update the DLQ configuration for Lambda functions.', + }, + ], + adviseBeforeFixFunction: 'Ensure that the specified DLQ exists and is correctly configured to handle failed events.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then( + () => { + this.stats.status = 'FINISHED'; + }, + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const functions = await this.getFunctions(); for (const func of functions) { if (func.DeadLetterConfig) { - compliantResources.push(func.FunctionArn!) + compliantResources.push(func.FunctionArn!); } else { - nonCompliantResources.push(func.FunctionArn!) + nonCompliantResources.push(func.FunctionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'dlq-arn' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - const dlqArn = requiredParametersForFix.find(param => param.name === 'dlq-arn')?.value + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then( + () => { + this.stats.status = 'FINISHED'; + }, + (err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + } + ); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const dlqArn = requiredParametersForFix.find((param) => param.name === 'dlq-arn')?.value; if (!dlqArn) { - throw new Error("Required parameter 'dlq-arn' is missing.") + throw new Error("Required parameter 'dlq-arn' is missing."); } for (const functionArn of nonCompliantResources) { - const functionName = functionArn.split(':').pop()! + const functionName = functionArn.split(':').pop()!; await this.client.send( new UpdateFunctionConfigurationCommand({ FunctionName: functionName, - DeadLetterConfig: { TargetArn: dlqArn } + DeadLetterConfig: { TargetArn: dlqArn }, }) - ) + ); } - } + }; + + private readonly getFunctions = async () => { + const response = await this.memoClient.send(new ListFunctionsCommand({})); + return response.Functions || []; + }; } diff --git a/src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts b/src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts index 1ae0a4d..b4a7696 100644 --- a/src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts +++ b/src/bpsets/lambda/LambdaFunctionPublicAccessProhibited.ts @@ -2,85 +2,153 @@ import { LambdaClient, ListFunctionsCommand, GetPolicyCommand, - RemovePermissionCommand -} from '@aws-sdk/client-lambda' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RemovePermissionCommand, +} from '@aws-sdk/client-lambda'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class LambdaFunctionPublicAccessProhibited implements BPSet { - private readonly client = new LambdaClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new LambdaClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getFunctions = async () => { - const response = await this.memoClient.send(new ListFunctionsCommand({})) - return response.Functions || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const functions = await this.getFunctions() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'LambdaFunctionPublicAccessProhibited', + description: 'Ensures that Lambda functions do not allow public access via their resource-based policies.', + priority: 1, + priorityReason: 'Publicly accessible Lambda functions pose significant security risks.', + awsService: 'Lambda', + awsServiceCategory: 'Serverless', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'ListFunctionsCommand', + reason: 'Retrieve all Lambda functions in the account.', + }, + { + name: 'GetPolicyCommand', + reason: 'Fetch the resource-based policy of a Lambda function.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RemovePermissionCommand', + reason: 'Remove public access permissions from a Lambda function.', + }, + ], + adviseBeforeFixFunction: 'Ensure that removing permissions does not disrupt legitimate use of the Lambda function.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const functions = await this.getFunctions(); for (const func of functions) { try { - const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! })) - const policy = JSON.parse(response.Policy!) + const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! })); + const policy = JSON.parse(response.Policy!); const hasPublicAccess = policy.Statement.some( (statement: any) => statement.Principal === '*' || statement.Principal?.AWS === '*' - ) + ); if (hasPublicAccess) { - nonCompliantResources.push(func.FunctionArn!) + nonCompliantResources.push(func.FunctionArn!); } else { - compliantResources.push(func.FunctionArn!) + compliantResources.push(func.FunctionArn!); } } catch (error) { if ((error as any).name === 'ResourceNotFoundException') { - nonCompliantResources.push(func.FunctionArn!) + compliantResources.push(func.FunctionArn!); } else { - throw error + throw error; } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const functionArn of nonCompliantResources) { - const functionName = functionArn.split(':').pop()! + const functionName = functionArn.split(':').pop()!; try { - const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName })) - const policy = JSON.parse(response.Policy!) + const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: functionName })); + const policy = JSON.parse(response.Policy!); for (const statement of policy.Statement) { if (statement.Principal === '*' || statement.Principal?.AWS === '*') { await this.client.send( new RemovePermissionCommand({ FunctionName: functionName, - StatementId: statement.Sid // Use the actual StatementId from the policy + StatementId: statement.Sid, // Use the actual StatementId from the policy }) - ) + ); } } } catch (error) { if ((error as any).name !== 'ResourceNotFoundException') { - throw error + throw error; } } } - } + }; + + private readonly getFunctions = async () => { + const response = await this.memoClient.send(new ListFunctionsCommand({})); + return response.Functions || []; + }; } diff --git a/src/bpsets/lambda/LambdaFunctionSettingsCheck.ts b/src/bpsets/lambda/LambdaFunctionSettingsCheck.ts index faad49e..6c2533a 100644 --- a/src/bpsets/lambda/LambdaFunctionSettingsCheck.ts +++ b/src/bpsets/lambda/LambdaFunctionSettingsCheck.ts @@ -1,65 +1,147 @@ -import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +import { + LambdaClient, + ListFunctionsCommand, + UpdateFunctionConfigurationCommand +} from '@aws-sdk/client-lambda'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class LambdaFunctionSettingsCheck implements BPSet { - private readonly client = new LambdaClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new LambdaClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getFunctions = async () => { - const response = await this.memoClient.send(new ListFunctionsCommand({})) - return response.Functions || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const defaultTimeout = 3 - const defaultMemorySize = 128 - const functions = await this.getFunctions() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'LambdaFunctionSettingsCheck', + description: 'Ensures Lambda functions have non-default timeout and memory size configurations.', + priority: 2, + priorityReason: 'Default configurations may not be suitable for production workloads.', + awsService: 'Lambda', + awsServiceCategory: 'Serverless', + bestPracticeCategory: 'Configuration', + requiredParametersForFix: [ + { + name: 'timeout', + description: 'Timeout value in seconds for the Lambda function.', + default: '3', + example: '30', + }, + { + name: 'memory-size', + description: 'Memory size in MB for the Lambda function.', + default: '128', + example: '256', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListFunctionsCommand', + reason: 'Retrieve all Lambda functions in the account.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateFunctionConfigurationCommand', + reason: 'Update the timeout and memory size settings for non-compliant Lambda functions.', + }, + ], + adviseBeforeFixFunction: 'Ensure that the timeout and memory size changes are suitable for the function\'s requirements.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const defaultTimeout = 3; + const defaultMemorySize = 128; + + const functions = await this.getFunctions(); for (const func of functions) { if (func.Timeout === defaultTimeout || func.MemorySize === defaultMemorySize) { - nonCompliantResources.push(func.FunctionArn!) + nonCompliantResources.push(func.FunctionArn!); } else { - compliantResources.push(func.FunctionArn!) + compliantResources.push(func.FunctionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'timeout' }, - { name: 'memory-size' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value - const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const timeout = requiredParametersForFix.find(param => param.name === 'timeout')?.value; + const memorySize = requiredParametersForFix.find(param => param.name === 'memory-size')?.value; if (!timeout || !memorySize) { - throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing.") + throw new Error("Required parameters 'timeout' and/or 'memory-size' are missing."); } for (const functionArn of nonCompliantResources) { - const functionName = functionArn.split(':').pop()! + const functionName = functionArn.split(':').pop()!; await this.client.send( new UpdateFunctionConfigurationCommand({ FunctionName: functionName, Timeout: parseInt(timeout, 10), - MemorySize: parseInt(memorySize, 10) + MemorySize: parseInt(memorySize, 10), }) - ) + ); } - } + }; + + private readonly getFunctions = async () => { + const response = await this.memoClient.send(new ListFunctionsCommand({})); + return response.Functions || []; + }; } diff --git a/src/bpsets/lambda/LambdaInsideVPC.ts b/src/bpsets/lambda/LambdaInsideVPC.ts index fa139fe..3c7f800 100644 --- a/src/bpsets/lambda/LambdaInsideVPC.ts +++ b/src/bpsets/lambda/LambdaInsideVPC.ts @@ -2,59 +2,131 @@ import { LambdaClient, ListFunctionsCommand, UpdateFunctionConfigurationCommand -} from '@aws-sdk/client-lambda' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-lambda'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class LambdaInsideVPC implements BPSet { - private readonly client = new LambdaClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new LambdaClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getFunctions = async () => { - const response = await this.memoClient.send(new ListFunctionsCommand({})) - return response.Functions || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const functions = await this.getFunctions() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'LambdaInsideVPC', + description: 'Ensures Lambda functions are configured to run inside a VPC.', + priority: 2, + priorityReason: 'Running Lambda inside a VPC enhances security by restricting access.', + awsService: 'Lambda', + awsServiceCategory: 'Serverless', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'subnet-ids', + description: 'Comma-separated list of VPC subnet IDs.', + default: '', + example: 'subnet-abc123,subnet-def456', + }, + { + name: 'security-group-ids', + description: 'Comma-separated list of VPC security group IDs.', + default: '', + example: 'sg-abc123,sg-def456', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListFunctionsCommand', + reason: 'Retrieve all Lambda functions in the account.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateFunctionConfigurationCommand', + reason: 'Update the VPC configuration for non-compliant Lambda functions.', + }, + ], + adviseBeforeFixFunction: 'Ensure the provided subnet and security group IDs are correct and appropriate for the Lambda function\'s requirements.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const functions = await this.getFunctions(); for (const func of functions) { if (func.VpcConfig && Object.keys(func.VpcConfig).length > 0) { - compliantResources.push(func.FunctionArn!) + compliantResources.push(func.FunctionArn!); } else { - nonCompliantResources.push(func.FunctionArn!) + nonCompliantResources.push(func.FunctionArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'subnet-ids' }, - { name: 'security-group-ids' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value - const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const subnetIds = requiredParametersForFix.find(param => param.name === 'subnet-ids')?.value; + const securityGroupIds = requiredParametersForFix.find(param => param.name === 'security-group-ids')?.value; if (!subnetIds || !securityGroupIds) { - throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing.") + throw new Error("Required parameters 'subnet-ids' and/or 'security-group-ids' are missing."); } for (const functionArn of nonCompliantResources) { - const functionName = functionArn.split(':').pop()! + const functionName = functionArn.split(':').pop()!; await this.client.send( new UpdateFunctionConfigurationCommand({ FunctionName: functionName, @@ -63,7 +135,12 @@ export class LambdaInsideVPC implements BPSet { SecurityGroupIds: securityGroupIds.split(',') } }) - ) + ); } - } + }; + + private readonly getFunctions = async () => { + const response = await this.memoClient.send(new ListFunctionsCommand({})); + return response.Functions || []; + }; } diff --git a/src/bpsets/rds/AuroraLastBackupRecoveryPointCreated.ts b/src/bpsets/rds/AuroraLastBackupRecoveryPointCreated.ts index dc58965..a9eecd4 100644 --- a/src/bpsets/rds/AuroraLastBackupRecoveryPointCreated.ts +++ b/src/bpsets/rds/AuroraLastBackupRecoveryPointCreated.ts @@ -2,79 +2,161 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' +} from '@aws-sdk/client-rds'; import { BackupClient, ListRecoveryPointsByResourceCommand -} from '@aws-sdk/client-backup' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-backup'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class AuroraLastBackupRecoveryPointCreated implements BPSet { - private readonly rdsClient = new RDSClient({}) - private readonly backupClient = new BackupClient({}) - private readonly memoRdsClient = Memorizer.memo(this.rdsClient) + private readonly rdsClient = new RDSClient({}); + private readonly backupClient = new BackupClient({}); + private readonly memoRdsClient = Memorizer.memo(this.rdsClient); - private readonly getDBClusters = async () => { - const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - private readonly getRecoveryPoints = async (resourceArn: string) => { - const response = await this.backupClient.send( - new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn }) - ) - return response.RecoveryPoints || [] - } + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'AuroraLastBackupRecoveryPointCreated', + description: 'Ensures that Aurora DB clusters have a recovery point created within the last 24 hours.', + priority: 1, + priorityReason: 'Ensuring regular backups protects against data loss.', + awsService: 'RDS', + awsServiceCategory: 'Aurora', + bestPracticeCategory: 'Backup', + requiredParametersForFix: [ + { + name: 'backup-retention-period', + description: 'The number of days to retain backups for the DB cluster.', + default: '7', + example: '7', + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Retrieve information about Aurora DB clusters.', + }, + { + name: 'ListRecoveryPointsByResourceCommand', + reason: 'Check the recovery points associated with the DB cluster.', + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Update the backup retention period for the DB cluster.', + } + ], + adviseBeforeFixFunction: 'Ensure that extending the backup retention period aligns with your data protection policies.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { - const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!) - const recoveryDates = recoveryPoints.map(rp => new Date(rp.CreationDate!)) - recoveryDates.sort((a, b) => b.getTime() - a.getTime()) + const recoveryPoints = await this.getRecoveryPoints(cluster.DBClusterArn!); + const recoveryDates = recoveryPoints.map(rp => new Date(rp.CreationDate!)); + recoveryDates.sort((a, b) => b.getTime() - a.getTime()); if ( recoveryDates.length > 0 && new Date().getTime() - recoveryDates[0].getTime() < 24 * 60 * 60 * 1000 ) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'backup-retention-period', value: '7' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const retentionPeriod = requiredParametersForFix.find( param => param.name === 'backup-retention-period' - )?.value + )?.value; if (!retentionPeriod) { - throw new Error("Required parameter 'backup-retention-period' is missing.") + throw new Error("Required parameter 'backup-retention-period' is missing."); } for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.rdsClient.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, BackupRetentionPeriod: parseInt(retentionPeriod, 10) }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; + + private readonly getRecoveryPoints = async (resourceArn: string) => { + const response = await this.backupClient.send( + new ListRecoveryPointsByResourceCommand({ ResourceArn: resourceArn }) + ); + return response.RecoveryPoints || []; + }; } diff --git a/src/bpsets/rds/AuroraMySQLBacktrackingEnabled.ts b/src/bpsets/rds/AuroraMySQLBacktrackingEnabled.ts index c0f147a..48975bd 100644 --- a/src/bpsets/rds/AuroraMySQLBacktrackingEnabled.ts +++ b/src/bpsets/rds/AuroraMySQLBacktrackingEnabled.ts @@ -2,51 +2,122 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class AuroraMySQLBacktrackingEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'AuroraMySQLBacktrackingEnabled', + description: 'Ensures that backtracking is enabled for Aurora MySQL clusters.', + priority: 1, + priorityReason: 'Enabling backtracking provides point-in-time recovery for Aurora MySQL databases.', + awsService: 'RDS', + awsServiceCategory: 'Aurora', + bestPracticeCategory: 'Data Protection', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Fetch Aurora MySQL DB clusters and check backtracking configuration.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Enable backtracking for non-compliant Aurora MySQL DB clusters.', + }, + ], + adviseBeforeFixFunction: 'Ensure that enabling backtracking aligns with your application requirements.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if ( cluster.Engine === 'aurora-mysql' && (!cluster.EarliestBacktrackTime || cluster.EarliestBacktrackTime === null) ) { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } else { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, - BacktrackWindow: 3600 // Set backtracking window to 1 hour + BacktrackWindow: 3600, // Set backtracking window to 1 hour }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/DBInstanceBackupEnabled.ts b/src/bpsets/rds/DBInstanceBackupEnabled.ts index 0b22c33..85c4dec 100644 --- a/src/bpsets/rds/DBInstanceBackupEnabled.ts +++ b/src/bpsets/rds/DBInstanceBackupEnabled.ts @@ -2,59 +2,137 @@ import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class DBInstanceBackupEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBInstances = async () => { - const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) - return response.DBInstances || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'DBInstanceBackupEnabled', + description: 'Ensures that backups are enabled for RDS instances.', + priority: 1, + priorityReason: 'Enabling backups is critical for data recovery and compliance.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Data Protection', + requiredParametersForFix: [ + { + name: 'retention-period', + description: 'The number of days to retain backups.', + default: '7', + example: '7' + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBInstancesCommand', + reason: 'Fetch information about RDS instances to check backup retention.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBInstanceCommand', + reason: 'Enable backup retention for non-compliant RDS instances.' + } + ], + adviseBeforeFixFunction: 'Ensure the retention period aligns with your organization’s backup policy.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbInstances = await this.getDBInstances() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbInstances = await this.getDBInstances(); for (const instance of dbInstances) { if (instance.BackupRetentionPeriod && instance.BackupRetentionPeriod > 0) { - compliantResources.push(instance.DBInstanceArn!) + compliantResources.push(instance.DBInstanceArn!); } else { - nonCompliantResources.push(instance.DBInstanceArn!) + nonCompliantResources.push(instance.DBInstanceArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'retention-period', value: '7' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const retentionPeriod = requiredParametersForFix.find( - param => param.name === 'retention-period' - )?.value + (param) => param.name === 'retention-period' + )?.value; if (!retentionPeriod) { - throw new Error("Required parameter 'retention-period' is missing.") + throw new Error("Required parameter 'retention-period' is missing."); } for (const arn of nonCompliantResources) { - const instanceId = arn.split(':instance/')[1] + const instanceId = arn.split(':instance/')[1]; await this.client.send( new ModifyDBInstanceCommand({ DBInstanceIdentifier: instanceId, BackupRetentionPeriod: parseInt(retentionPeriod, 10) }) - ) + ); } - } + }; + + private readonly getDBInstances = async () => { + const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); + return response.DBInstances || []; + }; } diff --git a/src/bpsets/rds/RDSClusterAutoMinorVersionUpgradeEnabled.ts b/src/bpsets/rds/RDSClusterAutoMinorVersionUpgradeEnabled.ts index 643193f..6504df2 100644 --- a/src/bpsets/rds/RDSClusterAutoMinorVersionUpgradeEnabled.ts +++ b/src/bpsets/rds/RDSClusterAutoMinorVersionUpgradeEnabled.ts @@ -2,49 +2,120 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterAutoMinorVersionUpgradeEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterAutoMinorVersionUpgradeEnabled', + description: 'Ensures Auto Minor Version Upgrade is enabled for RDS clusters.', + priority: 1, + priorityReason: 'Auto minor version upgrades help keep the database engine updated with minimal effort, ensuring security and performance improvements.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Configuration Management', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Fetch information about RDS clusters to check auto minor version upgrade setting.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Enable auto minor version upgrade for non-compliant RDS clusters.' + } + ], + adviseBeforeFixFunction: 'Ensure that enabling auto minor version upgrades aligns with your organization’s change management policies.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if (cluster.Engine === 'docdb' || cluster.AutoMinorVersionUpgrade) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, AutoMinorVersionUpgrade: true }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSClusterDefaultAdminCheck.ts b/src/bpsets/rds/RDSClusterDefaultAdminCheck.ts index 60a1de9..7825166 100644 --- a/src/bpsets/rds/RDSClusterDefaultAdminCheck.ts +++ b/src/bpsets/rds/RDSClusterDefaultAdminCheck.ts @@ -2,66 +2,147 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterDefaultAdminCheck implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterDefaultAdminCheck', + description: 'Ensures that RDS clusters do not use default administrative usernames (e.g., admin, postgres).', + priority: 2, + priorityReason: 'Using default administrative usernames increases the risk of brute force attacks.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'new-master-username', + description: 'The new master username for the RDS cluster.', + default: '', + example: 'secureAdminUser' + }, + { + name: 'new-master-password', + description: 'The new master password for the RDS cluster.', + default: '', + example: 'SecureP@ssword123' + } + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Fetches information about RDS clusters, including their master username.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Updates the master user password for non-compliant RDS clusters.' + } + ], + adviseBeforeFixFunction: 'Ensure that the new master username and password comply with your security policies.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if (!['admin', 'postgres'].includes(cluster.MasterUsername!)) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'new-master-username', value: '' }, - { name: 'new-master-password', value: '' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const newMasterUsername = requiredParametersForFix.find( - param => param.name === 'new-master-username' - )?.value + (param) => param.name === 'new-master-username' + )?.value; const newMasterPassword = requiredParametersForFix.find( - param => param.name === 'new-master-password' - )?.value + (param) => param.name === 'new-master-password' + )?.value; if (!newMasterUsername || !newMasterPassword) { - throw new Error("Required parameters 'new-master-username' and 'new-master-password' are missing.") + throw new Error("Required parameters 'new-master-username' and 'new-master-password' are missing."); } for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, MasterUserPassword: newMasterPassword }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSClusterDeletionProtectionEnabled.ts b/src/bpsets/rds/RDSClusterDeletionProtectionEnabled.ts index 6cbe99d..3f4a7aa 100644 --- a/src/bpsets/rds/RDSClusterDeletionProtectionEnabled.ts +++ b/src/bpsets/rds/RDSClusterDeletionProtectionEnabled.ts @@ -2,49 +2,120 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterDeletionProtectionEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterDeletionProtectionEnabled', + description: 'Ensures that RDS clusters have deletion protection enabled.', + priority: 2, + priorityReason: 'Deletion protection helps to prevent accidental deletion of critical database clusters.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Resilience', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'To fetch details about RDS clusters, including their deletion protection status.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'To enable deletion protection on non-compliant RDS clusters.' + } + ], + adviseBeforeFixFunction: 'Ensure that enabling deletion protection aligns with your operational policies.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if (cluster.DeletionProtection) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, DeletionProtection: true }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSClusterEncryptedAtRest.ts b/src/bpsets/rds/RDSClusterEncryptedAtRest.ts index 6494a1b..40ffdd3 100644 --- a/src/bpsets/rds/RDSClusterEncryptedAtRest.ts +++ b/src/bpsets/rds/RDSClusterEncryptedAtRest.ts @@ -1,43 +1,99 @@ import { RDSClient, - DescribeDBClustersCommand, - ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeDBClustersCommand +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterEncryptedAtRest implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterEncryptedAtRest', + description: 'Ensures that RDS clusters have encryption at rest enabled.', + priority: 1, + priorityReason: 'Encryption at rest is critical for data security and compliance.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'To check the encryption status of RDS clusters.' + } + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: 'Manually recreate the RDS cluster with encryption at rest enabled, as fixing this requires destructive operations.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if (cluster.StorageEncrypted) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: 'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.' + }); throw new Error( 'Fixing encryption at rest requires recreating the cluster. Please manually recreate the cluster with encryption enabled.' - ) - } + ); + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSClusterIAMAuthenticationEnabled.ts b/src/bpsets/rds/RDSClusterIAMAuthenticationEnabled.ts index 555607a..6500920 100644 --- a/src/bpsets/rds/RDSClusterIAMAuthenticationEnabled.ts +++ b/src/bpsets/rds/RDSClusterIAMAuthenticationEnabled.ts @@ -2,52 +2,128 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterIAMAuthenticationEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterIAMAuthenticationEnabled', + description: 'Ensures that IAM Database Authentication is enabled for RDS clusters.', + priority: 1, + priorityReason: 'IAM Authentication enhances security by allowing fine-grained access control.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'To fetch the list of RDS clusters and their IAM authentication status.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'To enable IAM Database Authentication for non-compliant clusters.' + } + ], + adviseBeforeFixFunction: 'Ensure that enabling IAM Database Authentication aligns with your security and application needs.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if ( cluster.Engine === 'docdb' || cluster.IAMDatabaseAuthenticationEnabled ) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] + const clusterId = arn.split(':cluster/')[1]; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, EnableIAMDatabaseAuthentication: true }) - ) + ); } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSClusterMultiAZEnabled.ts b/src/bpsets/rds/RDSClusterMultiAZEnabled.ts index 0c79b11..6eeb26d 100644 --- a/src/bpsets/rds/RDSClusterMultiAZEnabled.ts +++ b/src/bpsets/rds/RDSClusterMultiAZEnabled.ts @@ -1,43 +1,121 @@ import { RDSClient, - DescribeDBClustersCommand, - ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DescribeDBClustersCommand +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSClusterMultiAZEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSClusterMultiAZEnabled', + description: 'Ensures that RDS clusters are deployed across multiple availability zones.', + priority: 1, + priorityReason: 'Multi-AZ deployment improves availability and resilience of RDS clusters.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Availability', + requiredParametersForFix: [ + { + name: 'additional-azs', + description: 'Number of additional availability zones to add for Multi-AZ configuration.', + default: '2', + example: '2' + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'To fetch the list of RDS clusters and their availability zone configuration.' + } + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: + 'Enabling Multi-AZ for an existing cluster may require significant reconfiguration and potential downtime. Proceed with caution.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { if ((cluster.AvailabilityZones || []).length > 1) { - compliantResources.push(cluster.DBClusterArn!) + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'additional-azs', value: '2' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { throw new Error( 'Enabling Multi-AZ requires cluster reconfiguration. This must be performed manually.' - ) - } + ); + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSDBSecurityGroupNotAllowed.ts b/src/bpsets/rds/RDSDBSecurityGroupNotAllowed.ts index 5d07774..c677524 100644 --- a/src/bpsets/rds/RDSDBSecurityGroupNotAllowed.ts +++ b/src/bpsets/rds/RDSDBSecurityGroupNotAllowed.ts @@ -2,63 +2,140 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { EC2Client, DescribeSecurityGroupsCommand } from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSDBSecurityGroupNotAllowed implements BPSet { - private readonly rdsClient = new RDSClient({}) - private readonly ec2Client = new EC2Client({}) - private readonly memoRdsClient = Memorizer.memo(this.rdsClient) - private readonly memoEc2Client = Memorizer.memo(this.ec2Client) + private readonly rdsClient = new RDSClient({}); + private readonly ec2Client = new EC2Client({}); + private readonly memoRdsClient = Memorizer.memo(this.rdsClient); + private readonly memoEc2Client = Memorizer.memo(this.ec2Client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSDBSecurityGroupNotAllowed', + description: 'Ensures RDS clusters are not associated with the default security group.', + priority: 2, + priorityReason: 'Default security groups may allow unrestricted access, posing a security risk.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Fetch RDS cluster details including associated security groups.' + }, + { + name: 'DescribeSecurityGroupsCommand', + reason: 'Fetch details of default security groups.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Remove default security groups from the RDS cluster configuration.' + } + ], + adviseBeforeFixFunction: + 'Ensure valid non-default security groups are associated with the clusters before applying the fix.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbClusters = await this.getDBClusters(); + const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map(sg => sg.GroupId!); + + for (const cluster of dbClusters) { + const activeSecurityGroups = cluster.VpcSecurityGroups?.filter(sg => sg.Status === 'active') || []; + + if (activeSecurityGroups.some(sg => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) { + nonCompliantResources.push(cluster.DBClusterArn!); + } else { + compliantResources.push(cluster.DBClusterArn!); + } + } + + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { + for (const arn of nonCompliantResources) { + const clusterId = arn.split(':cluster/')[1]; + + await this.rdsClient.send( + new ModifyDBClusterCommand({ + DBClusterIdentifier: clusterId, + VpcSecurityGroupIds: [] // Ensure valid non-default security groups are used here + }) + ); + } + }; private readonly getDBClusters = async () => { - const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + const response = await this.memoRdsClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; private readonly getDefaultSecurityGroups = async () => { const response = await this.memoEc2Client.send( new DescribeSecurityGroupsCommand({ Filters: [{ Name: 'group-name', Values: ['default'] }] }) - ) - return response.SecurityGroups || [] - } - - public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbClusters = await this.getDBClusters() - const defaultSecurityGroupIds = (await this.getDefaultSecurityGroups()).map(sg => sg.GroupId!) - - for (const cluster of dbClusters) { - const activeSecurityGroups = cluster.VpcSecurityGroups?.filter(sg => sg.Status === 'active') || [] - - if (activeSecurityGroups.some(sg => defaultSecurityGroupIds.includes(sg.VpcSecurityGroupId!))) { - nonCompliantResources.push(cluster.DBClusterArn!) - } else { - compliantResources.push(cluster.DBClusterArn!) - } - } - - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } - - public readonly fix = async (nonCompliantResources: string[]) => { - for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] - - // Remove default security groups by modifying the cluster's security group configuration - await this.rdsClient.send( - new ModifyDBClusterCommand({ - DBClusterIdentifier: clusterId, - VpcSecurityGroupIds: [] // Update to valid non-default security groups - }) - ) - } - } + ); + return response.SecurityGroups || []; + }; } diff --git a/src/bpsets/rds/RDSEnhancedMonitoringEnabled.ts b/src/bpsets/rds/RDSEnhancedMonitoringEnabled.ts index 82dace9..6a45744 100644 --- a/src/bpsets/rds/RDSEnhancedMonitoringEnabled.ts +++ b/src/bpsets/rds/RDSEnhancedMonitoringEnabled.ts @@ -2,60 +2,140 @@ import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSEnhancedMonitoringEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBInstances = async () => { - const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) - return response.DBInstances || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSEnhancedMonitoringEnabled', + description: 'Ensures that Enhanced Monitoring is enabled for RDS instances.', + priority: 2, + priorityReason: 'Enhanced Monitoring provides valuable metrics for better monitoring and troubleshooting.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Monitoring', + requiredParametersForFix: [ + { + name: 'monitoring-interval', + description: 'The interval in seconds for Enhanced Monitoring.', + default: '60', + example: '60' + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBInstancesCommand', + reason: 'Fetch RDS instance details including monitoring configuration.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBInstanceCommand', + reason: 'Enable Enhanced Monitoring for non-compliant RDS instances.' + } + ], + adviseBeforeFixFunction: 'Ensure that enabling Enhanced Monitoring does not conflict with existing configurations.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbInstances = await this.getDBInstances() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbInstances = await this.getDBInstances(); for (const instance of dbInstances) { if (instance.MonitoringInterval && instance.MonitoringInterval > 0) { - compliantResources.push(instance.DBInstanceArn!) + compliantResources.push(instance.DBInstanceArn!); } else { - nonCompliantResources.push(instance.DBInstanceArn!) + nonCompliantResources.push(instance.DBInstanceArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'monitoring-interval', value: '60' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const monitoringInterval = requiredParametersForFix.find( - param => param.name === 'monitoring-interval' - )?.value + (param) => param.name === 'monitoring-interval' + )?.value; if (!monitoringInterval) { - throw new Error("Required parameter 'monitoring-interval' is missing.") + throw new Error("Required parameter 'monitoring-interval' is missing."); } for (const arn of nonCompliantResources) { - const instanceId = arn.split(':instance/')[1] + const instanceId = arn.split(':instance/')[1]; await this.client.send( new ModifyDBInstanceCommand({ DBInstanceIdentifier: instanceId, MonitoringInterval: parseInt(monitoringInterval, 10) }) - ) + ); } - } + }; + + private readonly getDBInstances = async () => { + const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); + return response.DBInstances || []; + }; } diff --git a/src/bpsets/rds/RDSInstancePublicAccessCheck.ts b/src/bpsets/rds/RDSInstancePublicAccessCheck.ts index cdc6b7d..6ffe26e 100644 --- a/src/bpsets/rds/RDSInstancePublicAccessCheck.ts +++ b/src/bpsets/rds/RDSInstancePublicAccessCheck.ts @@ -2,49 +2,125 @@ import { RDSClient, DescribeDBInstancesCommand, ModifyDBInstanceCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSInstancePublicAccessCheck implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBInstances = async () => { - const response = await this.memoClient.send(new DescribeDBInstancesCommand({})) - return response.DBInstances || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSInstancePublicAccessCheck', + description: 'Ensures RDS instances are not publicly accessible.', + priority: 1, + priorityReason: 'Publicly accessible RDS instances expose databases to potential security risks.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBInstancesCommand', + reason: 'Fetches the list of RDS instances and their public access settings.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBInstanceCommand', + reason: 'Disables public access for non-compliant RDS instances.' + } + ], + adviseBeforeFixFunction: 'Ensure there are valid private network configurations in place before disabling public access.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const dbInstances = await this.getDBInstances() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const dbInstances = await this.getDBInstances(); for (const instance of dbInstances) { if (instance.PubliclyAccessible) { - nonCompliantResources.push(instance.DBInstanceArn!) + nonCompliantResources.push(instance.DBInstanceArn!); } else { - compliantResources.push(instance.DBInstanceArn!) + compliantResources.push(instance.DBInstanceArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const arn of nonCompliantResources) { - const instanceId = arn.split(':instance/')[1] + const instanceId = arn.split(':instance/')[1]; await this.client.send( new ModifyDBInstanceCommand({ DBInstanceIdentifier: instanceId, PubliclyAccessible: false }) - ) + ); } - } + }; + + private readonly getDBInstances = async () => { + const response = await this.memoClient.send(new DescribeDBInstancesCommand({})); + return response.DBInstances || []; + }; } diff --git a/src/bpsets/rds/RDSLoggingEnabled.ts b/src/bpsets/rds/RDSLoggingEnabled.ts index 91af5e1..ee8de69 100644 --- a/src/bpsets/rds/RDSLoggingEnabled.ts +++ b/src/bpsets/rds/RDSLoggingEnabled.ts @@ -2,69 +2,147 @@ import { RDSClient, DescribeDBClustersCommand, ModifyDBClusterCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSLoggingEnabled implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusters = async () => { - const response = await this.memoClient.send(new DescribeDBClustersCommand({})) - return response.DBClusters || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSLoggingEnabled', + description: 'Ensures that logging is enabled for RDS clusters.', + priority: 1, + priorityReason: 'Enabling logs ensures visibility into database activities for security and troubleshooting.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Monitoring', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClustersCommand', + reason: 'Fetches the list of RDS clusters and their logging configurations.' + } + ], + commandUsedInFixFunction: [ + { + name: 'ModifyDBClusterCommand', + reason: 'Enables CloudWatch logging for non-compliant RDS clusters.' + } + ], + adviseBeforeFixFunction: 'Ensure that enabling logs does not negatively impact application performance.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const logsForEngine = { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const logsForEngine: Record = { 'aurora-mysql': ['audit', 'error', 'general', 'slowquery'], 'aurora-postgresql': ['postgresql'], 'docdb': ['audit', 'profiler'] - } - const dbClusters = await this.getDBClusters() + }; + + const dbClusters = await this.getDBClusters(); for (const cluster of dbClusters) { - if ( - JSON.stringify(cluster.EnabledCloudwatchLogsExports || []) === - JSON.stringify((logsForEngine as any)[cluster.Engine!] || []) - ) { - compliantResources.push(cluster.DBClusterArn!) + const requiredLogs = logsForEngine[cluster.Engine!] || []; + const enabledLogs = cluster.EnabledCloudwatchLogsExports || []; + + if (JSON.stringify(enabledLogs) === JSON.stringify(requiredLogs)) { + compliantResources.push(cluster.DBClusterArn!); } else { - nonCompliantResources.push(cluster.DBClusterArn!) + nonCompliantResources.push(cluster.DBClusterArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const logsForEngine: Record = { + 'aurora-mysql': ['audit', 'error', 'general', 'slowquery'], + 'aurora-postgresql': ['postgresql'], + 'docdb': ['audit', 'profiler'] + }; + + const dbClusters = await this.getDBClusters(); - public readonly fix = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const clusterId = arn.split(':cluster/')[1] - const logsForEngine = { - 'aurora-mysql': ['audit', 'error', 'general', 'slowquery'], - 'aurora-postgresql': ['postgresql'], - 'docdb': ['audit', 'profiler'] - } - - const dbClusters = await this.getDBClusters() - const cluster = dbClusters.find(c => c.DBClusterArn === arn) + const clusterId = arn.split(':cluster/')[1]; + const cluster = dbClusters.find((c) => c.DBClusterArn === arn); if (cluster) { - const logsToEnable = (logsForEngine as any)[cluster.Engine!] + const logsToEnable = logsForEngine[cluster.Engine!] || []; await this.client.send( new ModifyDBClusterCommand({ DBClusterIdentifier: clusterId, CloudwatchLogsExportConfiguration: { EnableLogTypes: logsToEnable } }) - ) + ); } } - } + }; + + private readonly getDBClusters = async () => { + const response = await this.memoClient.send(new DescribeDBClustersCommand({})); + return response.DBClusters || []; + }; } diff --git a/src/bpsets/rds/RDSSnapshotEncrypted.ts b/src/bpsets/rds/RDSSnapshotEncrypted.ts index a2b9eec..1213e29 100644 --- a/src/bpsets/rds/RDSSnapshotEncrypted.ts +++ b/src/bpsets/rds/RDSSnapshotEncrypted.ts @@ -2,53 +2,128 @@ import { RDSClient, DescribeDBClusterSnapshotsCommand, CopyDBClusterSnapshotCommand -} from '@aws-sdk/client-rds' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-rds'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RDSSnapshotEncrypted implements BPSet { - private readonly client = new RDSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new RDSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getDBClusterSnapshots = async () => { - const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({})) - return response.DBClusterSnapshots || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RDSSnapshotEncrypted', + description: 'Ensures RDS cluster snapshots are encrypted.', + priority: 1, + priorityReason: 'Encryption ensures data security and compliance with regulations.', + awsService: 'RDS', + awsServiceCategory: 'Database', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'KMS key ID to encrypt the snapshot.', + default: '', + example: 'arn:aws:kms:region:account-id:key/key-id' + } + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeDBClusterSnapshotsCommand', + reason: 'Fetches RDS cluster snapshots and their encryption status.' + } + ], + commandUsedInFixFunction: [ + { + name: 'CopyDBClusterSnapshotCommand', + reason: 'Copies the snapshot with encryption enabled using the provided KMS key.' + } + ], + adviseBeforeFixFunction: 'Ensure that the KMS key is properly configured and accessible.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources = [] - const nonCompliantResources = [] - const snapshots = await this.getDBClusterSnapshots() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const snapshots = await this.getDBClusterSnapshots(); for (const snapshot of snapshots) { if (snapshot.StorageEncrypted) { - compliantResources.push(snapshot.DBClusterSnapshotArn!) + compliantResources.push(snapshot.DBClusterSnapshotArn!); } else { - nonCompliantResources.push(snapshot.DBClusterSnapshotArn!) + nonCompliantResources.push(snapshot.DBClusterSnapshotArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'kms-key-id', value: '' } // Replace with your KMS key ID - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] ) => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const kmsKeyId = requiredParametersForFix.find( + (param) => param.name === 'kms-key-id' + )?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const arn of nonCompliantResources) { - const snapshotId = arn.split(':snapshot:')[1] + const snapshotId = arn.split(':snapshot:')[1]; await this.client.send( new CopyDBClusterSnapshotCommand({ @@ -56,7 +131,12 @@ export class RDSSnapshotEncrypted implements BPSet { TargetDBClusterSnapshotIdentifier: `${snapshotId}-encrypted`, KmsKeyId: kmsKeyId }) - ) + ); } - } + }; + + private readonly getDBClusterSnapshots = async () => { + const response = await this.memoClient.send(new DescribeDBClusterSnapshotsCommand({})); + return response.DBClusterSnapshots || []; + }; } diff --git a/src/bpsets/s3/S3AccessPointInVpcOnly.ts b/src/bpsets/s3/S3AccessPointInVpcOnly.ts index c1c579e..979960a 100644 --- a/src/bpsets/s3/S3AccessPointInVpcOnly.ts +++ b/src/bpsets/s3/S3AccessPointInVpcOnly.ts @@ -3,68 +3,145 @@ import { ListAccessPointsCommand, DeleteAccessPointCommand, CreateAccessPointCommand -} from '@aws-sdk/client-s3-control' -import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-s3-control'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3AccessPointInVpcOnly implements BPSet { - private readonly client = new S3ControlClient({}) - private readonly memoClient = Memorizer.memo(this.client) - private readonly stsClient = Memorizer.memo(new STSClient({})) + private readonly client = new S3ControlClient({}); + private readonly memoClient = Memorizer.memo(this.client); + private readonly stsClient = Memorizer.memo(new STSClient({})); - private readonly getAccountId = async (): Promise => { - const response = await this.stsClient.send(new GetCallerIdentityCommand({})) - return response.Account! - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const requiredParametersForFix = [{ name: 'your-vpc-id' }] + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3AccessPointInVpcOnly', + description: 'Ensures that all S3 access points are restricted to a VPC.', + priority: 1, + priorityReason: 'Restricting access points to a VPC ensures enhanced security and control.', + awsService: 'S3', + awsServiceCategory: 'Access Points', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'your-vpc-id', + description: 'The VPC ID to associate with the access points.', + default: '', + example: 'vpc-1234567890abcdef' + } + ], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'ListAccessPointsCommand', + reason: 'Lists all S3 access points in the account.' + } + ], + commandUsedInFixFunction: [ + { + name: 'DeleteAccessPointCommand', + reason: 'Deletes access points that are not restricted to a VPC.' + }, + { + name: 'CreateAccessPointCommand', + reason: 'Recreates access points with a VPC configuration.' + } + ], + adviseBeforeFixFunction: 'Ensure that the specified VPC ID is correct and accessible.' + }); - const accountId = await this.getAccountId() + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const accountId = await this.getAccountId(); const response = await this.memoClient.send( new ListAccessPointsCommand({ AccountId: accountId }) - ) + ); for (const accessPoint of response.AccessPointList || []) { if (accessPoint.NetworkOrigin === 'VPC') { - compliantResources.push(accessPoint.AccessPointArn!) + compliantResources.push(accessPoint.AccessPointArn!); } else { - nonCompliantResources.push(accessPoint.AccessPointArn!) + nonCompliantResources.push(accessPoint.AccessPointArn!); } } - return { compliantResources, nonCompliantResources, requiredParametersForFix } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - const accountId = await this.getAccountId() - const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const accountId = await this.getAccountId(); + const vpcId = requiredParametersForFix.find(param => param.name === 'your-vpc-id')?.value; if (!vpcId) { - throw new Error("Required parameter 'your-vpc-id' is missing.") + throw new Error("Required parameter 'your-vpc-id' is missing."); } for (const accessPointArn of nonCompliantResources) { - const accessPointName = accessPointArn.split(':').pop()! - const bucketName = accessPointArn.split('/')[1]! + const accessPointName = accessPointArn.split(':').pop()!; + const bucketName = accessPointArn.split('/')[1]!; await this.client.send( new DeleteAccessPointCommand({ AccountId: accountId, Name: accessPointName }) - ) + ); await this.client.send( new CreateAccessPointCommand({ @@ -75,7 +152,12 @@ export class S3AccessPointInVpcOnly implements BPSet { VpcId: vpcId } }) - ) + ); } - } + }; + + private readonly getAccountId = async (): Promise => { + const response = await this.stsClient.send(new GetCallerIdentityCommand({})); + return response.Account!; + }; } diff --git a/src/bpsets/s3/S3BucketDefaultLockEnabled.ts b/src/bpsets/s3/S3BucketDefaultLockEnabled.ts index 6210251..004e8d9 100644 --- a/src/bpsets/s3/S3BucketDefaultLockEnabled.ts +++ b/src/bpsets/s3/S3BucketDefaultLockEnabled.ts @@ -3,56 +3,120 @@ import { ListBucketsCommand, GetObjectLockConfigurationCommand, PutObjectLockConfigurationCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3BucketDefaultLockEnabled implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3BucketDefaultLockEnabled', + description: 'Ensures that all S3 buckets have default object lock configuration enabled.', + priority: 2, + priorityReason: 'Object lock configuration ensures immutability of bucket objects, protecting them from unintended deletions or modifications.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Data Protection', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetObjectLockConfigurationCommand', + reason: 'Checks if the object lock configuration is enabled for the bucket.' + } + ], + commandUsedInFixFunction: [ + { + name: 'PutObjectLockConfigurationCommand', + reason: 'Enables object lock configuration with a default retention rule.' + } + ], + adviseBeforeFixFunction: 'Ensure that the S3 bucket has object lock enabled before running the fix function, as this operation cannot be undone.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { try { await this.memoClient.send( new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! }) - ) - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + ); + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } catch (error) { if ((error as any).name === 'ObjectLockConfigurationNotFoundError') { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - throw error + throw error; } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutObjectLockConfigurationCommand({ Bucket: bucketName, @@ -66,7 +130,12 @@ export class S3BucketDefaultLockEnabled implements BPSet { } } }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts b/src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts index e62c76e..3870e04 100644 --- a/src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts +++ b/src/bpsets/s3/S3BucketLevelPublicAccessProhibited.ts @@ -3,62 +3,126 @@ import { ListBucketsCommand, GetPublicAccessBlockCommand, PutPublicAccessBlockCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3BucketLevelPublicAccessProhibited implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [] + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3BucketLevelPublicAccessProhibited', + description: 'Ensures that S3 buckets have public access blocked at the bucket level.', + priority: 1, + priorityReason: 'Blocking public access at the bucket level ensures security and prevents unauthorized access.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetPublicAccessBlockCommand', + reason: 'Retrieves the public access block configuration for the bucket.' + } + ], + commandUsedInFixFunction: [ + { + name: 'PutPublicAccessBlockCommand', + reason: 'Enforces public access block configuration at the bucket level.' + } + ], + adviseBeforeFixFunction: 'Ensure no legitimate use cases require public access before applying this fix.' + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { try { const response = await this.memoClient.send( new GetPublicAccessBlockCommand({ Bucket: bucket.Name! }) - ) - const config = response.PublicAccessBlockConfiguration + ); + const config = response.PublicAccessBlockConfiguration; if ( config?.BlockPublicAcls && config?.IgnorePublicAcls && config?.BlockPublicPolicy && config?.RestrictPublicBuckets ) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } - } catch (error) { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + } catch { + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutPublicAccessBlockCommand({ Bucket: bucketName, @@ -69,7 +133,12 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet { RestrictPublicBuckets: true } }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3BucketLoggingEnabled.ts b/src/bpsets/s3/S3BucketLoggingEnabled.ts index be60647..f1ac397 100644 --- a/src/bpsets/s3/S3BucketLoggingEnabled.ts +++ b/src/bpsets/s3/S3BucketLoggingEnabled.ts @@ -2,72 +2,154 @@ import { S3Client, ListBucketsCommand, GetBucketLoggingCommand, - PutBucketLoggingCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketLoggingCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3BucketLoggingEnabled implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3BucketLoggingEnabled', + description: 'Ensures that S3 buckets have logging enabled.', + priority: 2, + priorityReason: + 'Enabling logging on S3 buckets provides audit and security capabilities.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Logging', + requiredParametersForFix: [ + { + name: 'log-destination-bucket', + description: 'The bucket where access logs should be stored.', + default: '', + example: 'my-log-bucket', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketLoggingCommand', + reason: 'Retrieves the logging configuration for the bucket.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketLoggingCommand', + reason: 'Enables logging on the bucket.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the destination bucket for logs exists and has proper permissions for logging.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { - const response = await this.memoClient.send( - new GetBucketLoggingCommand({ Bucket: bucket.Name! }) - ) - if (response.LoggingEnabled) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) - } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + try { + const response = await this.memoClient.send( + new GetBucketLoggingCommand({ Bucket: bucket.Name! }) + ); + if (response.LoggingEnabled) { + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); + } else { + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); + } + } catch (error) { + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-destination-bucket' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { const logDestinationBucket = requiredParametersForFix.find( - param => param.name === 'log-destination-bucket' - )?.value + (param) => param.name === 'log-destination-bucket' + )?.value; if (!logDestinationBucket) { - throw new Error("Required parameter 'log-destination-bucket' is missing.") + throw new Error("Required parameter 'log-destination-bucket' is missing."); } for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutBucketLoggingCommand({ Bucket: bucketName, BucketLoggingStatus: { LoggingEnabled: { TargetBucket: logDestinationBucket, - TargetPrefix: `${bucketName}/logs/` - } - } + TargetPrefix: `${bucketName}/logs/`, + }, + }, }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3BucketSSLRequestsOnly.ts b/src/bpsets/s3/S3BucketSSLRequestsOnly.ts index 46099f7..de9a09b 100644 --- a/src/bpsets/s3/S3BucketSSLRequestsOnly.ts +++ b/src/bpsets/s3/S3BucketSSLRequestsOnly.ts @@ -2,19 +2,176 @@ import { S3Client, ListBucketsCommand, GetBucketPolicyCommand, - PutBucketPolicyCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketPolicyCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3BucketSSLRequestsOnly implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3BucketSSLRequestsOnly', + description: 'Ensures that all S3 bucket requests are made using SSL.', + priority: 2, + priorityReason: 'SSL ensures secure data transmission to and from S3 buckets.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketPolicyCommand', + reason: 'Retrieves the bucket policy to check for SSL conditions.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketPolicyCommand', + reason: 'Updates the bucket policy to enforce SSL requests.', + }, + ], + adviseBeforeFixFunction: + 'Ensure existing bucket policies will not conflict with the SSL-only policy.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); + + for (const bucket of buckets) { + try { + const response = await this.memoClient.send( + new GetBucketPolicyCommand({ Bucket: bucket.Name! }) + ); + const policy = JSON.parse(response.Policy!); + const hasSSLCondition = policy.Statement.some( + (stmt: any) => + stmt.Condition && + stmt.Condition.Bool && + stmt.Condition.Bool['aws:SecureTransport'] === 'false' + ); + + if (hasSSLCondition) { + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); + } else { + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); + } + } catch (error) { + if ((error as any).name === 'NoSuchBucketPolicy') { + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); + } else { + throw error; + } + } + } + + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + for (const bucketArn of nonCompliantResources) { + const bucketName = bucketArn.split(':::')[1]!; + let existingPolicy: any; + + try { + const response = await this.memoClient.send( + new GetBucketPolicyCommand({ Bucket: bucketName }) + ); + existingPolicy = JSON.parse(response.Policy!); + } catch (error) { + if ((error as any).name !== 'NoSuchBucketPolicy') { + throw error; + } + } + + const sslPolicyStatement = { + Sid: 'DenyNonSSLRequests', + Effect: 'Deny', + Principal: '*', + Action: 's3:*', + Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`], + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + }; + + let updatedPolicy; + if (existingPolicy) { + existingPolicy.Statement.push(sslPolicyStatement); + updatedPolicy = JSON.stringify(existingPolicy); + } else { + updatedPolicy = this.createSSLOnlyPolicy(bucketName); + } + + await this.client.send( + new PutBucketPolicyCommand({ + Bucket: bucketName, + Policy: updatedPolicy, + }) + ); + } + }; private readonly createSSLOnlyPolicy = (bucketName: string): string => { return JSON.stringify({ @@ -28,103 +185,16 @@ export class S3BucketSSLRequestsOnly implements BPSet { Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`], Condition: { Bool: { - 'aws:SecureTransport': 'false' - } - } - } - ] - }) - } + 'aws:SecureTransport': 'false', + }, + }, + }, + ], + }); + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() - - for (const bucket of buckets) { - try { - const response = await this.memoClient.send( - new GetBucketPolicyCommand({ Bucket: bucket.Name! }) - ) - const policy = JSON.parse(response.Policy!) - const hasSSLCondition = policy.Statement.some( - (stmt: any) => - stmt.Condition && - stmt.Condition.Bool && - stmt.Condition.Bool['aws:SecureTransport'] === 'false' - ) - - if (hasSSLCondition) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) - } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) - } - } catch (error) { - if ((error as any).name === 'NoSuchBucketPolicy') { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) - } else { - throw error - } - } - } - - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } - - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! - let existingPolicy: any - - try { - const response = await this.memoClient.send( - new GetBucketPolicyCommand({ Bucket: bucketName }) - ) - existingPolicy = JSON.parse(response.Policy!) - } catch (error) { - if ((error as any).name !== 'NoSuchBucketPolicy') { - throw error - } - } - - const sslPolicyStatement = { - Sid: 'DenyNonSSLRequests', - Effect: 'Deny', - Principal: '*', - Action: 's3:*', - Resource: [`arn:aws:s3:::${bucketName}/*`, `arn:aws:s3:::${bucketName}`], - Condition: { - Bool: { - 'aws:SecureTransport': 'false' - } - } - } - - let updatedPolicy - if (existingPolicy) { - existingPolicy.Statement.push(sslPolicyStatement) - updatedPolicy = JSON.stringify(existingPolicy) - } else { - updatedPolicy = this.createSSLOnlyPolicy(bucketName) - } - - await this.client.send( - new PutBucketPolicyCommand({ - Bucket: bucketName, - Policy: updatedPolicy - }) - ) - } - } + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3BucketVersioningEnabled.ts b/src/bpsets/s3/S3BucketVersioningEnabled.ts index 2514d8a..6c4fb87 100644 --- a/src/bpsets/s3/S3BucketVersioningEnabled.ts +++ b/src/bpsets/s3/S3BucketVersioningEnabled.ts @@ -2,61 +2,130 @@ import { S3Client, ListBucketsCommand, GetBucketVersioningCommand, - PutBucketVersioningCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketVersioningCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3BucketVersioningEnabled implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3BucketVersioningEnabled', + description: 'Ensures that versioning is enabled on all S3 buckets.', + priority: 1, + priorityReason: 'Enabling versioning protects against accidental data loss and allows recovery of previous versions.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Data Protection', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketVersioningCommand', + reason: 'Retrieve the current versioning status of the bucket.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketVersioningCommand', + reason: 'Enable versioning on the bucket.', + }, + ], + adviseBeforeFixFunction: 'Ensure that enabling versioning aligns with your data lifecycle and cost considerations.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { const response = await this.memoClient.send( new GetBucketVersioningCommand({ Bucket: bucket.Name! }) - ) + ); if (response.Status === 'Enabled') { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutBucketVersioningCommand({ Bucket: bucketName, VersioningConfiguration: { - Status: 'Enabled' - } + Status: 'Enabled', + }, }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3DefaultEncryptionKMS.ts b/src/bpsets/s3/S3DefaultEncryptionKMS.ts index d90d46f..2f11ef1 100644 --- a/src/bpsets/s3/S3DefaultEncryptionKMS.ts +++ b/src/bpsets/s3/S3DefaultEncryptionKMS.ts @@ -2,74 +2,146 @@ import { S3Client, ListBucketsCommand, GetBucketEncryptionCommand, - PutBucketEncryptionCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketEncryptionCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3DefaultEncryptionKMS implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3DefaultEncryptionKMS', + description: 'Ensures that all S3 buckets have default encryption enabled using AWS KMS.', + priority: 1, + priorityReason: 'Default encryption protects sensitive data stored in S3 buckets.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Data Protection', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'The KMS Key ID used for bucket encryption.', + default: '', + example: 'arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ab-cdef-EXAMPLE12345', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketEncryptionCommand', + reason: 'Retrieve the encryption configuration for a bucket.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketEncryptionCommand', + reason: 'Enable KMS encryption for the bucket.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the KMS key is properly configured with necessary permissions for S3 operations.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { try { const response = await this.memoClient.send( new GetBucketEncryptionCommand({ Bucket: bucket.Name! }) - ) - const encryption = response.ServerSideEncryptionConfiguration! + ); + const encryption = response.ServerSideEncryptionConfiguration!; const isKmsEnabled = encryption.Rules?.some( - rule => + (rule) => rule.ApplyServerSideEncryptionByDefault && rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm === 'aws:kms' - ) + ); if (isKmsEnabled) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } catch (error) { if ((error as any).name === 'ServerSideEncryptionConfigurationNotFoundError') { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - throw error + throw error; } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'kms-key-id' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutBucketEncryptionCommand({ Bucket: bucketName, @@ -78,13 +150,18 @@ export class S3DefaultEncryptionKMS implements BPSet { { ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'aws:kms', - KMSMasterKeyID: kmsKeyId - } - } - ] - } + KMSMasterKeyID: kmsKeyId, + }, + }, + ], + }, }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3EventNotificationsEnabled.ts b/src/bpsets/s3/S3EventNotificationsEnabled.ts index 7c925ae..b062915 100644 --- a/src/bpsets/s3/S3EventNotificationsEnabled.ts +++ b/src/bpsets/s3/S3EventNotificationsEnabled.ts @@ -2,73 +2,147 @@ import { S3Client, ListBucketsCommand, GetBucketNotificationConfigurationCommand, - PutBucketNotificationConfigurationCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketNotificationConfigurationCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3EventNotificationsEnabled implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3EventNotificationsEnabled', + description: 'Ensures that S3 buckets have event notifications configured.', + priority: 2, + priorityReason: 'Event notifications facilitate automated responses to S3 events, enhancing automation and security.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Monitoring & Automation', + requiredParametersForFix: [ + { + name: 'lambda-function-arn', + description: 'ARN of the Lambda function to invoke for bucket events.', + default: '', + example: 'arn:aws:lambda:us-east-1:123456789012:function:example-function', + }, + { + name: 'event-type', + description: 'S3 event type to trigger the notification.', + default: 's3:ObjectCreated:*', + example: 's3:ObjectCreated:Put', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketNotificationConfigurationCommand', + reason: 'Retrieve the current notification configuration for a bucket.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketNotificationConfigurationCommand', + reason: 'Add or update event notifications for the bucket.', + }, + ], + adviseBeforeFixFunction: 'Ensure the Lambda function has necessary permissions to handle the S3 events.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { const response = await this.memoClient.send( new GetBucketNotificationConfigurationCommand({ Bucket: bucket.Name! }) - ) + ); if ( response.LambdaFunctionConfigurations || response.QueueConfigurations || response.TopicConfigurations ) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'lambda-function-arn' }, - { name: 'event-type' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { const lambdaArn = requiredParametersForFix.find( - param => param.name === 'lambda-function-arn' - )?.value + (param) => param.name === 'lambda-function-arn' + )?.value; const eventType = requiredParametersForFix.find( - param => param.name === 'event-type' - )?.value + (param) => param.name === 'event-type' + )?.value; if (!lambdaArn || !eventType) { throw new Error( "Required parameters 'lambda-function-arn' and/or 'event-type' are missing." - ) + ); } for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutBucketNotificationConfigurationCommand({ Bucket: bucketName, @@ -76,12 +150,17 @@ export class S3EventNotificationsEnabled implements BPSet { LambdaFunctionConfigurations: [ { LambdaFunctionArn: lambdaArn, - Events: [eventType as any] - } - ] - } + Events: [eventType as any], + }, + ], + }, }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts b/src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts index fe9dc6f..18fae75 100644 --- a/src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts +++ b/src/bpsets/s3/S3LastBackupRecoveryPointCreated.ts @@ -1,52 +1,107 @@ import { S3Client, - ListBucketsCommand -} from '@aws-sdk/client-s3' -import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ListBucketsCommand, +} from '@aws-sdk/client-s3'; +import { BackupClient, ListRecoveryPointsByResourceCommand } from '@aws-sdk/client-backup'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3LastBackupRecoveryPointCreated implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) - private readonly backupClient = Memorizer.memo(new BackupClient({})) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); + private readonly backupClient = Memorizer.memo(new BackupClient({})); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3LastBackupRecoveryPointCreated', + description: 'Ensures that S3 buckets have recent backup recovery points.', + priority: 2, + priorityReason: 'Backup recovery points are critical for disaster recovery and data resilience.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Backup & Recovery', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListRecoveryPointsByResourceCommand', + reason: 'Checks for recent recovery points for the S3 bucket.', + }, + ], + commandUsedInFixFunction: [], + adviseBeforeFixFunction: 'Ensure the backup plan for S3 buckets is appropriately configured before proceeding.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { const recoveryPoints = await this.backupClient.send( new ListRecoveryPointsByResourceCommand({ - ResourceArn: `arn:aws:s3:::${bucket.Name!}` + ResourceArn: `arn:aws:s3:::${bucket.Name!}`, }) - ) + ); - if (recoveryPoints.RecoveryPoints && recoveryPoints.RecoveryPoints.length > 0) { - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + if ( + recoveryPoints.RecoveryPoints && + recoveryPoints.RecoveryPoints.length > 0 + ) { + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (): Promise => { - throw new Error('Fixing recovery points requires custom implementation for backup setup.') - } + public readonly fix = async () => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: 'Fixing recovery points requires custom implementation for backup setup.', + }); + throw new Error( + 'Fixing recovery points requires custom implementation for backup setup.' + ); + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/s3/S3LifecyclePolicyCheck.ts b/src/bpsets/s3/S3LifecyclePolicyCheck.ts index 4903aca..5291dde 100644 --- a/src/bpsets/s3/S3LifecyclePolicyCheck.ts +++ b/src/bpsets/s3/S3LifecyclePolicyCheck.ts @@ -2,73 +2,149 @@ import { S3Client, ListBucketsCommand, GetBucketLifecycleConfigurationCommand, - PutBucketLifecycleConfigurationCommand -} from '@aws-sdk/client-s3' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + PutBucketLifecycleConfigurationCommand, +} from '@aws-sdk/client-s3'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class S3LifecyclePolicyCheck implements BPSet { - private readonly client = new S3Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new S3Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getBuckets = async () => { - const response = await this.memoClient.send(new ListBucketsCommand({})) - return response.Buckets || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - public readonly check = async (): Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { name: string }[] - }> => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const buckets = await this.getBuckets() + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'S3LifecyclePolicyCheck', + description: 'Ensures that all S3 buckets have lifecycle policies configured.', + priority: 2, + priorityReason: + 'Lifecycle policies help manage storage costs by automatically transitioning or expiring objects.', + awsService: 'S3', + awsServiceCategory: 'Buckets', + bestPracticeCategory: 'Cost Management', + requiredParametersForFix: [ + { + name: 'lifecycle-policy-rule-id', + description: 'The ID of the lifecycle policy rule.', + default: '', + example: 'expire-old-objects', + }, + { + name: 'expiration-days', + description: 'Number of days after which objects should expire.', + default: '30', + example: '30', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetBucketLifecycleConfigurationCommand', + reason: 'To determine if the bucket has a lifecycle policy configured.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutBucketLifecycleConfigurationCommand', + reason: 'To configure a lifecycle policy for the bucket.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the lifecycle policy settings align with organizational storage management policies.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; + + public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const buckets = await this.getBuckets(); for (const bucket of buckets) { try { await this.memoClient.send( new GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name! }) - ) - compliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + ); + compliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } catch (error) { if ((error as any).name === 'NoSuchLifecycleConfiguration') { - nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`) + nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`); } else { - throw error + throw error; } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'lifecycle-policy-rule-id' }, - { name: 'expiration-days' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] - ): Promise => { + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { const ruleId = requiredParametersForFix.find( - param => param.name === 'lifecycle-policy-rule-id' - )?.value + (param) => param.name === 'lifecycle-policy-rule-id' + )?.value; const expirationDays = requiredParametersForFix.find( - param => param.name === 'expiration-days' - )?.value + (param) => param.name === 'expiration-days' + )?.value; if (!ruleId || !expirationDays) { throw new Error( "Required parameters 'lifecycle-policy-rule-id' and/or 'expiration-days' are missing." - ) + ); } for (const bucketArn of nonCompliantResources) { - const bucketName = bucketArn.split(':::')[1]! + const bucketName = bucketArn.split(':::')[1]!; await this.client.send( new PutBucketLifecycleConfigurationCommand({ Bucket: bucketName, @@ -78,13 +154,18 @@ export class S3LifecyclePolicyCheck implements BPSet { ID: ruleId, Status: 'Enabled', Expiration: { - Days: parseInt(expirationDays, 10) - } - } - ] - } + Days: parseInt(expirationDays, 10), + }, + }, + ], + }, }) - ) + ); } - } + }; + + private readonly getBuckets = async () => { + const response = await this.memoClient.send(new ListBucketsCommand({})); + return response.Buckets || []; + }; } diff --git a/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts b/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts index 8393d0b..f30c5de 100644 --- a/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts +++ b/src/bpsets/secretsmanager/SecretsManagerRotationEnabledCheck.ts @@ -2,47 +2,124 @@ import { SecretsManagerClient, ListSecretsCommand, RotateSecretCommand, - UpdateSecretCommand -} from '@aws-sdk/client-secrets-manager' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + UpdateSecretCommand, +} from '@aws-sdk/client-secrets-manager'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SecretsManagerRotationEnabledCheck implements BPSet { - private readonly client = new SecretsManagerClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new SecretsManagerClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecrets = async () => { - const response = await this.memoClient.send(new ListSecretsCommand({})) - return response.SecretList || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SecretsManagerRotationEnabledCheck', + description: 'Ensures all Secrets Manager secrets have rotation enabled.', + priority: 2, + priorityReason: 'Enabling rotation helps keep secrets secure by periodically rotating them.', + awsService: 'Secrets Manager', + awsServiceCategory: 'Secrets', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListSecretsCommand', + reason: 'To list all secrets managed by Secrets Manager.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RotateSecretCommand', + reason: 'To enable rotation for secrets without it enabled.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the secrets have a rotation lambda or custom rotation strategy configured before enabling rotation.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const secrets = await this.getSecrets() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const secrets = await this.getSecrets(); for (const secret of secrets) { if (secret.RotationEnabled) { - compliantResources.push(secret.ARN!) + compliantResources.push(secret.ARN!); } else { - nonCompliantResources.push(secret.ARN!) + nonCompliantResources.push(secret.ARN!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const arn of nonCompliantResources) { await this.client.send( new RotateSecretCommand({ - SecretId: arn + SecretId: arn, }) - ) + ); } - } + }; + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})); + return response.SecretList || []; + }; } diff --git a/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts b/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts index 6663418..003e461 100644 --- a/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts +++ b/src/bpsets/secretsmanager/SecretsManagerScheduledRotationSuccessCheck.ts @@ -1,55 +1,140 @@ import { SecretsManagerClient, ListSecretsCommand, - RotateSecretCommand -} from '@aws-sdk/client-secrets-manager' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RotateSecretCommand, +} from '@aws-sdk/client-secrets-manager'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SecretsManagerScheduledRotationSuccessCheck implements BPSet { - private readonly client = new SecretsManagerClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new SecretsManagerClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecrets = async () => { - const response = await this.memoClient.send(new ListSecretsCommand({})) - return response.SecretList || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SecretsManagerScheduledRotationSuccessCheck', + description: 'Checks if Secrets Manager secrets have successfully rotated within their scheduled period.', + priority: 2, + priorityReason: + 'Ensuring secrets are rotated as per schedule is critical to maintaining security.', + awsService: 'Secrets Manager', + awsServiceCategory: 'Secrets', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListSecretsCommand', + reason: 'Lists all secrets managed by Secrets Manager to determine rotation status.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RotateSecretCommand', + reason: 'Manually rotates secrets that have not been rotated successfully within their scheduled period.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the secrets have a valid rotation lambda or custom rotation strategy configured before triggering a manual rotation.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const secrets = await this.getSecrets() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const secrets = await this.getSecrets(); for (const secret of secrets) { if (secret.RotationEnabled) { - const now = new Date() - const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined + const now = new Date(); + const lastRotated = secret.LastRotatedDate + ? new Date(secret.LastRotatedDate) + : undefined; const rotationPeriod = secret.RotationRules?.AutomaticallyAfterDays ? secret.RotationRules.AutomaticallyAfterDays + 2 - : undefined + : undefined; - if (!lastRotated || !rotationPeriod || now.getTime() - lastRotated.getTime() > rotationPeriod * 24 * 60 * 60 * 1000) { - nonCompliantResources.push(secret.ARN!) + if ( + !lastRotated || + !rotationPeriod || + now.getTime() - lastRotated.getTime() > + rotationPeriod * 24 * 60 * 60 * 1000 + ) { + nonCompliantResources.push(secret.ARN!); } else { - compliantResources.push(secret.ARN!) + compliantResources.push(secret.ARN!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { for (const arn of nonCompliantResources) { await this.client.send( new RotateSecretCommand({ - SecretId: arn + SecretId: arn, }) - ) + ); } - } + }; + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})); + return response.SecretList || []; + }; } diff --git a/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts b/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts index 3d8f14b..3ceadd6 100644 --- a/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts +++ b/src/bpsets/secretsmanager/SecretsManagerSecretPeriodicRotation.ts @@ -1,52 +1,130 @@ import { SecretsManagerClient, ListSecretsCommand, - RotateSecretCommand -} from '@aws-sdk/client-secrets-manager' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RotateSecretCommand, +} from '@aws-sdk/client-secrets-manager'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SecretsManagerSecretPeriodicRotation implements BPSet { - private readonly client = new SecretsManagerClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new SecretsManagerClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecrets = async () => { - const response = await this.memoClient.send(new ListSecretsCommand({})) - return response.SecretList || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SecretsManagerSecretPeriodicRotation', + description: + 'Ensures that Secrets Manager secrets are rotated periodically (every 90 days).', + priority: 2, + priorityReason: + 'Periodic rotation of secrets ensures compliance with security best practices.', + awsService: 'Secrets Manager', + awsServiceCategory: 'Secrets', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListSecretsCommand', + reason: 'Lists all Secrets Manager secrets to check their rotation status.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RotateSecretCommand', + reason: 'Manually rotates the secrets that are non-compliant.', + }, + ], + adviseBeforeFixFunction: + 'Ensure rotation configurations (e.g., rotation Lambda) are in place before triggering manual rotation.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const secrets = await this.getSecrets() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const secrets = await this.getSecrets(); for (const secret of secrets) { if (secret.RotationEnabled) { - const now = new Date() - const lastRotated = secret.LastRotatedDate ? new Date(secret.LastRotatedDate) : undefined + const now = new Date(); + const lastRotated = secret.LastRotatedDate + ? new Date(secret.LastRotatedDate) + : undefined; if (!lastRotated || now.getTime() - lastRotated.getTime() > 90 * 24 * 60 * 60 * 1000) { - nonCompliantResources.push(secret.ARN!) + nonCompliantResources.push(secret.ARN!); } else { - compliantResources.push(secret.ARN!) + compliantResources.push(secret.ARN!); } } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { await this.client.send( new RotateSecretCommand({ - SecretId: arn + SecretId: arn, }) - ) + ); } - } + }; + + private readonly getSecrets = async () => { + const response = await this.memoClient.send(new ListSecretsCommand({})); + return response.SecretList || []; + }; } diff --git a/src/bpsets/securityhub/SecurityHubEnabled.ts b/src/bpsets/securityhub/SecurityHubEnabled.ts index 1f0d035..28e9348 100644 --- a/src/bpsets/securityhub/SecurityHubEnabled.ts +++ b/src/bpsets/securityhub/SecurityHubEnabled.ts @@ -1,51 +1,125 @@ import { SecurityHubClient, DescribeHubCommand, - EnableSecurityHubCommand -} from '@aws-sdk/client-securityhub' -import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + EnableSecurityHubCommand, +} from '@aws-sdk/client-securityhub'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SecurityHubEnabled implements BPSet { - private readonly securityHubClient = new SecurityHubClient({}) - private readonly stsClient = new STSClient({}) - private readonly memoSecurityHubClient = Memorizer.memo(this.securityHubClient) - private readonly memoStsClient = Memorizer.memo(this.stsClient) + private readonly securityHubClient = new SecurityHubClient({}); + private readonly stsClient = new STSClient({}); + private readonly memoSecurityHubClient = Memorizer.memo(this.securityHubClient); + private readonly memoStsClient = Memorizer.memo(this.stsClient); - private readonly getAWSAccountId = async () => { - const response = await this.memoStsClient.send(new GetCallerIdentityCommand({})) - return response.Account! - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SecurityHubEnabled', + description: 'Ensures that AWS Security Hub is enabled for the AWS account.', + priority: 1, + priorityReason: 'Enabling Security Hub provides centralized security insights and improves security posture.', + awsService: 'Security Hub', + awsServiceCategory: 'Security', + bestPracticeCategory: 'Governance', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeHubCommand', + reason: 'Checks if Security Hub is enabled for the AWS account.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'EnableSecurityHubCommand', + reason: 'Enables Security Hub for the AWS account.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that enabling Security Hub aligns with your organization’s security compliance policies.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const awsAccountId = await this.getAWSAccountId() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const awsAccountId = await this.getAWSAccountId(); try { - await this.memoSecurityHubClient.send(new DescribeHubCommand({})) - compliantResources.push(awsAccountId) + await this.memoSecurityHubClient.send(new DescribeHubCommand({})); + compliantResources.push(awsAccountId); } catch (error: any) { if (error.name === 'InvalidAccessException') { - nonCompliantResources.push(awsAccountId) + nonCompliantResources.push(awsAccountId); } else { - throw error + throw error; } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async (nonCompliantResources: string[]) => { + public readonly fix = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const accountId of nonCompliantResources) { if (accountId) { - await this.securityHubClient.send(new EnableSecurityHubCommand({})) + await this.securityHubClient.send(new EnableSecurityHubCommand({})); } } - } + }; + + private readonly getAWSAccountId = async (): Promise => { + const response = await this.memoStsClient.send(new GetCallerIdentityCommand({})); + return response.Account!; + }; } diff --git a/src/bpsets/sns/SNSEncryptedKMS.ts b/src/bpsets/sns/SNSEncryptedKMS.ts index c67451a..784ed96 100644 --- a/src/bpsets/sns/SNSEncryptedKMS.ts +++ b/src/bpsets/sns/SNSEncryptedKMS.ts @@ -2,58 +2,128 @@ import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, - SetTopicAttributesCommand -} from '@aws-sdk/client-sns' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + SetTopicAttributesCommand, +} from '@aws-sdk/client-sns'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SNSEncryptedKMS implements BPSet { - private readonly client = new SNSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new SNSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getTopics = async () => { - const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})) - const topics = topicsResponse.Topics || [] + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - const topicDetails = [] - for (const topic of topics) { - const attributes = await this.memoClient.send( - new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) - ) - topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }) - } + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SNSEncryptedKMS', + description: 'Ensures that all SNS topics are encrypted with a KMS key.', + priority: 2, + priorityReason: 'Encryption protects sensitive data in transit and at rest.', + awsService: 'SNS', + awsServiceCategory: 'Messaging', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'kms-key-id', + description: 'The ARN or ID of the KMS key to use for encryption.', + default: '', + example: 'arn:aws:kms:us-east-1:123456789012:key/abcd-1234-efgh-5678', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTopicsCommand', + reason: 'Lists all SNS topics in the account.', + }, + { + name: 'GetTopicAttributesCommand', + reason: 'Retrieves attributes for each SNS topic.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'SetTopicAttributesCommand', + reason: 'Sets the KMS key for encryption on the SNS topic.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the specified KMS key has the necessary permissions to encrypt SNS topics.', + }); - return topicDetails - } + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const topics = await this.getTopics() as any + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const topics = await this.getTopics(); for (const topic of topics) { - if (topic.KmsMasterKeyId) { - compliantResources.push(topic.TopicArn!) + if ((topic as any).KmsMasterKeyId) { + compliantResources.push(topic.TopicArn!); } else { - nonCompliantResources.push(topic.TopicArn!) + nonCompliantResources.push(topic.TopicArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'kms-key-id', value: '' }] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] ) => { - const kmsKeyId = requiredParametersForFix.find(param => param.name === 'kms-key-id')?.value + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] + ) => { + const kmsKeyId = requiredParametersForFix.find((param) => param.name === 'kms-key-id')?.value; if (!kmsKeyId) { - throw new Error("Required parameter 'kms-key-id' is missing.") + throw new Error("Required parameter 'kms-key-id' is missing."); } for (const arn of nonCompliantResources) { @@ -61,9 +131,24 @@ export class SNSEncryptedKMS implements BPSet { new SetTopicAttributesCommand({ TopicArn: arn, AttributeName: 'KmsMasterKeyId', - AttributeValue: kmsKeyId + AttributeValue: kmsKeyId, }) - ) + ); } - } + }; + + private readonly getTopics = async () => { + const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})); + const topics = topicsResponse.Topics || []; + + const topicDetails = []; + for (const topic of topics) { + const attributes = await this.memoClient.send( + new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) + ); + topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }); + } + + return topicDetails; + }; } diff --git a/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts b/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts index 9d0681b..203fc9c 100644 --- a/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts +++ b/src/bpsets/sns/SNSTopicMessageDeliveryNotificationEnabled.ts @@ -2,64 +2,134 @@ import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, - SetTopicAttributesCommand -} from '@aws-sdk/client-sns' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + SetTopicAttributesCommand, +} from '@aws-sdk/client-sns'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SNSTopicMessageDeliveryNotificationEnabled implements BPSet { - private readonly client = new SNSClient({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new SNSClient({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getTopics = async () => { - const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})) - const topics = topicsResponse.Topics || [] + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; - const topicDetails = [] - for (const topic of topics) { - const attributes = await this.memoClient.send( - new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) - ) - topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }) - } + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SNSTopicMessageDeliveryNotificationEnabled', + description: 'Ensures that SNS topics have message delivery notifications enabled.', + priority: 2, + priorityReason: 'Message delivery notifications are essential for monitoring message deliveries.', + awsService: 'SNS', + awsServiceCategory: 'Messaging', + bestPracticeCategory: 'Monitoring', + requiredParametersForFix: [ + { + name: 'sns-feedback-role-arn', + description: 'The ARN of the IAM role to be used for feedback notifications.', + default: '', + example: 'arn:aws:iam::123456789012:role/SNSFeedbackRole', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'ListTopicsCommand', + reason: 'Lists all SNS topics in the account.', + }, + { + name: 'GetTopicAttributesCommand', + reason: 'Retrieves attributes for each SNS topic to check for feedback roles.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'SetTopicAttributesCommand', + reason: 'Enables message delivery notifications by setting the DeliveryPolicy attribute.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the IAM role specified in the fix has the necessary permissions for SNS delivery notifications.', + }); - return topicDetails - } + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const topics = await this.getTopics() + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const topics = await this.getTopics(); for (const topic of topics) { - const feedbackRoles = Object.keys(topic).filter(key => key.endsWith('FeedbackRoleArn')) + const feedbackRoles = Object.keys(topic).filter((key) => + key.endsWith('FeedbackRoleArn') + ); if (feedbackRoles.length > 0) { - compliantResources.push(topic.TopicArn!) + compliantResources.push(topic.TopicArn!); } else { - nonCompliantResources.push(topic.TopicArn!) + nonCompliantResources.push(topic.TopicArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'sns-feedback-role-arn', value: '' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const feedbackRoleArn = requiredParametersForFix.find( - param => param.name === 'sns-feedback-role-arn' - )?.value + (param) => param.name === 'sns-feedback-role-arn' + )?.value; if (!feedbackRoleArn) { - throw new Error("Required parameter 'sns-feedback-role-arn' is missing.") + throw new Error("Required parameter 'sns-feedback-role-arn' is missing."); } for (const arn of nonCompliantResources) { @@ -69,11 +139,26 @@ export class SNSTopicMessageDeliveryNotificationEnabled implements BPSet { AttributeName: 'DeliveryPolicy', AttributeValue: JSON.stringify({ http: { - DefaultFeedbackRoleArn: feedbackRoleArn - } - }) + DefaultFeedbackRoleArn: feedbackRoleArn, + }, + }), }) - ) + ); } - } + }; + + private readonly getTopics = async () => { + const topicsResponse = await this.memoClient.send(new ListTopicsCommand({})); + const topics = topicsResponse.Topics || []; + + const topicDetails = []; + for (const topic of topics) { + const attributes = await this.memoClient.send( + new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }) + ); + topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! }); + } + + return topicDetails; + }; } diff --git a/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts b/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts index 45a4908..6dc3c52 100644 --- a/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts +++ b/src/bpsets/vpc/EC2TransitGatewayAutoVPCAttachDisabled.ts @@ -1,49 +1,121 @@ import { EC2Client, DescribeTransitGatewaysCommand, - ModifyTransitGatewayCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyTransitGatewayCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class EC2TransitGatewayAutoVPCAttachDisabled implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'EC2TransitGatewayAutoVPCAttachDisabled', + description: 'Ensures that Transit Gateways have Auto VPC Attachments disabled.', + priority: 2, + priorityReason: + 'Disabling Auto VPC Attachments reduces the risk of unintentional or unauthorized VPC attachments.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeTransitGatewaysCommand', + reason: 'Fetches information about Transit Gateways and their configurations.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyTransitGatewayCommand', + reason: 'Disables Auto VPC Attachments for non-compliant Transit Gateways.', + }, + ], + adviseBeforeFixFunction: + 'Ensure there are no dependencies on Auto VPC Attachments before disabling it.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const response = await this.memoClient.send(new DescribeTransitGatewaysCommand({})) - const transitGateways = response.TransitGateways || [] + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const response = await this.memoClient.send(new DescribeTransitGatewaysCommand({})); + const transitGateways = response.TransitGateways || []; for (const gateway of transitGateways) { if (gateway.Options?.AutoAcceptSharedAttachments === 'enable') { - nonCompliantResources.push(gateway.TransitGatewayArn!) + nonCompliantResources.push(gateway.TransitGatewayArn!); } else { - compliantResources.push(gateway.TransitGatewayArn!) + compliantResources.push(gateway.TransitGatewayArn!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { - const transitGatewayId = arn.split(':transit-gateway/')[1] + const transitGatewayId = arn.split(':transit-gateway/')[1]; await this.client.send( new ModifyTransitGatewayCommand({ TransitGatewayId: transitGatewayId, Options: { - AutoAcceptSharedAttachments: 'disable' - } + AutoAcceptSharedAttachments: 'disable', + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/RestrictedCommonPorts.ts b/src/bpsets/vpc/RestrictedCommonPorts.ts index cc1293f..960aefa 100644 --- a/src/bpsets/vpc/RestrictedCommonPorts.ts +++ b/src/bpsets/vpc/RestrictedCommonPorts.ts @@ -1,57 +1,125 @@ import { EC2Client, DescribeSecurityGroupRulesCommand, - RevokeSecurityGroupIngressCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RevokeSecurityGroupIngressCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RestrictedCommonPorts implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecurityGroupRules = async () => { - const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) - return response.SecurityGroupRules || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RestrictedCommonPorts', + description: + 'Ensures that common ports (e.g., SSH, HTTP, database ports) are not exposed to the public without proper restrictions.', + priority: 2, + priorityReason: 'Restricting common ports reduces the risk of unauthorized access to critical services.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeSecurityGroupRulesCommand', + reason: 'Fetches the list of security group rules to analyze exposure of common ports.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RevokeSecurityGroupIngressCommand', + reason: 'Revokes ingress rules for non-compliant security group rules.', + }, + ], + adviseBeforeFixFunction: + 'Ensure there are no dependencies on the removed rules. Revoking these rules may disrupt access to critical services.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const commonPorts = [-1, 22, 80, 3306, 3389, 5432, 6379, 11211] - const rules = await this.getSecurityGroupRules() + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; - for (const rule of rules) { + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const commonPorts = [-1, 22, 80, 3306, 3389, 5432, 6379, 11211]; + const rules = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})); + const securityGroupRules = rules.SecurityGroupRules || []; + + for (const rule of securityGroupRules) { if ( !rule.IsEgress && commonPorts.includes(rule.FromPort!) && commonPorts.includes(rule.ToPort!) && !rule.PrefixListId ) { - nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } else { - compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const resource of nonCompliantResources) { - const [groupId, ruleId] = resource.split(' / ') + const [groupId, ruleId] = resource.split(' / '); await this.client.send( new RevokeSecurityGroupIngressCommand({ GroupId: groupId, - SecurityGroupRuleIds: [ruleId] + SecurityGroupRuleIds: [ruleId], }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/RestrictedSSH.ts b/src/bpsets/vpc/RestrictedSSH.ts index 685aa7f..afd5655 100644 --- a/src/bpsets/vpc/RestrictedSSH.ts +++ b/src/bpsets/vpc/RestrictedSSH.ts @@ -1,55 +1,122 @@ import { EC2Client, DescribeSecurityGroupRulesCommand, - RevokeSecurityGroupIngressCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RevokeSecurityGroupIngressCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class RestrictedSSH implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecurityGroupRules = async () => { - const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) - return response.SecurityGroupRules || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'RestrictedSSH', + description: 'Ensures SSH (port 22) is not accessible from 0.0.0.0/0 in security groups.', + priority: 1, + priorityReason: 'Restricting SSH access reduces the risk of unauthorized access.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeSecurityGroupRulesCommand', + reason: 'Fetches the list of security group rules to check for unrestricted SSH access.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RevokeSecurityGroupIngressCommand', + reason: 'Revokes ingress rules for SSH access from 0.0.0.0/0.', + }, + ], + adviseBeforeFixFunction: + 'Ensure no critical systems depend on the current security group rules allowing unrestricted SSH access.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const rules = await this.getSecurityGroupRules() - for (const rule of rules) { + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const rules = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})); + const securityGroupRules = rules.SecurityGroupRules || []; + + for (const rule of securityGroupRules) { if ( !rule.IsEgress && rule.FromPort! <= 22 && rule.ToPort! >= 22 && rule.CidrIpv4 === '0.0.0.0/0' ) { - nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } else { - compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const resource of nonCompliantResources) { - const [groupId, ruleId] = resource.split(' / ') + const [groupId, ruleId] = resource.split(' / '); await this.client.send( new RevokeSecurityGroupIngressCommand({ GroupId: groupId, - SecurityGroupRuleIds: [ruleId] + SecurityGroupRuleIds: [ruleId], }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts b/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts index 6909d35..070a1d1 100644 --- a/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts +++ b/src/bpsets/vpc/SubnetAutoAssignPublicIPDisabled.ts @@ -1,45 +1,116 @@ import { EC2Client, DescribeSubnetsCommand, - ModifySubnetAttributeCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifySubnetAttributeCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class SubnetAutoAssignPublicIPDisabled implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'SubnetAutoAssignPublicIPDisabled', + description: 'Ensures that subnets do not automatically assign public IPs.', + priority: 2, + priorityReason: 'Automatically assigning public IPs increases the attack surface of the VPC.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeSubnetsCommand', + reason: 'Fetches details of all subnets to check the MapPublicIpOnLaunch attribute.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifySubnetAttributeCommand', + reason: 'Disables the automatic assignment of public IPs for the specified subnets.', + }, + ], + adviseBeforeFixFunction: + 'Ensure there are no workloads depending on automatic public IP assignment in the affected subnets.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const response = await this.memoClient.send(new DescribeSubnetsCommand({})) - const subnets = response.Subnets || [] + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const response = await this.memoClient.send(new DescribeSubnetsCommand({})); + const subnets = response.Subnets || []; for (const subnet of subnets) { if (subnet.MapPublicIpOnLaunch) { - nonCompliantResources.push(subnet.SubnetId!) + nonCompliantResources.push(subnet.SubnetId!); } else { - compliantResources.push(subnet.SubnetId!) + compliantResources.push(subnet.SubnetId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const subnetId of nonCompliantResources) { await this.client.send( new ModifySubnetAttributeCommand({ SubnetId: subnetId, - MapPublicIpOnLaunch: { Value: false } + MapPublicIpOnLaunch: { Value: false }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts b/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts index 4952d09..e1d1df4 100644 --- a/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts +++ b/src/bpsets/vpc/VPCDefaultSecurityGroupClosed.ts @@ -2,55 +2,130 @@ import { EC2Client, DescribeSecurityGroupsCommand, RevokeSecurityGroupIngressCommand, - RevokeSecurityGroupEgressCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RevokeSecurityGroupEgressCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class VPCDefaultSecurityGroupClosed implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'VPCDefaultSecurityGroupClosed', + description: 'Ensures that default VPC security groups have no ingress or egress rules.', + priority: 2, + priorityReason: 'Default security groups should be closed to enhance network security.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeSecurityGroupsCommand', + reason: 'Fetch details of default security groups for compliance checks.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RevokeSecurityGroupIngressCommand', + reason: 'Remove all ingress rules from the default security group.', + }, + { + name: 'RevokeSecurityGroupEgressCommand', + reason: 'Remove all egress rules from the default security group.', + }, + ], + adviseBeforeFixFunction: 'Ensure no critical resources depend on default security group rules.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; const response = await this.memoClient.send( new DescribeSecurityGroupsCommand({ - Filters: [{ Name: 'group-name', Values: ['default'] }] + Filters: [{ Name: 'group-name', Values: ['default'] }], }) - ) - const securityGroups = response.SecurityGroups || [] + ); + const securityGroups = response.SecurityGroups || []; for (const group of securityGroups) { if (group.IpPermissions?.length || group.IpPermissionsEgress?.length) { - nonCompliantResources.push(group.GroupId!) + nonCompliantResources.push(group.GroupId!); } else { - compliantResources.push(group.GroupId!) + compliantResources.push(group.GroupId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const groupId of nonCompliantResources) { await this.client.send( new RevokeSecurityGroupIngressCommand({ GroupId: groupId, - IpPermissions: [] + IpPermissions: [], // This revokes all ingress rules }) - ) + ); + await this.client.send( new RevokeSecurityGroupEgressCommand({ GroupId: groupId, - IpPermissions: [] + IpPermissions: [], // This revokes all egress rules }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/VPCFlowLogsEnabled.ts b/src/bpsets/vpc/VPCFlowLogsEnabled.ts index 3db350e..81ae1fb 100644 --- a/src/bpsets/vpc/VPCFlowLogsEnabled.ts +++ b/src/bpsets/vpc/VPCFlowLogsEnabled.ts @@ -2,53 +2,135 @@ import { EC2Client, DescribeVpcsCommand, DescribeFlowLogsCommand, - CreateFlowLogsCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + CreateFlowLogsCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class VPCFlowLogsEnabled implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'VPCFlowLogsEnabled', + description: 'Ensures that VPC Flow Logs are enabled for all VPCs.', + priority: 1, + priorityReason: 'Enabling VPC Flow Logs provides visibility into network traffic for compliance and security.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'log-group-name', + description: 'The CloudWatch log group where flow logs will be stored.', + default: '', + example: '/aws/vpc/flow-logs', + }, + { + name: 'iam-role-arn', + description: 'The IAM role ARN that grants permission to write flow logs to CloudWatch.', + default: '', + example: 'arn:aws:iam::123456789012:role/FlowLogsRole', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeFlowLogsCommand', + reason: 'Fetch existing flow logs to determine VPCs with enabled flow logs.', + }, + { + name: 'DescribeVpcsCommand', + reason: 'Fetch the list of VPCs in the account for comparison.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'CreateFlowLogsCommand', + reason: 'Enable flow logs for non-compliant VPCs.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the provided log group and IAM role are configured correctly to avoid errors when enabling flow logs.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const flowLogsResponse = await this.memoClient.send(new DescribeFlowLogsCommand({})) - const flowLogs = flowLogsResponse.FlowLogs || [] - const flowLogEnabledVpcs = flowLogs.map(log => log.ResourceId!) + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; - const vpcsResponse = await this.memoClient.send(new DescribeVpcsCommand({})) - const vpcs = vpcsResponse.Vpcs || [] + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const flowLogsResponse = await this.memoClient.send(new DescribeFlowLogsCommand({})); + const flowLogs = flowLogsResponse.FlowLogs || []; + const flowLogEnabledVpcs = flowLogs.map((log) => log.ResourceId!); + + const vpcsResponse = await this.memoClient.send(new DescribeVpcsCommand({})); + const vpcs = vpcsResponse.Vpcs || []; for (const vpc of vpcs) { if (flowLogEnabledVpcs.includes(vpc.VpcId!)) { - compliantResources.push(vpc.VpcId!) + compliantResources.push(vpc.VpcId!); } else { - nonCompliantResources.push(vpc.VpcId!) + nonCompliantResources.push(vpc.VpcId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [ - { name: 'log-group-name', value: '' }, - { name: 'iam-role-arn', value: '' } - ] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const logGroupName = requiredParametersForFix.find(param => param.name === 'log-group-name')?.value - const iamRoleArn = requiredParametersForFix.find(param => param.name === 'iam-role-arn')?.value + public readonly fix = async (nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[]) => { + const logGroupName = requiredParametersForFix.find((param) => param.name === 'log-group-name')?.value; + const iamRoleArn = requiredParametersForFix.find((param) => param.name === 'iam-role-arn')?.value; if (!logGroupName || !iamRoleArn) { - throw new Error("Required parameters 'log-group-name' and 'iam-role-arn' are missing.") + throw new Error("Required parameters 'log-group-name' and 'iam-role-arn' are missing."); } for (const vpcId of nonCompliantResources) { @@ -58,9 +140,9 @@ export class VPCFlowLogsEnabled implements BPSet { ResourceType: 'VPC', LogGroupName: logGroupName, DeliverLogsPermissionArn: iamRoleArn, - TrafficType: 'ALL' + TrafficType: 'ALL', }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts b/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts index 8dc7478..86ae250 100644 --- a/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts +++ b/src/bpsets/vpc/VPCNetworkACLUnusedCheck.ts @@ -1,44 +1,115 @@ import { EC2Client, DescribeNetworkAclsCommand, - DeleteNetworkAclCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + DeleteNetworkAclCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class VPCNetworkACLUnusedCheck implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'VPCNetworkACLUnusedCheck', + description: 'Ensures that unused network ACLs are identified and removed.', + priority: 2, + priorityReason: 'Unused network ACLs can clutter the environment and pose a maintenance risk.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeNetworkAclsCommand', + reason: 'Fetch details of all network ACLs to identify unused ones.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'DeleteNetworkAclCommand', + reason: 'Delete network ACLs that are not associated with any resources.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the identified network ACLs are not required for future configurations before deleting them.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const response = await this.memoClient.send(new DescribeNetworkAclsCommand({})) - const networkAcls = response.NetworkAcls || [] + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const response = await this.memoClient.send(new DescribeNetworkAclsCommand({})); + const networkAcls = response.NetworkAcls || []; for (const acl of networkAcls) { if (!acl.Associations || acl.Associations.length === 0) { - nonCompliantResources.push(acl.NetworkAclId!) + nonCompliantResources.push(acl.NetworkAclId!); } else { - compliantResources.push(acl.NetworkAclId!) + compliantResources.push(acl.NetworkAclId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const aclId of nonCompliantResources) { await this.client.send( new DeleteNetworkAclCommand({ - NetworkAclId: aclId + NetworkAclId: aclId, }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts b/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts index 47db69e..81702a3 100644 --- a/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts +++ b/src/bpsets/vpc/VPCPeeringDNSResolutionCheck.ts @@ -1,56 +1,126 @@ import { EC2Client, DescribeVpcPeeringConnectionsCommand, - ModifyVpcPeeringConnectionOptionsCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + ModifyVpcPeeringConnectionOptionsCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class VPCPeeringDNSResolutionCheck implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); + + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'VPCPeeringDNSResolutionCheck', + description: 'Ensures that DNS resolution is enabled for all VPC peering connections.', + priority: 2, + priorityReason: 'DNS resolution is necessary for seamless communication across VPCs.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Connectivity', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'DescribeVpcPeeringConnectionsCommand', + reason: 'Retrieve details of all VPC peering connections.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'ModifyVpcPeeringConnectionOptionsCommand', + reason: 'Enable DNS resolution for VPC peering connections.', + }, + ], + adviseBeforeFixFunction: 'Ensure the DNS resolution setting aligns with your network architecture and connectivity needs.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] + this.stats.status = 'CHECKING'; - const response = await this.memoClient.send(new DescribeVpcPeeringConnectionsCommand({})) - const vpcPeeringConnections = response.VpcPeeringConnections || [] + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + + const response = await this.memoClient.send(new DescribeVpcPeeringConnectionsCommand({})); + const vpcPeeringConnections = response.VpcPeeringConnections || []; for (const connection of vpcPeeringConnections) { - const accepterOptions = connection.AccepterVpcInfo?.PeeringOptions - const requesterOptions = connection.RequesterVpcInfo?.PeeringOptions + const accepterOptions = connection.AccepterVpcInfo?.PeeringOptions; + const requesterOptions = connection.RequesterVpcInfo?.PeeringOptions; if ( !accepterOptions?.AllowDnsResolutionFromRemoteVpc || !requesterOptions?.AllowDnsResolutionFromRemoteVpc ) { - nonCompliantResources.push(connection.VpcPeeringConnectionId!) + nonCompliantResources.push(connection.VpcPeeringConnectionId!); } else { - compliantResources.push(connection.VpcPeeringConnectionId!) + compliantResources.push(connection.VpcPeeringConnectionId!); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const connectionId of nonCompliantResources) { await this.client.send( new ModifyVpcPeeringConnectionOptionsCommand({ VpcPeeringConnectionId: connectionId, AccepterPeeringConnectionOptions: { - AllowDnsResolutionFromRemoteVpc: true + AllowDnsResolutionFromRemoteVpc: true, }, RequesterPeeringConnectionOptions: { - AllowDnsResolutionFromRemoteVpc: true - } + AllowDnsResolutionFromRemoteVpc: true, + }, }) - ) + ); } - } + }; } diff --git a/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts b/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts index 501dad2..164093c 100644 --- a/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts +++ b/src/bpsets/vpc/VPCSGOpenOnlyToAuthorizedPorts.ts @@ -1,26 +1,83 @@ import { EC2Client, DescribeSecurityGroupRulesCommand, - RevokeSecurityGroupIngressCommand -} from '@aws-sdk/client-ec2' -import { BPSet } from '../../types' -import { Memorizer } from '../../Memorizer' + RevokeSecurityGroupIngressCommand, +} from '@aws-sdk/client-ec2'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; +import { Memorizer } from '../../Memorizer'; export class VPCSGOpenOnlyToAuthorizedPorts implements BPSet { - private readonly client = new EC2Client({}) - private readonly memoClient = Memorizer.memo(this.client) + private readonly client = new EC2Client({}); + private readonly memoClient = Memorizer.memo(this.client); - private readonly getSecurityGroupRules = async () => { - const response = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) - return response.SecurityGroupRules || [] - } + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'VPCSGOpenOnlyToAuthorizedPorts', + description: + 'Ensures that security group rules do not allow unrestricted access to unauthorized ports.', + priority: 3, + priorityReason: + 'Restricting open access to unauthorized ports is crucial for minimizing the attack surface.', + awsService: 'EC2', + awsServiceCategory: 'Networking', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: true, + commandUsedInCheckFunction: [ + { + name: 'DescribeSecurityGroupRulesCommand', + reason: 'Retrieve all security group rules for analysis.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'RevokeSecurityGroupIngressCommand', + reason: 'Revoke ingress rules that allow open access to unauthorized ports.', + }, + ], + adviseBeforeFixFunction: + 'Ensure that the removal of these rules does not impact legitimate network traffic.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; + }; public readonly check = async () => { - const compliantResources: string[] = [] - const nonCompliantResources: string[] = [] - const authorizedPorts = [80, 443] // Example authorized ports + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { + const compliantResources: string[] = []; + const nonCompliantResources: string[] = []; + const authorizedPorts = [80, 443]; // Example authorized ports + + const rules = await this.memoClient.send(new DescribeSecurityGroupRulesCommand({})) + .then((response) => response.SecurityGroupRules || []); - const rules = await this.getSecurityGroupRules() for (const rule of rules) { if ( !rule.IsEgress && @@ -28,29 +85,42 @@ export class VPCSGOpenOnlyToAuthorizedPorts implements BPSet { !authorizedPorts.includes(rule.FromPort!) && !authorizedPorts.includes(rule.ToPort!) ) { - nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + nonCompliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } else { - compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`) + compliantResources.push(`${rule.GroupId} / ${rule.SecurityGroupRuleId}`); } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [] - } - } + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const resource of nonCompliantResources) { - const [groupId, ruleId] = resource.split(' / ') + const [groupId, ruleId] = resource.split(' / '); await this.client.send( new RevokeSecurityGroupIngressCommand({ GroupId: groupId, - SecurityGroupRuleIds: [ruleId] + SecurityGroupRuleIds: [ruleId], }) - ) + ); } - } + }; } diff --git a/src/bpsets/waf/WAFv2LoggingEnabled.ts b/src/bpsets/waf/WAFv2LoggingEnabled.ts index 2d0eb5b..e459f8d 100644 --- a/src/bpsets/waf/WAFv2LoggingEnabled.ts +++ b/src/bpsets/waf/WAFv2LoggingEnabled.ts @@ -4,7 +4,7 @@ import { GetLoggingConfigurationCommand, PutLoggingConfigurationCommand, } from '@aws-sdk/client-wafv2'; -import { BPSet } from '../../types'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { Memorizer } from '../../Memorizer'; export class WAFv2LoggingEnabled implements BPSet { @@ -13,13 +13,72 @@ export class WAFv2LoggingEnabled implements BPSet { private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); private readonly memoGlobalClient = Memorizer.memo(this.globalClient, 'global'); - private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { - const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; - const response = await client.send(new ListWebACLsCommand({ Scope: scope })); - return response.WebACLs || []; + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'WAFv2LoggingEnabled', + description: 'Ensures that AWS WAFv2 WebACLs have logging enabled.', + priority: 2, + priorityReason: 'Logging is critical for monitoring and auditing web traffic behavior.', + awsService: 'WAFv2', + awsServiceCategory: 'Web Application Firewall', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'log-group-arn', + description: 'The ARN of the CloudWatch Log Group for logging.', + default: '', + example: 'arn:aws:logs:us-east-1:123456789012:log-group:example-log-group', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetLoggingConfigurationCommand', + reason: 'Check if logging is configured for the WebACL.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'PutLoggingConfigurationCommand', + reason: 'Enable logging for the WebACL.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the specified log group exists and has the correct permissions to be used by WAFv2.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; }; public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { const compliantResources: string[] = []; const nonCompliantResources: string[] = []; @@ -41,18 +100,34 @@ export class WAFv2LoggingEnabled implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'log-group-arn', value: '' }], - }; + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; }; - public readonly fix = async ( - nonCompliantResources: string[], - requiredParametersForFix: { name: string; value: string }[] - ) => { - const logGroupArn = requiredParametersForFix.find(param => param.name === 'log-group-arn')?.value; + private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListWebACLsCommand({ Scope: scope })); + return response.WebACLs || []; + }; + + public readonly fix = async (nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[]) => { + const logGroupArn = requiredParametersForFix.find((param) => param.name === 'log-group-arn')?.value; if (!logGroupArn) { throw new Error("Required parameter 'log-group-arn' is missing."); diff --git a/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts b/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts index 63a0890..c7b2b79 100644 --- a/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts +++ b/src/bpsets/waf/WAFv2RuleGroupLoggingEnabled.ts @@ -4,7 +4,7 @@ import { GetRuleGroupCommand, UpdateRuleGroupCommand, } from '@aws-sdk/client-wafv2'; -import { BPSet } from '../../types'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { Memorizer } from '../../Memorizer'; export class WAFv2RuleGroupLoggingEnabled implements BPSet { @@ -13,13 +13,64 @@ export class WAFv2RuleGroupLoggingEnabled implements BPSet { private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); private readonly memoGlobalClient = Memorizer.memo(this.globalClient, 'global'); - private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { - const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; - const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); - return response.RuleGroups || []; + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'WAFv2RuleGroupLoggingEnabled', + description: 'Ensures that AWS WAFv2 Rule Groups have logging enabled.', + priority: 2, + priorityReason: 'Enabling logging on WAF Rule Groups helps monitor and audit security.', + awsService: 'WAFv2', + awsServiceCategory: 'Web Application Firewall', + bestPracticeCategory: 'Security', + requiredParametersForFix: [], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetRuleGroupCommand', + reason: 'Retrieve details of a WAFv2 Rule Group.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateRuleGroupCommand', + reason: 'Enable logging for the WAFv2 Rule Group.', + }, + ], + adviseBeforeFixFunction: 'Ensure necessary CloudWatch permissions are granted for logging.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; }; public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { const compliantResources: string[] = []; const nonCompliantResources: string[] = []; @@ -40,17 +91,35 @@ export class WAFv2RuleGroupLoggingEnabled implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [], - }; + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); + return response.RuleGroups || []; }; public readonly fix = async (nonCompliantResources: string[]) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async (nonCompliantResources: string[]) => { for (const arn of nonCompliantResources) { const client = arn.includes('global') ? this.globalClient : this.regionalClient; - const [name, id] = arn.split('/')[1].split(':'); await client.send( @@ -63,7 +132,7 @@ export class WAFv2RuleGroupLoggingEnabled implements BPSet { MetricName: `WAFRuleGroup-${name}`, SampledRequestsEnabled: true, }, - LockToken: undefined + LockToken: undefined, // Replace with actual LockToken if needed. }) ); } diff --git a/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts b/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts index a6136b5..4579a09 100644 --- a/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts +++ b/src/bpsets/waf/WAFv2RuleGroupNotEmpty.ts @@ -2,9 +2,9 @@ import { WAFV2Client, ListRuleGroupsCommand, GetRuleGroupCommand, - UpdateRuleGroupCommand + UpdateRuleGroupCommand, } from '@aws-sdk/client-wafv2'; -import { BPSet } from '../../types'; +import { BPSet, BPSetFixFn, BPSetMetadata, BPSetStats } from '../../types'; import { Memorizer } from '../../Memorizer'; export class WAFv2RuleGroupNotEmpty implements BPSet { @@ -13,13 +13,72 @@ export class WAFv2RuleGroupNotEmpty implements BPSet { private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); private readonly memoGlobalClient = Memorizer.memo(this.globalClient, 'global'); - private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { - const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; - const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); - return response.RuleGroups || []; + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'WAFv2RuleGroupNotEmpty', + description: 'Ensures WAFv2 Rule Groups are not empty and contain at least one rule.', + priority: 2, + priorityReason: 'Empty rule groups provide no security benefit and should be avoided.', + awsService: 'WAFv2', + awsServiceCategory: 'Web Application Firewall', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'default-rule', + description: 'Default rule JSON to populate empty rule groups.', + default: '{}', + example: '{"IpSetReferenceStatement": {"Arn": "example-arn"}}', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetRuleGroupCommand', + reason: 'Retrieve details of a WAFv2 Rule Group to check its rules.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateRuleGroupCommand', + reason: 'Add default rule to empty WAFv2 Rule Groups.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the default rule JSON is correctly formatted and meets security requirements.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; }; public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { const compliantResources: string[] = []; const nonCompliantResources: string[] = []; @@ -40,14 +99,33 @@ export class WAFv2RuleGroupNotEmpty implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'default-rule', value: '' }] - }; + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; }; - public readonly fix = async ( + private readonly getRuleGroups = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListRuleGroupsCommand({ Scope: scope })); + return response.RuleGroups || []; + }; + + public readonly fix: BPSetFixFn = async (...args) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(...args) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] ) => { @@ -59,7 +137,6 @@ export class WAFv2RuleGroupNotEmpty implements BPSet { for (const arn of nonCompliantResources) { const client = arn.includes('global') ? this.globalClient : this.regionalClient; - const [name, id] = arn.split('/')[1].split(':'); await client.send( @@ -77,15 +154,15 @@ export class WAFv2RuleGroupNotEmpty implements BPSet { VisibilityConfig: { CloudWatchMetricsEnabled: true, MetricName: `DefaultRule-${name}`, - SampledRequestsEnabled: true - } - } + SampledRequestsEnabled: true, + }, + }, ], VisibilityConfig: { CloudWatchMetricsEnabled: true, MetricName: `RuleGroup-${name}`, - SampledRequestsEnabled: true - } + SampledRequestsEnabled: true, + }, }) ); } diff --git a/src/bpsets/waf/WAFv2WebACLNotEmpty.ts b/src/bpsets/waf/WAFv2WebACLNotEmpty.ts index 3a03562..d7fe3c6 100644 --- a/src/bpsets/waf/WAFv2WebACLNotEmpty.ts +++ b/src/bpsets/waf/WAFv2WebACLNotEmpty.ts @@ -2,9 +2,9 @@ import { WAFV2Client, ListWebACLsCommand, GetWebACLCommand, - UpdateWebACLCommand + UpdateWebACLCommand, } from '@aws-sdk/client-wafv2'; -import { BPSet } from '../../types'; +import { BPSet, BPSetMetadata, BPSetStats } from '../../types'; import { Memorizer } from '../../Memorizer'; export class WAFv2WebACLNotEmpty implements BPSet { @@ -13,13 +13,72 @@ export class WAFv2WebACLNotEmpty implements BPSet { private readonly memoRegionalClient = Memorizer.memo(this.regionalClient); private readonly memoGlobalClient = Memorizer.memo(this.globalClient, 'global'); - private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { - const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; - const response = await client.send(new ListWebACLsCommand({ Scope: scope })); - return response.WebACLs || []; + private readonly stats: BPSetStats = { + compliantResources: [], + nonCompliantResources: [], + status: 'LOADED', + errorMessage: [], + }; + + public readonly getMetadata = (): BPSetMetadata => ({ + name: 'WAFv2WebACLNotEmpty', + description: 'Ensures WAFv2 Web ACLs are not empty and contain at least one rule.', + priority: 2, + priorityReason: 'Empty Web ACLs provide no protection and should contain at least one rule.', + awsService: 'WAFv2', + awsServiceCategory: 'Web Application Firewall', + bestPracticeCategory: 'Security', + requiredParametersForFix: [ + { + name: 'default-rule', + description: 'Default rule JSON to populate empty Web ACLs.', + default: '{}', + example: '{"IpSetReferenceStatement": {"Arn": "example-arn"}}', + }, + ], + isFixFunctionUsesDestructiveCommand: false, + commandUsedInCheckFunction: [ + { + name: 'GetWebACLCommand', + reason: 'Retrieve details of a WAFv2 Web ACL to check its rules.', + }, + ], + commandUsedInFixFunction: [ + { + name: 'UpdateWebACLCommand', + reason: 'Add a default rule to empty Web ACLs.', + }, + ], + adviseBeforeFixFunction: + 'Ensure the default rule JSON is correctly formatted and aligns with security requirements.', + }); + + public readonly getStats = () => this.stats; + + public readonly clearStats = () => { + this.stats.compliantResources = []; + this.stats.nonCompliantResources = []; + this.stats.status = 'LOADED'; + this.stats.errorMessage = []; }; public readonly check = async () => { + this.stats.status = 'CHECKING'; + + await this.checkImpl() + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly checkImpl = async () => { const compliantResources: string[] = []; const nonCompliantResources: string[] = []; @@ -40,16 +99,38 @@ export class WAFv2WebACLNotEmpty implements BPSet { } } - return { - compliantResources, - nonCompliantResources, - requiredParametersForFix: [{ name: 'default-rule', value: '' }] - }; + this.stats.compliantResources = compliantResources; + this.stats.nonCompliantResources = nonCompliantResources; + }; + + private readonly getWebACLs = async (scope: 'REGIONAL' | 'CLOUDFRONT') => { + const client = scope === 'REGIONAL' ? this.memoRegionalClient : this.memoGlobalClient; + const response = await client.send(new ListWebACLsCommand({ Scope: scope })); + return response.WebACLs || []; }; public readonly fix = async ( nonCompliantResources: string[], requiredParametersForFix: { name: string; value: string }[] + ) => { + this.stats.status = 'CHECKING'; + + await this.fixImpl(nonCompliantResources, requiredParametersForFix) + .then(() => { + this.stats.status = 'FINISHED'; + }) + .catch((err) => { + this.stats.status = 'ERROR'; + this.stats.errorMessage.push({ + date: new Date(), + message: err.message, + }); + }); + }; + + private readonly fixImpl = async ( + nonCompliantResources: string[], + requiredParametersForFix: { name: string; value: string }[] ) => { const defaultRule = requiredParametersForFix.find(param => param.name === 'default-rule')?.value; @@ -59,7 +140,6 @@ export class WAFv2WebACLNotEmpty implements BPSet { for (const arn of nonCompliantResources) { const client = arn.includes('global') ? this.globalClient : this.regionalClient; - const [name, id] = arn.split('/')[1].split(':'); await client.send( @@ -77,16 +157,16 @@ export class WAFv2WebACLNotEmpty implements BPSet { VisibilityConfig: { CloudWatchMetricsEnabled: true, MetricName: `DefaultRule-${name}`, - SampledRequestsEnabled: true - } - } + SampledRequestsEnabled: true, + }, + }, ], DefaultAction: { Allow: {} }, VisibilityConfig: { CloudWatchMetricsEnabled: true, MetricName: `WebACL-${name}`, - SampledRequestsEnabled: true - } + SampledRequestsEnabled: true, + }, }) ); } diff --git a/src/types.d.ts b/src/types.d.ts index d49a02a..5816890 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -7,22 +7,21 @@ * @author Minhyeok Park */ export interface BPSet { - check: () => Promise<{ - compliantResources: string[] - nonCompliantResources: string[] - requiredParametersForFix: { - name: string - }[] - }>, - fix: ( - nonCompliantResources: string[], - requiredParametersForFix: { - name: string, - value: string - }[] - ) => Promise + getMetadata: () => BPSetMetadata + getStats: () => BPSetStats + clearStats: () => void + check: () => Promise + fix: BPSetFixFn } +export type BPSetFixFn = ( + nonCompliantResources: string[], + requiredParametersForFix: { + name: string, + value: string + }[] +) => Promise + export interface BPSetMetadata { name: string description: string @@ -47,10 +46,12 @@ export interface BPSetMetadata { reason: string }[] adviseBeforeFixFunction: string +} + +export interface BPSetStats { nonCompliantResources: string[] compliantResources: string[] status: 'LOADED' | 'CHECKING' | 'ERROR' | 'FINISHED' - idx: number errorMessage: { date: Date, message: string