---
# Copyright widdix GmbH
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
# OLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
Description: bucketAV for Cloudflare R2 powered by multiple engines (dedicated VPC; scanners run in private subnets) - Antivirus protection for Cloudflare R2
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - KeyName
          - InfrastructureAlarmsEmail
          - CloudflareAccountId
          - CloudflareApiToken
          - CloudflareAccessKeyId
          - CloudflareAccessKeySecret
      - Label:
          default: Scan Parameters
        Parameters:
          - DeleteInfectedFiles
          - ReportEventBridge
          - ScanDelayInSeconds
          - EngineClamAVEnableCache
          - EngineClamAVAdditionalDatabaseUrls
          - EngineSophosSophosLiveProtectionCloudLookups
      - Label:
          default: VPC Parameters
        Parameters:
          - VpcCidrBlock
          - SSHIngressCidrIp
          - VpcSubnetCidrBits
          - FlowLogRetentionInDays
      - Label:
          default: Auto Scaling Group Parameters
        Parameters:
          - EngineClamAVAutoScalingMinSize
          - EngineClamAVAutoScalingMaxSize
          - EngineSophosAutoScalingMinSize
          - EngineSophosAutoScalingMaxSize
          - CapacityStrategy
      - Label:
          default: EC2 Parameters
        Parameters:
          - EngineClamAVInstanceType
          - EngineClamAVAlternateInstanceType1
          - EngineClamAVAlternateInstanceType2
          - EngineClamAVAlternateInstanceType3
          - EngineSophosInstanceType
          - EngineSophosAlternateInstanceType1
          - EngineSophosAlternateInstanceType2
          - EngineSophosAlternateInstanceType3
          - EngineSophosVolumeSize
          - EngineSophosVolumeIops
          - EngineSophosVolumeThroughput
          - LogsRetentionInDays
          - SystemsManagerAccess
      - Label:
          default: Permissions Parameters
        Parameters:
          - ManagedPolicyArns
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - CloudflareQueueFunctionReservedConcurrentExecutions
          - OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - DashboardLambdaFunctionReservedConcurrentExecutions
Parameters:
  DeleteInfectedFiles:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Automatically delete infected files.
  ReportEventBridge:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Report scan results to EventBridge.
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: "Name of the EC2 key pair to log in via SSH (username: ec2-user)."
  LogsRetentionInDays:
    Type: Number
    Default: 14
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1827
      - 3653
    Description: Specifies the number of days you want to retain log events.
  SystemsManagerAccess:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable AWS Systems Manager Session Manager to connect to the EC2 instances. To fully enable SSM, add arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore to the ManagedPolicyArns configuration parameter as well.
  PermissionsBoundary:
    Type: String
    Default: ""
    Description: Optional IAM policy ARN that will be used as the permissions boundary for all roles.
  CapacityStrategy:
    Type: String
    Default: SpotWithOnDemandFallback
    AllowedValues:
      - SpotWithOnDemandFallback
      - OnDemandOnly
      - SpotOnly
    Description: Take advantage of unused EC2 capacity in the AWS cloud by launching spot instances that are up to 90% cheaper than on-demand prices. Keep in mind that spot instances can be interrupted at any time and are replaced automatically!
  ManagedPolicyArns:
    Type: String
    Default: ""
    Description: Optional comma-delimited list of IAM managed policy ARNs to attach to the IAM role of the EC2 instances.
  ScanDelayInSeconds:
    Type: Number
    Default: 0
    Description: Delay the scanning of objects by 0-900 seconds.
    MaxValue: 900
    MinValue: 0
  InfrastructureAlarmsEmail:
    Type: String
    Default: ""
    Description: Optional but strongly recommended email address receiving infrastructure alarms (for more than one email address, please subscribe to the Infrastructure Alarms SNS topic after stack creation).
  ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions:
    Type: Number
    Default: 0
    Description: Maximum number of execution environment instances for the Lambda function (set to 0 to disable; Check out the CloudWatch metric ConcurrentExecutions to get the maximum concurrent invocations of the past).
    MinValue: 0
  FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions:
    Type: Number
    Default: 0
    Description: Maximum number of execution environment instances for the Lambda function (set to 0 to disable; Check out the CloudWatch metric ConcurrentExecutions to get the maximum concurrent invocations of the past).
    MinValue: 0
  EngineClamAVInstanceType:
    Type: String
    Default: m6i.large
    AllowedValues:
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
    Description: Specifies the instance type of the EC2 instance (low performance for t3a.small and t3.small)
  EngineClamAVAlternateInstanceType1:
    Type: String
    Default: m7i.large
    AllowedValues:
      - Disabled
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  EngineClamAVAlternateInstanceType2:
    Type: String
    Default: t3.large
    AllowedValues:
      - Disabled
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  EngineClamAVAlternateInstanceType3:
    Type: String
    Default: Disabled
    AllowedValues:
      - Disabled
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  EngineSophosInstanceType:
    Type: String
    Default: m6i.large
    AllowedValues:
      - t3a.nano
      - t3a.micro
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - m5zn.large
      - m5zn.xlarge
      - m5zn.2xlarge
      - m5zn.3xlarge
      - m5zn.6xlarge
      - m5zn.12xlarge
    Description: Specifies the instance type of the EC2 instance
  EngineSophosAlternateInstanceType1:
    Type: String
    Default: m7i.large
    AllowedValues:
      - Disabled
      - t3a.nano
      - t3a.micro
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - m5zn.large
      - m5zn.xlarge
      - m5zn.2xlarge
      - m5zn.3xlarge
      - m5zn.6xlarge
      - m5zn.12xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  EngineSophosAlternateInstanceType2:
    Type: String
    Default: t3.large
    AllowedValues:
      - Disabled
      - t3a.nano
      - t3a.micro
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - m5zn.large
      - m5zn.xlarge
      - m5zn.2xlarge
      - m5zn.3xlarge
      - m5zn.6xlarge
      - m5zn.12xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  EngineSophosAlternateInstanceType3:
    Type: String
    Default: Disabled
    AllowedValues:
      - Disabled
      - t3a.nano
      - t3a.micro
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.8xlarge
      - m5a.12xlarge
      - m5a.16xlarge
      - m5a.24xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
      - m6i.4xlarge
      - m6i.8xlarge
      - m6i.12xlarge
      - m6i.16xlarge
      - m6i.24xlarge
      - m6i.32xlarge
      - m6a.large
      - m6a.xlarge
      - m6a.2xlarge
      - m6a.4xlarge
      - m6a.8xlarge
      - m6a.12xlarge
      - m6a.16xlarge
      - m6a.24xlarge
      - m6a.32xlarge
      - m6a.48xlarge
      - m7i.large
      - m7i.xlarge
      - m7i.2xlarge
      - m7i.4xlarge
      - m7i.8xlarge
      - m7i.12xlarge
      - m7i.16xlarge
      - m7i.24xlarge
      - m7i.48xlarge
      - m7a.medium
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - m7a.12xlarge
      - m7a.16xlarge
      - m7a.24xlarge
      - m7a.32xlarge
      - m7a.48xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
      - c5a.large
      - c5a.xlarge
      - c5a.2xlarge
      - c5a.4xlarge
      - c5a.8xlarge
      - c5a.12xlarge
      - c5a.16xlarge
      - c5a.24xlarge
      - c6i.large
      - c6i.xlarge
      - c6i.2xlarge
      - c6i.4xlarge
      - c6i.8xlarge
      - c6i.12xlarge
      - c6i.16xlarge
      - c6i.24xlarge
      - c6i.32xlarge
      - c6a.large
      - c6a.xlarge
      - c6a.2xlarge
      - c6a.4xlarge
      - c6a.8xlarge
      - c6a.12xlarge
      - c6a.16xlarge
      - c6a.24xlarge
      - c6a.32xlarge
      - c6a.48xlarge
      - c7i.large
      - c7i.xlarge
      - c7i.2xlarge
      - c7i.4xlarge
      - c7i.8xlarge
      - c7i.12xlarge
      - c7i.16xlarge
      - c7i.24xlarge
      - c7i.48xlarge
      - c7a.medium
      - c7a.large
      - c7a.xlarge
      - c7a.2xlarge
      - c7a.4xlarge
      - c7a.8xlarge
      - c7a.12xlarge
      - c7a.16xlarge
      - c7a.24xlarge
      - c7a.32xlarge
      - c7a.48xlarge
      - m5zn.large
      - m5zn.xlarge
      - m5zn.2xlarge
      - m5zn.3xlarge
      - m5zn.6xlarge
      - m5zn.12xlarge
    Description: Alternate instance type used when capacity strategy is set to SpotWithOnDemandFallback or SpotOnly to increase spot instance availability. Choose different instance types for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3.
  SSHIngressCidrIp:
    Type: String
    Default: ""
    Description: Optional ingress rule allows SSH access from this IP address range usually from a peered VPC (e.g., 172.31.0.0/16).
  VpcCidrBlock:
    Type: String
    Default: 10.0.0.0/16
    Description: The IPv4 network range for the VPC, in CIDR notation (e.g., 10.0.0.0/16).
  VpcSubnetCidrBits:
    Type: Number
    Default: 12
    Description: The number of subnet bits for the CIDR (e.g., a value 8 will create a CIDR with a mask of /24).
    MaxValue: 14
    MinValue: 6
  FlowLogRetentionInDays:
    Type: Number
    Default: 14
    AllowedValues:
      - 0
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1827
      - 3653
    Description: Specifies the number of days you want to retain VPC Flow Log events (set to 0 to disable).
  CloudflareAccountId:
    Type: String
    Description: Cloudflare account ID.
    MinLength: 1
  CloudflareApiToken:
    Type: String
    Description: Cloudflare API token.
    MinLength: 1
    NoEcho: true
  CloudflareAccessKeyId:
    Type: String
    Description: Cloudflare access key ID.
    MinLength: 1
  CloudflareAccessKeySecret:
    Type: String
    Description: Cloudflare access key secret.
    MinLength: 1
    NoEcho: true
  CloudflareQueueFunctionReservedConcurrentExecutions:
    Type: Number
    Default: 0
    Description: Maximum number of execution environment instances for the Lambda function (set to 0 to disable; Check out the CloudWatch metric ConcurrentExecutions to get the maximum concurrent invocations of the past).
    MinValue: 0
  EngineClamAVAutoScalingMinSize:
    Type: Number
    Default: 1
    ConstraintDescription: Must be >= 0
    Description: Minimum number of EC2 instances scanning files (in production, we recommend 2 for high availability).
    MinValue: 0
  EngineClamAVAutoScalingMaxSize:
    Type: Number
    Default: 1
    ConstraintDescription: Must be >= 1
    Description: Maximum number of EC2 instances scanning files (in production, we recommend at least 2 for high availability).
    MinValue: 1
  EngineClamAVEnableCache:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable cache checks for hash sums of scanned files (disable only during performance tests).
  EngineClamAVAdditionalDatabaseUrls:
    Type: String
    Default: ""
    Description: Optional comma-delimited list of ClamAV database files available via http(s).
  EngineSophosAutoScalingMinSize:
    Type: Number
    Default: 1
    ConstraintDescription: Must be >= 0
    Description: Minimum number of EC2 instances scanning files (in production, we recommend 2 for high availability).
    MinValue: 0
  EngineSophosAutoScalingMaxSize:
    Type: Number
    Default: 1
    ConstraintDescription: Must be >= 1
    Description: Maximum number of EC2 instances scanning files (in production, we recommend at least 2 for high availability).
    MinValue: 1
  EngineSophosSophosLiveProtectionCloudLookups:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Live Protection cloud lookups use Sophos' SXL technology and infrastructure to enable the antivirus engine to determine whether a suspicious file is malicious or clean by querying Sophos's extensive database of both malware and clean files. SXL improves detection rates and lowers false-positives. The file hash is shared with Sophos if you enable this feature!
  EngineSophosVolumeSize:
    Type: Number
    Default: 32
    ConstraintDescription: Must be in the range [32-5136]
    Description: The size of the EBS volume, in gibibytes (GiB). You can only scan files that are smaller than VolumeSize-16. Max S3 file size 5120 GiB.
    MaxValue: 5136
    MinValue: 32
  EngineSophosVolumeIops:
    Type: Number
    Default: 3000
    ConstraintDescription: Must be in the range [3000-16000]
    Description: The provisioned I/O operations per second (IOPS)
    MaxValue: 16000
    MinValue: 3000
  EngineSophosVolumeThroughput:
    Type: Number
    Default: 125
    ConstraintDescription: Must be in the range [125-1000]
    Description: The provisioned throughput per second in MiB.
    MaxValue: 1000
    MinValue: 125
  OnScanResultsLambdaFunctionReservedConcurrentExecutions:
    Type: Number
    Default: 0
    Description: Maximum number of execution environment instances for the Lambda function (set to 0 to disable; Check out the CloudWatch metric ConcurrentExecutions to get the maximum concurrent invocations of the past).
    MinValue: 0
  DashboardLambdaFunctionReservedConcurrentExecutions:
    Type: Number
    Default: 0
    Description: Maximum number of execution environment instances for the Lambda function (set to 0 to disable; Check out the CloudWatch metric ConcurrentExecutions to get the maximum concurrent invocations of the past).
    MinValue: 0
Conditions:
  HasReportEventBridge:
    Fn::Equals:
      - Ref: ReportEventBridge
      - "true"
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasInfrastructureAlarmsEmail:
    Fn::Not:
      - Fn::Equals:
          - Ref: InfrastructureAlarmsEmail
          - ""
  HasScanQueueFanOutLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - 0
  HasFindingsTopicFanInLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - 0
  HasCloudflareQueueFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: CloudflareQueueFunctionReservedConcurrentExecutions
          - 0
  HasOnScanResultsLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - 0
  HasDashboardLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: DashboardLambdaFunctionReservedConcurrentExecutions
          - 0
Rules:
  EngineClamAVInstanceTypeAsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVInstanceType
        AssertDescription: Region ap-southeast-5 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVInstanceTypeAsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVInstanceType
        AssertDescription: Region ap-southeast-7 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVInstanceTypeMexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVInstanceType
        AssertDescription: Region mx-central-1 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType1AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType1
        AssertDescription: Region ap-southeast-5 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType1AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType1
        AssertDescription: Region ap-southeast-7 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType1MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType1
        AssertDescription: Region mx-central-1 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType2AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType2
        AssertDescription: Region ap-southeast-5 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType2AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType2
        AssertDescription: Region ap-southeast-7 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType2MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType2
        AssertDescription: Region mx-central-1 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType3AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType3
        AssertDescription: Region ap-southeast-5 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType3AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType3
        AssertDescription: Region ap-southeast-7 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType3MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
            - Ref: EngineClamAVAlternateInstanceType3
        AssertDescription: Region mx-central-1 supports t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge
  EngineClamAVAlternateInstanceType1Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineClamAVAlternateInstanceType1
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineClamAVInstanceType
                  - Ref: EngineClamAVAlternateInstanceType2
                  - Ref: EngineClamAVAlternateInstanceType3
                - Ref: EngineClamAVAlternateInstanceType1
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
  EngineClamAVAlternateInstanceType2Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineClamAVAlternateInstanceType2
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineClamAVInstanceType
                  - Ref: EngineClamAVAlternateInstanceType1
                  - Ref: EngineClamAVAlternateInstanceType3
                - Ref: EngineClamAVAlternateInstanceType2
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
  EngineClamAVAlternateInstanceType3Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineClamAVAlternateInstanceType3
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineClamAVInstanceType
                  - Ref: EngineClamAVAlternateInstanceType1
                  - Ref: EngineClamAVAlternateInstanceType2
                - Ref: EngineClamAVAlternateInstanceType3
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
  EngineSophosInstanceTypeAsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosInstanceType
        AssertDescription: Region ap-southeast-5 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosInstanceTypeAsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosInstanceType
        AssertDescription: Region ap-southeast-7 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosInstanceTypeMexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosInstanceType
        AssertDescription: Region mx-central-1 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType1AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType1
        AssertDescription: Region ap-southeast-5 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType1AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType1
        AssertDescription: Region ap-southeast-7 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType1MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType1
        AssertDescription: Region mx-central-1 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType2AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType2
        AssertDescription: Region ap-southeast-5 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType2AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType2
        AssertDescription: Region ap-southeast-7 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType2MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType2
        AssertDescription: Region mx-central-1 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType3AsiaPacificMalaysia:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-5
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType3
        AssertDescription: Region ap-southeast-5 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType3AsiaPacificThailand:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - ap-southeast-7
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType3
        AssertDescription: Region ap-southeast-7 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType3MexicoCentral:
    RuleCondition:
      Fn::Equals:
        - Ref: AWS::Region
        - mx-central-1
    Assertions:
      - Assert:
          Fn::Contains:
            - - Disabled
              - t3.nano
              - t3.micro
              - t3.small
              - t3.medium
              - t3.large
              - t3.xlarge
              - t3.2xlarge
              - m6i.large
              - m6i.xlarge
              - m6i.2xlarge
              - m6i.4xlarge
              - m6i.8xlarge
              - m6i.12xlarge
              - m6i.16xlarge
              - m6i.24xlarge
              - m6i.32xlarge
              - m7i.large
              - m7i.xlarge
              - m7i.2xlarge
              - m7i.4xlarge
              - m7i.8xlarge
              - m7i.12xlarge
              - m7i.16xlarge
              - m7i.24xlarge
              - m7i.48xlarge
              - c6i.large
              - c6i.xlarge
              - c6i.2xlarge
              - c6i.4xlarge
              - c6i.8xlarge
              - c6i.12xlarge
              - c6i.16xlarge
              - c6i.24xlarge
              - c6i.32xlarge
              - c7i.large
              - c7i.xlarge
              - c7i.2xlarge
              - c7i.4xlarge
              - c7i.8xlarge
              - c7i.12xlarge
              - c7i.16xlarge
              - c7i.24xlarge
              - c7i.48xlarge
            - Ref: EngineSophosAlternateInstanceType3
        AssertDescription: Region mx-central-1 supports t3.nano, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge, m6i.large, m6i.xlarge, m6i.2xlarge, m6i.4xlarge, m6i.8xlarge, m6i.12xlarge, m6i.16xlarge, m6i.24xlarge, m6i.32xlarge, m7i.large, m7i.xlarge, m7i.2xlarge, m7i.4xlarge, m7i.8xlarge, m7i.12xlarge, m7i.16xlarge, m7i.24xlarge, m7i.48xlarge, c6i.large, c6i.xlarge, c6i.2xlarge, c6i.4xlarge, c6i.8xlarge, c6i.12xlarge, c6i.16xlarge, c6i.24xlarge, c6i.32xlarge, c7i.large, c7i.xlarge, c7i.2xlarge, c7i.4xlarge, c7i.8xlarge, c7i.12xlarge, c7i.16xlarge, c7i.24xlarge, c7i.48xlarge
  EngineSophosAlternateInstanceType1Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineSophosAlternateInstanceType1
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineSophosInstanceType
                  - Ref: EngineSophosAlternateInstanceType2
                  - Ref: EngineSophosAlternateInstanceType3
                - Ref: EngineSophosAlternateInstanceType1
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
  EngineSophosAlternateInstanceType2Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineSophosAlternateInstanceType2
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineSophosInstanceType
                  - Ref: EngineSophosAlternateInstanceType1
                  - Ref: EngineSophosAlternateInstanceType3
                - Ref: EngineSophosAlternateInstanceType2
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
  EngineSophosAlternateInstanceType3Unique:
    RuleCondition:
      Fn::Not:
        - Fn::Equals:
            - Ref: EngineSophosAlternateInstanceType3
            - Disabled
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - Ref: EngineSophosInstanceType
                  - Ref: EngineSophosAlternateInstanceType1
                  - Ref: EngineSophosAlternateInstanceType2
                - Ref: EngineSophosAlternateInstanceType3
        AssertDescription: Do not use the same instance type for InstanceType, AlternateInstanceType1, AlternateInstanceType2, and AlternateInstanceType3
Resources:
  InfrastructureAlarmsTopic:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: bucketav:cloudformation:logical-id
          Value: InfrastructureAlarmsTopic
        - Key: bucketav:cloudformation:stack-id
          Value:
            Ref: AWS::StackId
        - Key: bucketav:cloudformation:stack-name
          Value:
            Ref: AWS::StackName
  InfrastructureAlarmsSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Ref: InfrastructureAlarmsEmail
      Protocol: email
      TopicArn:
        Ref: InfrastructureAlarmsTopic
    Condition: HasInfrastructureAlarmsEmail
  DeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
      Tags:
        - Key: bucketav:cloudformation:logical-id
          Value: DeadLetterQueue
        - Key: bucketav:cloudformation:stack-id
          Value:
            Ref: AWS::StackId
        - Key: bucketav:cloudformation:stack-name
          Value:
            Ref: AWS::StackName
  ScanQueue:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds:
        Ref: ScanDelayInSeconds
      MessageRetentionPeriod: 1209600
      RedrivePolicy:
        deadLetterTargetArn:
          Fn::GetAtt:
            - DeadLetterQueue
            - Arn
        maxReceiveCount: 3
      SqsManagedSseEnabled: true
      Tags:
        - Key: bucketav:cloudformation:logical-id
          Value: ScanQueue
        - Key: bucketav:cloudformation:stack-id
          Value:
            Ref: AWS::StackId
        - Key: bucketav:cloudformation:stack-name
          Value:
            Ref: AWS::StackName
      VisibilityTimeout: 300
  DeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: Dead letter queue contains messages. Some scan jobs were dropped. Please follow https://bucketav.com/help/operations/monitoring-alerting.html#deadletterqueuealarm
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - DeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 60
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  ScanQueueOldMessagesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: Scan queue contains messages older than 12 hours. Please follow https://bucketav.com/help/operations/monitoring-alerting.html#scanqueueoldmessagesalarm
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - ScanQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateAgeOfOldestMessage
      Namespace: AWS/SQS
      Period: 60
      Statistic: Maximum
      Threshold: 43200
      TreatMissingData: notBreaching
  ServiceDiscoveryScanQueueArn:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /ScanQueueArn
      Type: String
      Value:
        Fn::GetAtt:
          - ScanQueue
          - Arn
  ServiceDiscoveryScanQueueUrl:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /ScanQueueUrl
      Type: String
      Value:
        Ref: ScanQueue
  CloudflareApiTokenSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      SecretString:
        Ref: CloudflareApiToken
  CloudflareAccessKeySecretSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      SecretString:
        Ref: CloudflareAccessKeySecret
  ServiceDiscoveryCloudflareAccessKeyId:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /CloudflareAccessKeyId
      Type: String
      Value:
        Ref: CloudflareAccessKeyId
  ServiceDiscoveryCloudflareR2EndpointHostname:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /CloudflareR2EndpointHostname
      Type: String
      Value:
        Fn::Join:
          - ""
          - - Ref: CloudflareAccountId
            - .r2.cloudflarestorage.com
  ServiceDiscoveryCloudflareAccessKeySecretArn:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /CloudflareAccessKeySecretArn
      Type: String
      Value:
        Ref: CloudflareAccessKeySecretSecret
  CloudflareUser:
    Type: AWS::IAM::User
    Properties:
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - ScanQueue
                    - Arn
          PolicyName: sqs
  CloudflareAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName:
        Ref: CloudflareUser
  CloudflareQueueRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  Ref: CloudflareApiTokenSecret
          PolicyName: core
  CloudflareQueueFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          var __defProp = Object.defineProperty;
          var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
          var __getOwnPropNames = Object.getOwnPropertyNames;
          var __hasOwnProp = Object.prototype.hasOwnProperty;
          var __export = (target, all) => {
            for (var name in all)
              __defProp(target, name, { get: all[name], enumerable: true });
          };
          var __copyProps = (to, from, except, desc) => {
            if (from && typeof from === "object" || typeof from === "function") {
              for (let key of __getOwnPropNames(from))
                if (!__hasOwnProp.call(to, key) && key !== except)
                  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
            }
            return to;
          };
          var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

          // lambda/cloudflare-queue.js
          var cloudflare_queue_exports = {};
          __export(cloudflare_queue_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(cloudflare_queue_exports);
          var import_client_secrets_manager2 = require("@aws-sdk/client-secrets-manager");

          // lambda/lib.js
          var import_client_cloudformation = require("@aws-sdk/client-cloudformation");
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_sns = require("@aws-sdk/client-sns");
          var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var import_client_s32 = require("@aws-sdk/client-s3");
          var import_client_ssm = require("@aws-sdk/client-ssm");
          var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
          var import_client_organizations = require("@aws-sdk/client-organizations");
          var import_credential_providers = require("@aws-sdk/credential-providers");

          // lambda/node_modules/@smithy/core/dist-es/pagination/createPaginator.js
          var makePagedClientRequest = async (CommandCtor, client, input, withCommand = (_) => _, ...args) => {
            let command = new CommandCtor(input);
            command = withCommand(command) ?? command;
            return await client.send(command, ...args);
          };
          function createPaginator(ClientCtor, CommandCtor, inputTokenName, outputTokenName, pageSizeTokenName) {
            return async function* paginateOperation(config, input, ...additionalArguments) {
              const _input = input;
              let token = config.startingToken ?? _input[inputTokenName];
              let hasNext = true;
              let page;
              while (hasNext) {
                _input[inputTokenName] = token;
                if (pageSizeTokenName) {
                  _input[pageSizeTokenName] = _input[pageSizeTokenName] ?? config.pageSize;
                }
                if (config.client instanceof ClientCtor) {
                  page = await makePagedClientRequest(CommandCtor, config.client, input, config.withCommand, ...additionalArguments);
                } else {
                  throw new Error(`Invalid client, expected instance of ${ClientCtor.name}`);
                }
                yield page;
                const prevToken = token;
                token = get(page, outputTokenName);
                hasNext = !!(token && (!config.stopOnSameToken || token !== prevToken));
              }
              return void 0;
            };
          }
          var get = (fromObject, path) => {
            let cursor = fromObject;
            const pathComponents = path.split(".");
            for (const step of pathComponents) {
              if (!cursor || typeof cursor !== "object") {
                return void 0;
              }
              cursor = cursor[step];
            }
            return cursor;
          };

          // lambda/fetch-retry.js
          function fetch_retry_default(fetch3, defaults) {
            defaults = defaults || {};
            if (typeof fetch3 !== "function") {
              throw new ArgumentError("fetch must be a function");
            }
            if (typeof defaults !== "object") {
              throw new ArgumentError("defaults must be an object");
            }
            if (defaults.retries !== void 0 && !isPositiveInteger(defaults.retries)) {
              throw new ArgumentError("retries must be a positive integer");
            }
            if (defaults.retryDelay !== void 0 && !isPositiveInteger(defaults.retryDelay) && typeof defaults.retryDelay !== "function") {
              throw new ArgumentError("retryDelay must be a positive integer or a function returning a positive integer");
            }
            if (defaults.retryOn !== void 0 && !Array.isArray(defaults.retryOn) && typeof defaults.retryOn !== "function") {
              throw new ArgumentError("retryOn property expects an array or function");
            }
            var baseDefaults = {
              retries: 3,
              retryDelay: 1e3,
              retryOn: []
            };
            defaults = Object.assign(baseDefaults, defaults);
            return function fetchRetry(input, init) {
              var retries = defaults.retries;
              var retryDelay = defaults.retryDelay;
              var retryOn = defaults.retryOn;
              if (init && init.retries !== void 0) {
                if (isPositiveInteger(init.retries)) {
                  retries = init.retries;
                } else {
                  throw new ArgumentError("retries must be a positive integer");
                }
              }
              if (init && init.retryDelay !== void 0) {
                if (isPositiveInteger(init.retryDelay) || typeof init.retryDelay === "function") {
                  retryDelay = init.retryDelay;
                } else {
                  throw new ArgumentError("retryDelay must be a positive integer or a function returning a positive integer");
                }
              }
              if (init && init.retryOn) {
                if (Array.isArray(init.retryOn) || typeof init.retryOn === "function") {
                  retryOn = init.retryOn;
                } else {
                  throw new ArgumentError("retryOn property expects an array or function");
                }
              }
              return new Promise(function(resolve, reject) {
                var wrappedFetch = function(attempt) {
                  var _input = typeof Request !== "undefined" && input instanceof Request ? input.clone() : input;
                  fetch3(_input, init).then(function(response) {
                    if (Array.isArray(retryOn) && retryOn.indexOf(response.status) === -1) {
                      resolve(response);
                    } else if (typeof retryOn === "function") {
                      try {
                        return Promise.resolve(retryOn(attempt, null, response)).then(function(retryOnResponse) {
                          if (retryOnResponse) {
                            retry(attempt, null, response);
                          } else {
                            resolve(response);
                          }
                        }).catch(reject);
                      } catch (error) {
                        reject(error);
                      }
                    } else {
                      if (attempt < retries) {
                        retry(attempt, null, response);
                      } else {
                        resolve(response);
                      }
                    }
                  }).catch(function(error) {
                    if (typeof retryOn === "function") {
                      try {
                        Promise.resolve(retryOn(attempt, error, null)).then(function(retryOnResponse) {
                          if (retryOnResponse) {
                            retry(attempt, error, null);
                          } else {
                            reject(error);
                          }
                        }).catch(function(error2) {
                          reject(error2);
                        });
                      } catch (error2) {
                        reject(error2);
                      }
                    } else if (attempt < retries) {
                      retry(attempt, error, null);
                    } else {
                      reject(error);
                    }
                  });
                };
                function retry(attempt, error, response) {
                  var delay = typeof retryDelay === "function" ? retryDelay(attempt, error, response) : retryDelay;
                  setTimeout(function() {
                    wrappedFetch(++attempt);
                  }, delay);
                }
                wrappedFetch(0);
              });
            };
          }
          function isPositiveInteger(value) {
            return Number.isInteger(value) && value >= 0;
          }
          function ArgumentError(message) {
            this.name = "ArgumentError";
            this.message = message;
          }

          // lambda/lib-cloudflare.js
          var BASIC_WAIT_TIME_IN_MILLIS = 300;
          var MAX_WAIT_TIME_IN_MILLIS = 5e3;
          var DEFAULT_TIMEOUT_IN_MILLIS = 1e4;
          async function fetch2(url, options) {
            options.signal = AbortSignal.timeout(DEFAULT_TIMEOUT_IN_MILLIS);
            return fetch_retry_default(global.fetch, {
              retryDelay: (attempt) => {
                const delay = Math.floor(Math.random() * Math.min(MAX_WAIT_TIME_IN_MILLIS, BASIC_WAIT_TIME_IN_MILLIS * Math.pow(2, attempt)));
                options.signal = AbortSignal.timeout(DEFAULT_TIMEOUT_IN_MILLIS + delay);
                return delay;
              },
              retryOn: (attempt, error, response) => {
                if (attempt >= 3) {
                  return false;
                }
                if (error || response.status === 429 || response.status >= 500) {
                  return true;
                }
              }
            })(url, options);
          }
          async function createQueueConsumer(apiToken, accountId, queueId, scriptName, dlqName) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues/${queueId}/consumers`, {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiToken}`
              },
              body: JSON.stringify({
                type: "worker",
                dead_letter_queue: dlqName,
                script_name: scriptName,
                settings: {
                  batch_size: 10,
                  // SQS supports no more than 10 in SendMessageBatch
                  max_retries: 3,
                  max_wait_time_ms: 1e3,
                  retry_delay: 1
                  // in seconds
                }
              })
            });
            if (response.status === 400) {
              const body2 = await response.json();
              if (body2.errors.some((e) => e.code === 11004)) {
                return;
              } else {
                console.log("response url", response.url);
                console.log("response status", response.status);
                console.log("response", body2);
                throw new Error("unexpected error code");
              }
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }
          async function listQueueConsumers(apiToken, accountId, queueId) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues/${queueId}/consumers`, {
              method: "GET",
              headers: {
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return [];
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
            if (body.result_info?.total_pages > 1) {
              throw new Error("implement paging");
            }
            return body.result;
          }
          async function deleteQueueConsumer(apiToken, accountId, queueId, consumerId) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues/${queueId}/consumers/${consumerId}`, {
              method: "DELETE",
              headers: {
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return;
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }
          async function listQueues(apiToken, accountId) {
            const queues = [];
            let page = 1;
            while (true) {
              const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues?per_page=100&page=${page}`, {
                method: "GET",
                headers: {
                  Authorization: `Bearer ${apiToken}`
                }
              });
              if (response.status !== 200) {
                console.log("response url", response.url);
                console.log("response status", response.status);
                console.log("response", await response.text());
                throw new Error("unexpected status code");
              }
              const body = await response.json();
              if (body.success !== true) {
                console.log("body", body);
                throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
              }
              queues.push(...body.result);
              if (page === body.result_info.total_pages) {
                break;
              } else {
                page++;
              }
            }
            return queues;
          }
          async function createQueue(apiToken, accountId, queueName) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues`, {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiToken}`
              },
              body: JSON.stringify({
                queue_name: queueName
              })
            });
            if (response.status === 409) {
              const body2 = await response.json();
              if (body2.errors.some((e) => e.code === 11009)) {
                const queues = await listQueues(apiToken, accountId);
                const queue = queues.find((queue2) => queue2.queue_name === queueName);
                if (queue === void 0) {
                  throw new Error("Queue not found after error 11009");
                }
                return queue.queue_id;
              } else {
                console.log("response url", response.url);
                console.log("response status", response.status);
                console.log("response", body2);
                throw new Error("unexpected error code");
              }
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            } else {
              const queueId = body.result.queue_id;
              return queueId;
            }
          }
          async function deleteQueue(apiToken, accountId, queueId) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/queues/${queueId}`, {
              method: "DELETE",
              headers: {
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return;
            }
            if (response.status === 400) {
              const body2 = await response.json();
              if (body2.errors.some((e) => e.code === 11017)) {
                return;
              } else {
                console.log("response url", response.url);
                console.log("response status", response.status);
                console.log("response", body2);
                throw new Error("unexpected error code");
              }
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }
          async function putWorker(apiToken, accountId, scriptName, scriptCode, environmentVariables, secrets) {
            const data = new FormData();
            data.append("worker.js", new Blob([scriptCode], { type: "application/javascript+module" }), "worker.js");
            data.append("metadata", JSON.stringify({
              bindings: [
                ...Object.keys(environmentVariables).map((key) => ({
                  name: key,
                  text: environmentVariables[key],
                  type: "plain_text"
                })),
                ...Object.keys(secrets).map((key) => ({
                  name: key,
                  text: secrets[key],
                  type: "secret_text"
                }))
              ],
              compatibility_date: "2024-07-08",
              main_module: "worker.js"
            }));
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`, {
              method: "PUT",
              headers: {
                Authorization: `Bearer ${apiToken}`
              },
              body: data
            });
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }
          async function deleteWorker(apiToken, accountId, scriptName) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`, {
              method: "DELETE",
              headers: {
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return;
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }

          // lambda/lib.js
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function fetchCloudflareApiToken(secretsmanager2, cloudflareApiTokenSecretArn) {
            const { SecretString: cloudflareApiToken } = await secretsmanager2.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: cloudflareApiTokenSecretArn }));
            return cloudflareApiToken;
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;
          async function cfnCustomResourceSuccess(event, physicalResourceId, optionalData) {
            const response = await fetch(event.ResponseURL, {
              method: "PUT",
              headers: {
                "Content-Type": "application/json"
              },
              body: JSON.stringify({
                Status: "SUCCESS",
                PhysicalResourceId: physicalResourceId,
                StackId: event.StackId,
                RequestId: event.RequestId,
                LogicalResourceId: event.LogicalResourceId,
                ...optionalData !== void 0 && { Data: optionalData }
              })
            });
            if (response.status !== 200) {
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
          }
          async function cfnCustomResourceFailed(event, physicalResourceId, optionalReason) {
            const response = await fetch(event.ResponseURL, {
              method: "PUT",
              headers: {
                "Content-Type": "application/json"
              },
              body: JSON.stringify({
                Status: "FAILED",
                ...optionalReason !== void 0 && { Reason: optionalReason },
                PhysicalResourceId: physicalResourceId === void 0 || physicalResourceId === null ? event.LogicalResourceId : physicalResourceId,
                // physicalResourceId might not be available if create fails
                StackId: event.StackId,
                RequestId: event.RequestId,
                LogicalResourceId: event.LogicalResourceId
              })
            });
            if (response.status !== 200) {
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
          }

          // lambda/cloudflare-queue.js
          var secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
          var CLOUDFLARE_API_TOKEN_SECRET_ARN = process.env.CLOUDFLARE_API_TOKEN_SECRET_ARN;
          function generateQueueName(stackId) {
            const s = stackId.split("/");
            return s[1].substr(0, 63).toLowerCase();
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const apiToken = await fetchCloudflareApiToken(secretsmanager, CLOUDFLARE_API_TOKEN_SECRET_ARN);
            if (event.RequestType === "Create") {
              try {
                const queueName = generateQueueName(event.StackId);
                const deadLetterQueueName = `${queueName}-dlq`;
                const scriptName = queueName;
                const queueId = await createQueue(apiToken, CLOUDFLARE_ACCOUNT_ID, queueName);
                const deadLetterQueueId = await createQueue(apiToken, CLOUDFLARE_ACCOUNT_ID, deadLetterQueueName);
                const physicalResourceId = `${queueId}:${deadLetterQueueId}`;
                await putWorker(apiToken, CLOUDFLARE_ACCOUNT_ID, scriptName, event.ResourceProperties.ScriptCode, {
                  AWS_SQS_SCAN_QUEUE_URL: event.ResourceProperties.ScanQueueUrl,
                  AWS_REGION: event.ResourceProperties.AwsRegion,
                  AWS_ACCESS_KEY_ID: event.ResourceProperties.AwsAccessKeyId
                }, {
                  AWS_SECRET_ACCESS_KEY: event.ResourceProperties.AwsSecretAccessKey
                });
                await createQueueConsumer(apiToken, CLOUDFLARE_ACCOUNT_ID, queueId, scriptName, deadLetterQueueName);
                await cfnCustomResourceSuccess(event, physicalResourceId, {
                  QueueId: queueId,
                  QueueName: queueName,
                  DeadLetterQueueId: deadLetterQueueId,
                  DeadLetterQueueName: deadLetterQueueName
                });
              } catch (err) {
                await cfnCustomResourceFailed(event, void 0, err.message);
              }
            } else if (event.RequestType === "Update") {
              try {
                const [queueId, deadLetterQueueId] = event.PhysicalResourceId.split(":");
                const queueName = generateQueueName(event.StackId);
                const deadLetterQueueName = `${queueName}-dlq`;
                const scriptName = queueName;
                await putWorker(apiToken, CLOUDFLARE_ACCOUNT_ID, scriptName, event.ResourceProperties.ScriptCode, {
                  AWS_SQS_SCAN_QUEUE_URL: event.ResourceProperties.ScanQueueUrl,
                  AWS_REGION: event.ResourceProperties.AwsRegion,
                  AWS_ACCESS_KEY_ID: event.ResourceProperties.AwsAccessKeyId
                }, {
                  AWS_SECRET_ACCESS_KEY: event.ResourceProperties.AwsSecretAccessKey
                });
                await cfnCustomResourceSuccess(event, event.PhysicalResourceId, {
                  QueueId: queueId,
                  QueueName: queueName,
                  DeadLetterQueueId: deadLetterQueueId,
                  DeadLetterQueueName: deadLetterQueueName
                });
              } catch (err) {
                await cfnCustomResourceFailed(event, event.PhysicalResourceId, err.message);
                console.log(err);
              }
            } else if (event.RequestType === "Delete") {
              if (event.PhysicalResourceId.split(":").length !== 2) {
                return await cfnCustomResourceSuccess(event, event.PhysicalResourceId);
              }
              try {
                const [queueId, deadLetterQueueId] = event.PhysicalResourceId.split(":");
                const queueName = generateQueueName(event.StackId);
                const deadLetterQueueName = `${queueName}-dlq`;
                const scriptName = queueName;
                const consumers = await listQueueConsumers(apiToken, CLOUDFLARE_ACCOUNT_ID, queueId);
                const consumer = consumers.find((consumer2) => consumer2.script === scriptName);
                if (consumer !== void 0) {
                  await deleteQueueConsumer(apiToken, CLOUDFLARE_ACCOUNT_ID, queueId, consumer.consumer_id);
                }
                await deleteWorker(apiToken, CLOUDFLARE_ACCOUNT_ID, scriptName);
                await deleteQueue(apiToken, CLOUDFLARE_ACCOUNT_ID, queueId);
                await deleteQueue(apiToken, CLOUDFLARE_ACCOUNT_ID, deadLetterQueueId);
                await cfnCustomResourceSuccess(event, queueId, {
                  QueueId: queueId,
                  QueueName: queueName,
                  DeadLetterQueueId: deadLetterQueueId,
                  DeadLetterQueueName: deadLetterQueueName
                });
              } catch (err) {
                await cfnCustomResourceFailed(event, event.PhysicalResourceId, err.message);
              }
            } else {
              throw new Error("unsupported request type");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          CLOUDFLARE_ACCOUNT_ID:
            Ref: CloudflareAccountId
          CLOUDFLARE_API_TOKEN_SECRET_ARN:
            Ref: CloudflareApiTokenSecret
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasCloudflareQueueFunctionReservedConcurrentExecutions
          - Ref: CloudflareQueueFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - CloudflareQueueRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 300
      VpcConfig:
        Ref: AWS::NoValue
  CloudflareQueueLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: CloudflareQueueFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  CloudflareQueuePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - CloudflareQueueLogGroup
                - Arn
      PolicyName: lambda
      Roles:
        - Ref: CloudflareQueueRole
  CloudflareQueue:
    Type: Custom::CloudflareQueue
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - CloudflareQueueFunction
          - Arn
      ServiceTimeout: "300"
      Version: 2.0.0
      ScriptCode: |
        // workers/node_modules/aws4fetch/dist/aws4fetch.esm.mjs
        var encoder = new TextEncoder();
        var HOST_SERVICES = {
          appstream2: "appstream",
          cloudhsmv2: "cloudhsm",
          email: "ses",
          marketplace: "aws-marketplace",
          mobile: "AWSMobileHubService",
          pinpoint: "mobiletargeting",
          queue: "sqs",
          "git-codecommit": "codecommit",
          "mturk-requester-sandbox": "mturk-requester",
          "personalize-runtime": "personalize"
        };
        var UNSIGNABLE_HEADERS = /* @__PURE__ */ new Set([
          "authorization",
          "content-type",
          "content-length",
          "user-agent",
          "presigned-expires",
          "expect",
          "x-amzn-trace-id",
          "range",
          "connection"
        ]);
        var AwsClient = class {
          constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) {
            if (accessKeyId == null) throw new TypeError("accessKeyId is a required option");
            if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option");
            this.accessKeyId = accessKeyId;
            this.secretAccessKey = secretAccessKey;
            this.sessionToken = sessionToken;
            this.service = service;
            this.region = region;
            this.cache = cache || /* @__PURE__ */ new Map();
            this.retries = retries != null ? retries : 10;
            this.initRetryMs = initRetryMs || 50;
          }
          async sign(input, init) {
            if (input instanceof Request) {
              const { method, url, headers, body } = input;
              init = Object.assign({ method, url, headers }, init);
              if (init.body == null && headers.has("Content-Type")) {
                init.body = body != null && headers.has("X-Amz-Content-Sha256") ? body : await input.clone().arrayBuffer();
              }
              input = url;
            }
            const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws));
            const signed = Object.assign({}, init, await signer.sign());
            delete signed.aws;
            try {
              return new Request(signed.url.toString(), signed);
            } catch (e) {
              if (e instanceof TypeError) {
                return new Request(signed.url.toString(), Object.assign({ duplex: "half" }, signed));
              }
              throw e;
            }
          }
          async fetch(input, init) {
            for (let i = 0; i <= this.retries; i++) {
              const fetched = fetch(await this.sign(input, init));
              if (i === this.retries) {
                return fetched;
              }
              const res = await fetched;
              if (res.status < 500 && res.status !== 429) {
                return res;
              }
              await new Promise((resolve) => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i)));
            }
            throw new Error("An unknown error occurred, ensure retries is not negative");
          }
        };
        var AwsV4Signer = class {
          constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) {
            if (url == null) throw new TypeError("url is a required option");
            if (accessKeyId == null) throw new TypeError("accessKeyId is a required option");
            if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option");
            this.method = method || (body ? "POST" : "GET");
            this.url = new URL(url);
            this.headers = new Headers(headers || {});
            this.body = body;
            this.accessKeyId = accessKeyId;
            this.secretAccessKey = secretAccessKey;
            this.sessionToken = sessionToken;
            let guessedService, guessedRegion;
            if (!service || !region) {
              [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers);
            }
            this.service = service || guessedService || "";
            this.region = region || guessedRegion || "us-east-1";
            this.cache = cache || /* @__PURE__ */ new Map();
            this.datetime = datetime || (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
            this.signQuery = signQuery;
            this.appendSessionToken = appendSessionToken || this.service === "iotdevicegateway";
            this.headers.delete("Host");
            if (this.service === "s3" && !this.signQuery && !this.headers.has("X-Amz-Content-Sha256")) {
              this.headers.set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD");
            }
            const params = this.signQuery ? this.url.searchParams : this.headers;
            params.set("X-Amz-Date", this.datetime);
            if (this.sessionToken && !this.appendSessionToken) {
              params.set("X-Amz-Security-Token", this.sessionToken);
            }
            this.signableHeaders = ["host", ...this.headers.keys()].filter((header) => allHeaders || !UNSIGNABLE_HEADERS.has(header)).sort();
            this.signedHeaders = this.signableHeaders.join(";");
            this.canonicalHeaders = this.signableHeaders.map((header) => header + ":" + (header === "host" ? this.url.host : (this.headers.get(header) || "").replace(/\s+/g, " "))).join("\n");
            this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, "aws4_request"].join("/");
            if (this.signQuery) {
              if (this.service === "s3" && !params.has("X-Amz-Expires")) {
                params.set("X-Amz-Expires", "86400");
              }
              params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
              params.set("X-Amz-Credential", this.accessKeyId + "/" + this.credentialString);
              params.set("X-Amz-SignedHeaders", this.signedHeaders);
            }
            if (this.service === "s3") {
              try {
                this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, " "));
              } catch (e) {
                this.encodedPath = this.url.pathname;
              }
            } else {
              this.encodedPath = this.url.pathname.replace(/\/+/g, "/");
            }
            if (!singleEncode) {
              this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, "/");
            }
            this.encodedPath = encodeRfc3986(this.encodedPath);
            const seenKeys = /* @__PURE__ */ new Set();
            this.encodedSearch = [...this.url.searchParams].filter(([k]) => {
              if (!k) return false;
              if (this.service === "s3") {
                if (seenKeys.has(k)) return false;
                seenKeys.add(k);
              }
              return true;
            }).map((pair) => pair.map((p) => encodeRfc3986(encodeURIComponent(p)))).sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0).map((pair) => pair.join("=")).join("&");
          }
          async sign() {
            if (this.signQuery) {
              this.url.searchParams.set("X-Amz-Signature", await this.signature());
              if (this.sessionToken && this.appendSessionToken) {
                this.url.searchParams.set("X-Amz-Security-Token", this.sessionToken);
              }
            } else {
              this.headers.set("Authorization", await this.authHeader());
            }
            return {
              method: this.method,
              url: this.url,
              headers: this.headers,
              body: this.body
            };
          }
          async authHeader() {
            return [
              "AWS4-HMAC-SHA256 Credential=" + this.accessKeyId + "/" + this.credentialString,
              "SignedHeaders=" + this.signedHeaders,
              "Signature=" + await this.signature()
            ].join(", ");
          }
          async signature() {
            const date = this.datetime.slice(0, 8);
            const cacheKey = [this.secretAccessKey, date, this.region, this.service].join();
            let kCredentials = this.cache.get(cacheKey);
            if (!kCredentials) {
              const kDate = await hmac("AWS4" + this.secretAccessKey, date);
              const kRegion = await hmac(kDate, this.region);
              const kService = await hmac(kRegion, this.service);
              kCredentials = await hmac(kService, "aws4_request");
              this.cache.set(cacheKey, kCredentials);
            }
            return buf2hex(await hmac(kCredentials, await this.stringToSign()));
          }
          async stringToSign() {
            return [
              "AWS4-HMAC-SHA256",
              this.datetime,
              this.credentialString,
              buf2hex(await hash(await this.canonicalString()))
            ].join("\n");
          }
          async canonicalString() {
            return [
              this.method.toUpperCase(),
              this.encodedPath,
              this.encodedSearch,
              this.canonicalHeaders + "\n",
              this.signedHeaders,
              await this.hexBodyHash()
            ].join("\n");
          }
          async hexBodyHash() {
            let hashHeader = this.headers.get("X-Amz-Content-Sha256") || (this.service === "s3" && this.signQuery ? "UNSIGNED-PAYLOAD" : null);
            if (hashHeader == null) {
              if (this.body && typeof this.body !== "string" && !("byteLength" in this.body)) {
                throw new Error("body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header");
              }
              hashHeader = buf2hex(await hash(this.body || ""));
            }
            return hashHeader;
          }
        };
        async function hmac(key, string) {
          const cryptoKey = await crypto.subtle.importKey(
            "raw",
            typeof key === "string" ? encoder.encode(key) : key,
            { name: "HMAC", hash: { name: "SHA-256" } },
            false,
            ["sign"]
          );
          return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(string));
        }
        async function hash(content) {
          return crypto.subtle.digest("SHA-256", typeof content === "string" ? encoder.encode(content) : content);
        }
        var HEX_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
        function buf2hex(arrayBuffer) {
          const buffer = new Uint8Array(arrayBuffer);
          let out = "";
          for (let idx = 0; idx < buffer.length; idx++) {
            const n = buffer[idx];
            out += HEX_CHARS[n >>> 4 & 15];
            out += HEX_CHARS[n & 15];
          }
          return out;
        }
        function encodeRfc3986(urlEncodedStr) {
          return urlEncodedStr.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
        }
        function guessServiceRegion(url, headers) {
          const { hostname, pathname } = url;
          if (hostname.endsWith(".on.aws")) {
            const match2 = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/);
            return match2 != null ? ["lambda", match2[1] || ""] : ["", ""];
          }
          if (hostname.endsWith(".r2.cloudflarestorage.com")) {
            return ["s3", "auto"];
          }
          if (hostname.endsWith(".backblazeb2.com")) {
            const match2 = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/);
            return match2 != null ? ["s3", match2[1] || ""] : ["", ""];
          }
          const match = hostname.replace("dualstack.", "").match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/);
          let service = match && match[1] || "";
          let region = match && match[2];
          if (region === "us-gov") {
            region = "us-gov-west-1";
          } else if (region === "s3" || region === "s3-accelerate") {
            region = "us-east-1";
            service = "s3";
          } else if (service === "iot") {
            if (hostname.startsWith("iot.")) {
              service = "execute-api";
            } else if (hostname.startsWith("data.jobs.iot.")) {
              service = "iot-jobs-data";
            } else {
              service = pathname === "/mqtt" ? "iotdevicegateway" : "iotdata";
            }
          } else if (service === "autoscaling") {
            const targetPrefix = (headers.get("X-Amz-Target") || "").split(".")[0];
            if (targetPrefix === "AnyScaleFrontendService") {
              service = "application-autoscaling";
            } else if (targetPrefix === "AnyScaleScalingPlannerFrontendService") {
              service = "autoscaling-plans";
            }
          } else if (region == null && service.startsWith("s3-")) {
            region = service.slice(3).replace(/^fips-|^external-1/, "");
            service = "s3";
          } else if (service.endsWith("-fips")) {
            service = service.slice(0, -5);
          } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) {
            [service, region] = [region, service];
          }
          return [HOST_SERVICES[service] || service, region || ""];
        }

        // workers/queue.js
        var queue_default = {
          async queue(batch, env) {
            const aws = new AwsClient({ accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY });
            const response = await aws.fetch(`https://sqs.${env.AWS_REGION}.amazonaws.com/`, {
              method: "POST",
              headers: {
                "X-Amz-Target": "AmazonSQS.SendMessageBatch",
                "Content-Type": "application/x-amz-json-1.0"
              },
              body: JSON.stringify({
                Entries: batch.messages.map((message, i) => ({
                  Id: `id${i}`,
                  MessageBody: JSON.stringify(message.body)
                })),
                QueueUrl: env.AWS_SQS_SCAN_QUEUE_URL
              })
            });
            if (response.status !== 200) {
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body?.Failed?.length > 0) {
              body.Failed.forEach((failure) => {
                console.log(`batch ${failure.Id} failed with ${failure.Code} (${failure.SenderFault}): ${failure.Message}`);
              });
              throw new Error("SQS batch failed");
            }
          }
        };
        export {
          queue_default as default
        };
        /*! Bundled license information:

        aws4fetch/dist/aws4fetch.esm.mjs:
          (**
           * @license MIT <https://opensource.org/licenses/MIT>
           * @copyright Michael Hart 2024
           *)
        */
      ScanQueueUrl:
        Ref: ScanQueue
      AwsRegion:
        Ref: AWS::Region
      AwsAccessKeyId:
        Ref: CloudflareAccessKey
      AwsSecretAccessKey:
        Fn::GetAtt:
          - CloudflareAccessKey
          - SecretAccessKey
    DependsOn:
      - CloudflareQueuePolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  EngineClamAV:
    Type: AWS::CloudFormation::Stack
    Properties:
      Parameters:
        SSHIngressCidrIp:
          Ref: SSHIngressCidrIp
        VpcCidrBlock:
          Ref: VpcCidrBlock
        VpcSubnetCidrBits:
          Ref: VpcSubnetCidrBits
        FlowLogRetentionInDays:
          Ref: FlowLogRetentionInDays
        KeyName:
          Ref: KeyName
        InfrastructureAlarmsEmail:
          Ref: InfrastructureAlarmsEmail
        DeleteInfectedFiles: "false"
        ReportEventBridge: "false"
        ScanDelayInSeconds: "0"
        EnableCache:
          Ref: EngineClamAVEnableCache
        AdditionalDatabaseUrls:
          Ref: EngineClamAVAdditionalDatabaseUrls
        AutoScalingMinSize:
          Ref: EngineClamAVAutoScalingMinSize
        AutoScalingMaxSize:
          Ref: EngineClamAVAutoScalingMaxSize
        CapacityStrategy:
          Ref: CapacityStrategy
        InstanceType:
          Ref: EngineClamAVInstanceType
        AlternateInstanceType1:
          Ref: EngineClamAVAlternateInstanceType3
        LogsRetentionInDays:
          Ref: LogsRetentionInDays
        SystemsManagerAccess:
          Ref: SystemsManagerAccess
        ManagedPolicyArns:
          Ref: ManagedPolicyArns
        PermissionsBoundary:
          Ref: PermissionsBoundary
        InternalMultiStackName:
          Ref: AWS::StackName
        CloudflareAccountId:
          Ref: CloudflareAccountId
        CloudflareApiToken:
          Ref: CloudflareApiToken
        CloudflareAccessKeyId:
          Ref: CloudflareAccessKeyId
        CloudflareAccessKeySecret:
          Ref: CloudflareAccessKeySecret
      TemplateURL: https://awsmp-cft-211125678794-1707910187780.s3.us-east-1.amazonaws.com/2d56fcd0-c9d0-45ed-84b9-97f7e1c14547/2d56fcd0-c9d0-45ed-84b9-97f7e1c14547/dedicated-private-vpc.yaml
      TimeoutInMinutes: 60
  EngineSophos:
    Type: AWS::CloudFormation::Stack
    Properties:
      Parameters:
        SSHIngressCidrIp:
          Ref: SSHIngressCidrIp
        VpcCidrBlock:
          Ref: VpcCidrBlock
        VpcSubnetCidrBits:
          Ref: VpcSubnetCidrBits
        FlowLogRetentionInDays:
          Ref: FlowLogRetentionInDays
        KeyName:
          Ref: KeyName
        InfrastructureAlarmsEmail:
          Ref: InfrastructureAlarmsEmail
        DeleteInfectedFiles: "false"
        ReportEventBridge: "false"
        ScanDelayInSeconds: "0"
        SophosLiveProtectionCloudLookups:
          Ref: EngineSophosSophosLiveProtectionCloudLookups
        AutoScalingMinSize:
          Ref: EngineSophosAutoScalingMinSize
        AutoScalingMaxSize:
          Ref: EngineSophosAutoScalingMaxSize
        CapacityStrategy:
          Ref: CapacityStrategy
        InstanceType:
          Ref: EngineSophosInstanceType
        AlternateInstanceType1:
          Ref: EngineSophosAlternateInstanceType3
        VolumeSize:
          Ref: EngineSophosVolumeSize
        VolumeIops:
          Ref: EngineSophosVolumeIops
        VolumeThroughput:
          Ref: EngineSophosVolumeThroughput
        LogsRetentionInDays:
          Ref: LogsRetentionInDays
        SystemsManagerAccess:
          Ref: SystemsManagerAccess
        ManagedPolicyArns:
          Ref: ManagedPolicyArns
        PermissionsBoundary:
          Ref: PermissionsBoundary
        InternalMultiStackName:
          Ref: AWS::StackName
        CloudflareAccountId:
          Ref: CloudflareAccountId
        CloudflareApiToken:
          Ref: CloudflareApiToken
        CloudflareAccessKeyId:
          Ref: CloudflareAccessKeyId
        CloudflareAccessKeySecret:
          Ref: CloudflareAccessKeySecret
      TemplateURL: https://awsmp-cft-211125678794-1707910187780.s3.us-east-1.amazonaws.com/db151397-11fc-4af2-89e1-ea4c19dfc51b/db151397-11fc-4af2-89e1-ea4c19dfc51b/dedicated-private-vpc.yaml
      TimeoutInMinutes: 60
  ScanQueueFanOutLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: LogsRetentionInDays
  ScanQueueFanOutLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  Fn::GetAtt:
                    - ScanQueueFanOutLambdaLogGroup
                    - Arn
              - Effect: Allow
                Action:
                  - sqs:ReceiveMessage
                  - sqs:DeleteMessage
                  - sqs:GetQueueAttributes
                Resource:
                  Fn::GetAtt:
                    - ScanQueue
                    - Arn
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  - Fn::GetAtt:
                      - EngineClamAV
                      - Outputs.InternalScanQueueArn
                  - Fn::GetAtt:
                      - EngineSophos
                      - Outputs.InternalScanQueueArn
          PolicyName: lambda
  ScanQueueFanOutLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          var __defProp = Object.defineProperty;
          var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
          var __getOwnPropNames = Object.getOwnPropertyNames;
          var __hasOwnProp = Object.prototype.hasOwnProperty;
          var __export = (target, all) => {
            for (var name in all)
              __defProp(target, name, { get: all[name], enumerable: true });
          };
          var __copyProps = (to, from, except, desc) => {
            if (from && typeof from === "object" || typeof from === "function") {
              for (let key of __getOwnPropNames(from))
                if (!__hasOwnProp.call(to, key) && key !== except)
                  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
            }
            return to;
          };
          var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

          // lambda/multi-scan-queue-fan-out.js
          var multi_scan_queue_fan_out_exports = {};
          __export(multi_scan_queue_fan_out_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(multi_scan_queue_fan_out_exports);
          var import_client_sqs = require("@aws-sdk/client-sqs");
          var sqs = new import_client_sqs.SQSClient({ apiVersion: "2012-11-05" });
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const data1 = await sqs.send(new import_client_sqs.SendMessageBatchCommand({
              Entries: event.Records.map((record, i) => ({ Id: `${i}`, MessageBody: record.body })),
              QueueUrl: process.env.SCAN_QUEUE_URL1
            }));
            const data2 = await sqs.send(new import_client_sqs.SendMessageBatchCommand({
              Entries: event.Records.map((record, i) => ({ Id: `${i}`, MessageBody: record.body })),
              QueueUrl: process.env.SCAN_QUEUE_URL2
            }));
            if (data1.Failed?.length > 0) {
              console.error("Sending messages to queue1 failed, the following messages are affected:");
              data1.Failed.forEach((failed) => {
                const i = parseInt(failed.Id, 10);
                console.error(`${failed.Code}[${failed.SenderFault}]: ${failed.Message}`, event.Records[i]);
              });
            }
            if (data2.Failed?.length > 0) {
              console.error("Sending messages to queue2 failed, the following messages are affected:");
              data2.Failed.forEach((failed) => {
                const i = parseInt(failed.Id, 10);
                console.error(`${failed.Code}[${failed.SenderFault}]: ${failed.Message}`, event.Records[i]);
              });
            }
            if (data1.Failed?.length > 0 || data2.Failed?.length > 0) {
              throw new Error("Sending messages failed");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          SCAN_QUEUE_URL1:
            Fn::GetAtt:
              - EngineClamAV
              - Outputs.InternalScanQueueUrl
          SCAN_QUEUE_URL2:
            Fn::GetAtt:
              - EngineSophos
              - Outputs.InternalScanQueueUrl
      Handler: index.handler
      LoggingConfig:
        LogGroup:
          Ref: ScanQueueFanOutLambdaLogGroup
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - Ref: ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - ScanQueueFanOutLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 60
      VpcConfig:
        Ref: AWS::NoValue
  ScanQueueFanOutErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV ScanQueue Fan-Out failed. Check logs of AWS Lambda Function "
            - Ref: ScanQueueFanOutLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: ScanQueueFanOutLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  ScanQueueFanOutThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription: bucketAV ScanQueue Fan-Out throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: ScanQueueFanOutLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  ScanQueueFanOutLambdaEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      BatchSize: 10
      EventSourceArn:
        Fn::GetAtt:
          - ScanQueue
          - Arn
      FunctionName:
        Ref: ScanQueueFanOutLambdaFunction
      ScalingConfig:
        Fn::If:
          - HasScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - MaximumConcurrency:
              Ref: ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
  ScanResultsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: bucket_key
          AttributeType: S
        - AttributeName: version
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: bucket_key
          KeyType: HASH
        - AttributeName: version
          KeyType: RANGE
      SSESpecification:
        SSEEnabled: true
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true
  ScanResultsTableUserErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV multi user errors.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: TableName
          Value:
            Ref: ScanResultsTable
      EvaluationPeriods: 1
      MetricName: UserErrors
      Namespace: AWS/DynamoDB
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  ScanResultsTableSystemErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV multi system errors.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: TableName
          Value:
            Ref: ScanResultsTable
      EvaluationPeriods: 1
      MetricName: SystemErrors
      Namespace: AWS/DynamoDB
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  OnScanResultsLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: LogsRetentionInDays
  OnScanResultsLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  Fn::GetAtt:
                    - OnScanResultsLambdaLogGroup
                    - Arn
              - Effect: Allow
                Action:
                  - dynamodb:DescribeStream
                  - dynamodb:GetRecords
                  - dynamodb:GetShardIterator
                Resource:
                  Fn::GetAtt:
                    - ScanResultsTable
                    - StreamArn
              - Effect: Allow
                Action: dynamodb:ListStreams
                Resource:
                  Fn::Join:
                    - ""
                    - - Fn::GetAtt:
                          - ScanResultsTable
                          - Arn
                      - /stream/*
              - Effect: Allow
                Action: cloudwatch:PutMetricData
                Resource: "*"
                Condition:
                  StringEquals:
                    cloudwatch:namespace:
                      Ref: AWS::StackName
          PolicyName: lambda
  OnScanResultsLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          var __defProp = Object.defineProperty;
          var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
          var __getOwnPropNames = Object.getOwnPropertyNames;
          var __hasOwnProp = Object.prototype.hasOwnProperty;
          var __export = (target, all) => {
            for (var name in all)
              __defProp(target, name, { get: all[name], enumerable: true });
          };
          var __copyProps = (to, from, except, desc) => {
            if (from && typeof from === "object" || typeof from === "function") {
              for (let key of __getOwnPropNames(from))
                if (!__hasOwnProp.call(to, key) && key !== except)
                  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
            }
            return to;
          };
          var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

          // lambda/multi-on-scan-results.js
          var multi_on_scan_results_exports = {};
          __export(multi_on_scan_results_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(multi_on_scan_results_exports);
          var import_client_cloudwatch = require("@aws-sdk/client-cloudwatch");

          // lambda/lib-multi.js
          function mapScanResult(m) {
            const scanResult = {
              status: m.status.S
            };
            if ("size" in m) {
              scanResult.size = parseInt(m.size.N, 10);
            }
            if ("finding" in m) {
              scanResult.finding = m.finding.S;
            }
            if ("trace_id" in m) {
              scanResult.traceId = m.trace_id.S;
            }
            if ("custom_data" in m) {
              scanResult.customData = m.custom_data.S;
            }
            if ("realfiletype" in m) {
              scanResult.realfiletype = m.realfiletype.S;
            }
            return scanResult;
          }
          function mapScanResults(item) {
            const scanResultAttributes = Object.keys(item).filter((attribute) => attribute.startsWith("scan_result_"));
            return scanResultAttributes.map((scanResultAttribute) => mapScanResult(item[scanResultAttribute].M));
          }
          function aggregateScanResults(scanResults, propertyName) {
            return scanResults.reduce((acc, scanResult) => {
              if (!(scanResult[propertyName] in acc)) {
                acc[scanResult[propertyName]] = 0;
              }
              acc[scanResult[propertyName]]++;
              return acc;
            }, {});
          }
          function selectScanResults(scanResults, propertyName, defaultValue = void 0) {
            for (const scanResult of scanResults) {
              if (propertyName in scanResult) {
                return scanResult[propertyName];
              }
            }
            return defaultValue;
          }
          function aggreateScanResult(bucket, key, version, scanResults) {
            const statusMap = aggregateScanResults(scanResults, "status");
            const size = selectScanResults(scanResults, "size", -1);
            let status;
            let finding = selectScanResults(scanResults, "finding");
            if (statusMap.clean === 1 && statusMap.no === 1 && scanResults.find((scanResult2) => scanResult2.status === "no" && scanResult2.finding === "bucketav:too-big") !== void 0) {
              status = "clean";
              finding = void 0;
            } else if (statusMap.clean === scanResults.length) {
              status = "clean";
            } else if (statusMap.no >= 1) {
              status = "no";
            } else if (statusMap.infected >= 1) {
              status = "infected";
            } else {
              throw new Error("status is not decideable");
            }
            const scanResult = { bucket, key, version, size, status, action: "no" };
            if (finding !== void 0) {
              scanResult.finding = finding;
            }
            const traceId = selectScanResults(scanResults, "traceId");
            if (traceId !== void 0) {
              scanResult.traceId = traceId;
            }
            const customData = selectScanResults(scanResults, "customData");
            if (customData !== void 0) {
              scanResult.customData = customData;
            }
            const realFileType = selectScanResults(scanResults, "realfiletype");
            if (realFileType !== void 0) {
              scanResult.realFileType = realFileType;
            }
            return scanResult;
          }

          // lambda/multi-on-scan-results.js
          var GB_IN_BYTES = 1024 * 1024 * 1024;
          var cloudwatch = new import_client_cloudwatch.CloudWatchClient({ apiVersion: "2010-08-01" });
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const count = {
              clean: 0,
              no: 0,
              infected: 0
            };
            let processedBytes = 0;
            for (const record of event.Records) {
              console.log(JSON.stringify(record, null, 2));
              if (record.eventName === "INSERT" || record.eventName === "MODIFY") {
                const scanResults = mapScanResults(record.dynamodb.NewImage);
                if (scanResults.length === 2) {
                  const { status, size } = aggreateScanResult("bucket", "key", "version", scanResults);
                  count[status]++;
                  processedBytes += size;
                }
              } else if (record.eventName === "REMOVE") {
              } else {
                throw new Error(`unsupported eventName: ${record.eventName}`);
              }
            }
            const input = {
              Namespace: process.env.CORE_STACK_NAME,
              MetricData: []
            };
            if (count.clean > 0) {
              input.MetricData.push({
                MetricName: "clean",
                Value: count.clean,
                Unit: "Count"
              });
            }
            if (count.infected > 0) {
              input.MetricData.push({
                MetricName: "infected",
                Value: count.infected,
                Unit: "Count"
              });
            }
            if (count.no > 0) {
              input.MetricData.push({
                MetricName: "no",
                Value: count.no,
                Unit: "Count"
              });
            }
            if (processedBytes > 0) {
              input.MetricData.push({
                MetricName: "scanned_data",
                Value: processedBytes / GB_IN_BYTES,
                Unit: "None"
              });
            }
            if (input.MetricData.length > 0) {
              await cloudwatch.send(new import_client_cloudwatch.PutMetricDataCommand(input));
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: AWS::StackName
      Handler: index.handler
      LoggingConfig:
        LogGroup:
          Ref: OnScanResultsLambdaLogGroup
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasOnScanResultsLambdaFunctionReservedConcurrentExecutions
          - Ref: OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - OnScanResultsLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 300
      VpcConfig:
        Ref: AWS::NoValue
  OnScanResultsLambdaFunctionAlarmIteratorAge:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV multi iterator age.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: OnScanResultsLambdaFunction
      EvaluationPeriods: 1
      MetricName: IteratorAge
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Maximum
      Threshold: 180000
      TreatMissingData: notBreaching
  OnScanResultsLambdaEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      BatchSize: 10000
      EventSourceArn:
        Fn::GetAtt:
          - ScanResultsTable
          - StreamArn
      FunctionName:
        Ref: OnScanResultsLambdaFunction
      MaximumBatchingWindowInSeconds: 60
      ScalingConfig:
        Fn::If:
          - HasOnScanResultsLambdaFunctionReservedConcurrentExecutions
          - MaximumConcurrency:
              Ref: OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      StartingPosition: LATEST
  FindingsTopic:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: bucketav:cloudformation:logical-id
          Value: FindingsTopic
        - Key: bucketav:cloudformation:stack-id
          Value:
            Ref: AWS::StackId
        - Key: bucketav:cloudformation:stack-name
          Value:
            Ref: AWS::StackName
  ServiceDiscoveryFindingsTopicArn:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /FindingsTopicArn
      Type: String
      Value:
        Ref: FindingsTopic
  Logs:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: LogsRetentionInDays
  FindingsTopicFanInLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  Fn::GetAtt:
                    - Logs
                    - Arn
              - Effect: Allow
                Action: dynamodb:UpdateItem
                Resource:
                  Fn::GetAtt:
                    - ScanResultsTable
                    - Arn
              - Effect: Allow
                Action: sns:Publish
                Resource:
                  Fn::GetAtt:
                    - FindingsTopic
                    - TopicArn
              - Fn::If:
                  - HasReportEventBridge
                  - Effect: Allow
                    Action: events:PutEvents
                    Resource:
                      Fn::Join:
                        - ""
                        - - "arn:"
                          - Ref: AWS::Partition
                          - ":events:"
                          - Ref: AWS::Region
                          - ":"
                          - Ref: AWS::AccountId
                          - :event-bus/default
                    Condition:
                      StringEquals:
                        events:detail-type: Scan Result
                        events:source: com.bucketav
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: AWS::StackName
                        - /CloudflareR2EndpointHostname
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: AWS::StackName
                        - /CloudflareAccessKeyId
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: AWS::StackName
                        - /CloudflareAccessKeySecretArn
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  Ref: CloudflareAccessKeySecretSecret
          PolicyName: lambda
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - FindingsTopicFanInLambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
  FindingsTopicFanInLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          var __defProp = Object.defineProperty;
          var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
          var __getOwnPropNames = Object.getOwnPropertyNames;
          var __hasOwnProp = Object.prototype.hasOwnProperty;
          var __export = (target, all) => {
            for (var name in all)
              __defProp(target, name, { get: all[name], enumerable: true });
          };
          var __copyProps = (to, from, except, desc) => {
            if (from && typeof from === "object" || typeof from === "function") {
              for (let key of __getOwnPropNames(from))
                if (!__hasOwnProp.call(to, key) && key !== except)
                  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
            }
            return to;
          };
          var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

          // lambda/multi-findings-topic-fan-in.js
          var multi_findings_topic_fan_in_exports = {};
          __export(multi_findings_topic_fan_in_exports, {
            handler: () => handler,
            replaceSurrogatePairs: () => replaceSurrogatePairs,
            shorten: () => shorten,
            toString: () => toString
          });
          module.exports = __toCommonJS(multi_findings_topic_fan_in_exports);
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_sns2 = require("@aws-sdk/client-sns");
          var import_client_eventbridge2 = require("@aws-sdk/client-eventbridge");
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_secrets_manager2 = require("@aws-sdk/client-secrets-manager");

          // lambda/node_modules/yocto-queue/index.js
          var Node = class {
            value;
            next;
            constructor(value) {
              this.value = value;
            }
          };
          var Queue = class {
            #head;
            #tail;
            #size;
            constructor() {
              this.clear();
            }
            enqueue(value) {
              const node = new Node(value);
              if (this.#head) {
                this.#tail.next = node;
                this.#tail = node;
              } else {
                this.#head = node;
                this.#tail = node;
              }
              this.#size++;
            }
            dequeue() {
              const current = this.#head;
              if (!current) {
                return;
              }
              this.#head = this.#head.next;
              this.#size--;
              if (!this.#head) {
                this.#tail = void 0;
              }
              return current.value;
            }
            peek() {
              if (!this.#head) {
                return;
              }
              return this.#head.value;
            }
            clear() {
              this.#head = void 0;
              this.#tail = void 0;
              this.#size = 0;
            }
            get size() {
              return this.#size;
            }
            *[Symbol.iterator]() {
              let current = this.#head;
              while (current) {
                yield current.value;
                current = current.next;
              }
            }
            *drain() {
              while (this.#head) {
                yield this.dequeue();
              }
            }
          };

          // lambda/node_modules/p-limit/index.js
          function pLimit(concurrency) {
            let rejectOnClear = false;
            if (typeof concurrency === "object") {
              ({ concurrency, rejectOnClear = false } = concurrency);
            }
            validateConcurrency(concurrency);
            if (typeof rejectOnClear !== "boolean") {
              throw new TypeError("Expected `rejectOnClear` to be a boolean");
            }
            const queue = new Queue();
            let activeCount = 0;
            const resumeNext = () => {
              if (activeCount < concurrency && queue.size > 0) {
                activeCount++;
                queue.dequeue().run();
              }
            };
            const next = () => {
              activeCount--;
              resumeNext();
            };
            const run = async (function_, resolve, arguments_) => {
              const result = (async () => function_(...arguments_))();
              resolve(result);
              try {
                await result;
              } catch {
              }
              next();
            };
            const enqueue = (function_, resolve, reject, arguments_) => {
              const queueItem = { reject };
              new Promise((internalResolve) => {
                queueItem.run = internalResolve;
                queue.enqueue(queueItem);
              }).then(run.bind(void 0, function_, resolve, arguments_));
              if (activeCount < concurrency) {
                resumeNext();
              }
            };
            const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
              enqueue(function_, resolve, reject, arguments_);
            });
            Object.defineProperties(generator, {
              activeCount: {
                get: () => activeCount
              },
              pendingCount: {
                get: () => queue.size
              },
              clearQueue: {
                value() {
                  if (!rejectOnClear) {
                    queue.clear();
                    return;
                  }
                  const abortError = AbortSignal.abort().reason;
                  while (queue.size > 0) {
                    queue.dequeue().reject(abortError);
                  }
                }
              },
              concurrency: {
                get: () => concurrency,
                set(newConcurrency) {
                  validateConcurrency(newConcurrency);
                  concurrency = newConcurrency;
                  queueMicrotask(() => {
                    while (activeCount < concurrency && queue.size > 0) {
                      resumeNext();
                    }
                  });
                }
              },
              map: {
                async value(iterable, function_) {
                  const promises = Array.from(iterable, (value, index) => this(function_, value, index));
                  return Promise.all(promises);
                }
              }
            });
            return generator;
          }
          function validateConcurrency(concurrency) {
            if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
              throw new TypeError("Expected `concurrency` to be a number from 1 and up");
            }
          }

          // lambda/lib-multi.js
          function mapScanResult(m) {
            const scanResult = {
              status: m.status.S
            };
            if ("size" in m) {
              scanResult.size = parseInt(m.size.N, 10);
            }
            if ("finding" in m) {
              scanResult.finding = m.finding.S;
            }
            if ("trace_id" in m) {
              scanResult.traceId = m.trace_id.S;
            }
            if ("custom_data" in m) {
              scanResult.customData = m.custom_data.S;
            }
            if ("realfiletype" in m) {
              scanResult.realfiletype = m.realfiletype.S;
            }
            return scanResult;
          }
          function mapScanResults(item) {
            const scanResultAttributes = Object.keys(item).filter((attribute) => attribute.startsWith("scan_result_"));
            return scanResultAttributes.map((scanResultAttribute) => mapScanResult(item[scanResultAttribute].M));
          }
          function aggregateScanResults(scanResults, propertyName) {
            return scanResults.reduce((acc, scanResult) => {
              if (!(scanResult[propertyName] in acc)) {
                acc[scanResult[propertyName]] = 0;
              }
              acc[scanResult[propertyName]]++;
              return acc;
            }, {});
          }
          function selectScanResults(scanResults, propertyName, defaultValue = void 0) {
            for (const scanResult of scanResults) {
              if (propertyName in scanResult) {
                return scanResult[propertyName];
              }
            }
            return defaultValue;
          }
          function aggreateScanResult(bucket, key, version, scanResults) {
            const statusMap = aggregateScanResults(scanResults, "status");
            const size = selectScanResults(scanResults, "size", -1);
            let status;
            let finding = selectScanResults(scanResults, "finding");
            if (statusMap.clean === 1 && statusMap.no === 1 && scanResults.find((scanResult2) => scanResult2.status === "no" && scanResult2.finding === "bucketav:too-big") !== void 0) {
              status = "clean";
              finding = void 0;
            } else if (statusMap.clean === scanResults.length) {
              status = "clean";
            } else if (statusMap.no >= 1) {
              status = "no";
            } else if (statusMap.infected >= 1) {
              status = "infected";
            } else {
              throw new Error("status is not decideable");
            }
            const scanResult = { bucket, key, version, size, status, action: "no" };
            if (finding !== void 0) {
              scanResult.finding = finding;
            }
            const traceId = selectScanResults(scanResults, "traceId");
            if (traceId !== void 0) {
              scanResult.traceId = traceId;
            }
            const customData = selectScanResults(scanResults, "customData");
            if (customData !== void 0) {
              scanResult.customData = customData;
            }
            const realFileType = selectScanResults(scanResults, "realfiletype");
            if (realFileType !== void 0) {
              scanResult.realFileType = realFileType;
            }
            return scanResult;
          }

          // lambda/lib.js
          var import_client_cloudformation = require("@aws-sdk/client-cloudformation");
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_sns = require("@aws-sdk/client-sns");
          var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var import_client_s32 = require("@aws-sdk/client-s3");
          var import_client_ssm = require("@aws-sdk/client-ssm");
          var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
          var import_client_organizations = require("@aws-sdk/client-organizations");
          var import_credential_providers = require("@aws-sdk/credential-providers");

          // lambda/node_modules/@smithy/core/dist-es/pagination/createPaginator.js
          var makePagedClientRequest = async (CommandCtor, client, input, withCommand = (_) => _, ...args) => {
            let command = new CommandCtor(input);
            command = withCommand(command) ?? command;
            return await client.send(command, ...args);
          };
          function createPaginator(ClientCtor, CommandCtor, inputTokenName, outputTokenName, pageSizeTokenName) {
            return async function* paginateOperation(config, input, ...additionalArguments) {
              const _input = input;
              let token = config.startingToken ?? _input[inputTokenName];
              let hasNext = true;
              let page;
              while (hasNext) {
                _input[inputTokenName] = token;
                if (pageSizeTokenName) {
                  _input[pageSizeTokenName] = _input[pageSizeTokenName] ?? config.pageSize;
                }
                if (config.client instanceof ClientCtor) {
                  page = await makePagedClientRequest(CommandCtor, config.client, input, config.withCommand, ...additionalArguments);
                } else {
                  throw new Error(`Invalid client, expected instance of ${ClientCtor.name}`);
                }
                yield page;
                const prevToken = token;
                token = get(page, outputTokenName);
                hasNext = !!(token && (!config.stopOnSameToken || token !== prevToken));
              }
              return void 0;
            };
          }
          var get = (fromObject, path) => {
            let cursor = fromObject;
            const pathComponents = path.split(".");
            for (const step of pathComponents) {
              if (!cursor || typeof cursor !== "object") {
                return void 0;
              }
              cursor = cursor[step];
            }
            return cursor;
          };

          // lambda/lib.js
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function fetchCrossAccount(ssm2, coreStackName) {
            try {
              const data = await ssm2.send(new import_client_ssm.GetParameterCommand({
                Name: `/bucketAV/${coreStackName}/CrossAccount`
              }));
              return data.Parameter.Value === "true";
            } catch (err) {
              if (err.name === "ParameterNotFound") {
                return false;
              } else {
                throw err;
              }
            }
          }
          async function fetchS3(defaultS32, dynamodb2, bucketCache2, isCrossAccount, bucketName, bucketCacheTableName) {
            if (isCrossAccount === true) {
              const cachedS3 = bucketCache2.get(bucketName);
              if (cachedS3 !== void 0) {
                return cachedS3;
              } else {
                const bucketCacheData = await dynamodb2.send(new import_client_dynamodb.GetItemCommand({
                  TableName: bucketCacheTableName,
                  Key: {
                    bucket_name: { S: bucketName }
                  },
                  ConsistentRead: true
                }));
                if (bucketCacheData.Item === void 0) {
                  console.log(`uncached S3 bucket ${bucketName}`);
                  bucketCache2.set(bucketName, defaultS32);
                  return defaultS32;
                } else {
                  if ("role_arn" in bucketCacheData.Item && "external_id" in bucketCacheData.Item) {
                    const configuration = {
                      apiVersion: "2006-03-01",
                      credentials: (0, import_credential_providers.fromTemporaryCredentials)({
                        params: {
                          ExternalId: bucketCacheData.Item.external_id.S,
                          RoleArn: bucketCacheData.Item.role_arn.S,
                          RoleSessionName: "bucketav"
                        }
                      })
                    };
                    if ("bucket_region" in bucketCacheData.Item) {
                      configuration.region = bucketCacheData.Item.bucket_region.S;
                    }
                    const s3 = new import_client_s32.S3Client(configuration);
                    bucketCache2.set(bucketName, s3);
                    return s3;
                  } else {
                    bucketCache2.set(bucketName, defaultS32);
                    return defaultS32;
                  }
                }
              }
            } else {
              return defaultS32;
            }
          }
          async function fetchR2(ssm2, secretsmanager2, coreStackName) {
            const { Parameter: { Value: cloudflareR2EndpointHostname } } = await ssm2.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareR2EndpointHostname` }));
            const { Parameter: { Value: cloudflareAccessKeyId } } = await ssm2.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeyId` }));
            const { Parameter: { Value: cloudflareAccessKeySecretArn } } = await ssm2.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeySecretArn` }));
            const { SecretString: cloudflareAccessKey } = await secretsmanager2.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: cloudflareAccessKeySecretArn }));
            return new import_client_s32.S3Client({
              region: "auto",
              endpoint: `https://${cloudflareR2EndpointHostname}`,
              credentials: {
                accessKeyId: cloudflareAccessKeyId,
                secretAccessKey: cloudflareAccessKey
              }
            });
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;

          // lambda/multi-findings-topic-fan-in.js
          var dynamodb = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
          var sns = new import_client_sns2.SNSClient({ apiVersion: "2010-03-31" });
          var eventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07" });
          var defaultS3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var bucketCache = /* @__PURE__ */ new Map();
          var bucketCacheRefreshedAt = 0;
          var isCrossAccountPromise = void 0;
          function shorten(url) {
            if (url?.length > 128) {
              return url.substr(0, 63) + "..." + url.substr(url.length - 62);
            }
            return url;
          }
          function toString(bucket, key, version) {
            if (version !== void 0) {
              return shorten(`s3://${bucket}/${key} ${version}`);
            }
            return shorten(`s3://${bucket}/${key}`);
          }
          function isHighSurrogate(ch) {
            return ch >= "\uD800" && ch < "\uDBFF1";
          }
          function isLowSurrogate(ch) {
            return ch >= "\uDC00" && ch < "\uDFFF1";
          }
          function replaceSurrogatePairs(str, replacement) {
            let ret = "";
            for (let i = 0; i < str.length; i++) {
              const c = str.charAt(i);
              if (isHighSurrogate(c)) {
                ret += replacement;
              } else if (isLowSurrogate(c)) {
                continue;
              } else {
                ret += c;
              }
            }
            return ret;
          }
          async function publishNotification(sns2, eventbridge2, { bucket, key, version, size, status, action, finding, traceId, customData, realFileType }) {
            const json_message = {
              bucket,
              key,
              status,
              action,
              size
            };
            const message_attributes = {
              bucket: {
                DataType: "String",
                StringValue: bucket
              },
              key: {
                DataType: "String",
                StringValue: replaceSurrogatePairs(key, "#")
                // Surrogate values are not supported in the message attributes (src https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
              },
              status: {
                DataType: "String",
                StringValue: status
              },
              action: {
                DataType: "String",
                StringValue: action
              },
              size: {
                DataType: "Number",
                StringValue: `${size}`
              }
            };
            let message = toString(bucket, key, version);
            if (version !== void 0) {
              message_attributes.version = {
                DataType: "String",
                StringValue: version
              };
              json_message.version = version;
            }
            message += ` is ${status}`;
            if (finding !== void 0) {
              message_attributes.finding = {
                DataType: "String",
                StringValue: replaceSurrogatePairs(finding, "#")
                // Surrogate values are not supported in the message attributes (src https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
              };
              json_message.finding = finding;
              message += ` by ${finding}`;
            }
            if (traceId !== void 0) {
              message_attributes.trace_id = {
                DataType: "String",
                StringValue: replaceSurrogatePairs(traceId, "#")
                // Surrogate values are not supported in the message attributes (src https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
              };
              json_message.trace_id = traceId;
            }
            if (customData !== void 0) {
              json_message.custom_data = customData;
            }
            if (realFileType !== void 0) {
              json_message.realfiletype = realFileType;
            }
            message += `, ${action} action executed`;
            await sns2.send(new import_client_sns2.PublishCommand({
              TopicArn: process.env.FINDINGS_TOPIC_ARN,
              Message: JSON.stringify({
                default: JSON.stringify(json_message),
                email: message
              }),
              Subject: `bucketAV Scan Result for S3 Bucket ${bucket}`,
              // max 100 chars long (S3 bucket name max is 63)
              MessageStructure: "json",
              MessageAttributes: message_attributes
            }));
            if (process.env.REPORT_EVENT_BRIDGE === "true") {
              await eventbridge2.send(new import_client_eventbridge2.PutEventsCommand({
                Entries: [{
                  Source: "com.bucketav",
                  DetailType: "Scan Result",
                  Detail: JSON.stringify({
                    ...json_message,
                    core_stack_name: process.env.CORE_STACK_NAME
                  })
                }]
              }));
            }
          }
          async function tag(s3, bucket, key, version, status) {
            const tagKey = process.env.TAG_KEY;
            try {
              let { TagSet: tag_set } = await s3.send(new import_client_s33.GetObjectTaggingCommand({
                Bucket: bucket,
                Key: key,
                VersionId: version
              }));
              const i = tag_set.findIndex((tag2) => tag2.Key === tagKey);
              if (tag_set.length === 0) {
                tag_set = [{ Key: tagKey, Value: status }];
              } else if (i !== -1) {
                if (tag_set[i].Value === status) {
                  console.debug(`${toString(bucket, key, version)} is already tagged and value is the same, skip tagging to save costs`);
                  return true;
                }
                tag_set[i].Value = status;
              } else if (tag_set.length >= 10) {
                console.warn(`${toString(bucket, key, version)} already has 10 tags, tag ${tagKey} can not be added, skip tagging`);
                return false;
              } else {
                tag_set.push({ Key: tagKey, Value: status });
              }
              await s3.send(new import_client_s33.PutObjectTaggingCommand({
                Bucket: bucket,
                Key: key,
                VersionId: version,
                Tagging: {
                  TagSet: tag_set
                }
              }));
              return true;
            } catch (err) {
              if (err.name === "AccessDenied") {
                console.warn(`${toString(bucket, key, version)} could not be tagged because of missing permissions (AccessDenied), skip tagging`);
                return false;
              } else {
                throw err;
              }
            }
          }
          async function remove(s3, bucket, key, version) {
            try {
              await s3.send(new import_client_s33.DeleteObjectCommand({
                Bucket: bucket,
                Key: key,
                VersionId: version
              }));
              return true;
            } catch (err) {
              if (err.name === "AccessDenied") {
                console.warn(`${toString(bucket, key, version)} could not be deleted because of missing permissions (AccessDenied), skip deleting`);
                return false;
              } else {
                throw err;
              }
            }
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const CORE_PLATFORM = process.env.CORE_PLATFORM;
            const now = Date.now();
            if (now - bucketCacheRefreshedAt > 36e5) {
              bucketCache.clear();
              if (CORE_PLATFORM === "aws") {
                isCrossAccountPromise = fetchCrossAccount(ssm, process.env.CORE_STACK_NAME);
              }
              bucketCacheRefreshedAt = now;
            }
            const ttl = Math.floor(now / 1e3) + 60 * 60;
            const limit = pLimit(8);
            await Promise.all(event.Records.map((record) => limit(async () => {
              const { bucket, key, version, size, status, finding, trace_id, custom_data, realfiletype } = JSON.parse(record.Sns.Message);
              const s3 = CORE_PLATFORM === "aws" ? await fetchS3(defaultS3, dynamodb, bucketCache, await isCrossAccountPromise, bucket, `${process.env.CORE_STACK_NAME}-BucketCache`) : await fetchR2(ssm, secretsmanager, process.env.CORE_STACK_NAME);
              const data = await dynamodb.send(new import_client_dynamodb2.UpdateItemCommand({
                TableName: process.env.SCAN_RESULTS_TABLE_NAME,
                Key: {
                  bucket_key: { S: `${bucket}/${key}` },
                  version: { S: version === void 0 ? "null" : version }
                },
                UpdateExpression: "SET #scan_result=:scan_result, #ttl=:ttl",
                // we don't use a map for scan_results because we would need to create the item with an empty map first to allow SET operations on the map
                ExpressionAttributeValues: {
                  ":scan_result": { M: {
                    ...size !== void 0 ? { size: { N: size.toFixed(0) } } : {},
                    status: { S: status },
                    ...finding !== void 0 ? { finding: { S: finding } } : {},
                    ...trace_id !== void 0 ? { trace_id: { S: trace_id } } : {},
                    ...custom_data !== void 0 ? { custom_data: { S: custom_data } } : {},
                    ...realfiletype !== void 0 ? { realfiletype: { S: realfiletype } } : {}
                  } },
                  ":ttl": { N: ttl.toFixed(0) }
                },
                ExpressionAttributeNames: {
                  "#scan_result": "scan_result_" + record.Sns.TopicArn.split(":").pop(),
                  "#ttl": "ttl"
                },
                ReturnValues: "ALL_NEW"
              }));
              const scanResults = mapScanResults(data.Attributes);
              if (scanResults.length === 2) {
                const scanResult = aggreateScanResult(bucket, key, version, scanResults);
                if (scanResult.status === "clean") {
                  if (process.env.TAG_FILES === "true") {
                    console.log(`${toString(bucket, key, version)} is clean (tagging) [size=${scanResult.size}]`);
                    const tagged = await tag(s3, bucket, key, version, scanResult.status);
                    if (tagged === true) {
                      scanResult.action = "tag";
                    }
                  } else {
                    console.log(`${toString(bucket, key, version)} is clean (do nothing) [size=${scanResult.size}]`);
                  }
                } else if (scanResult.status === "no") {
                  if (scanResult.finding.startsWith("Heuristics.Encrypted.")) {
                    if (process.env.TAG_FILES === "true") {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is encrypted by ${scanResult.finding} (tagging) [size=${scanResult.size}]`);
                      const tagged = await tag(s3, bucket, key, version, scanResult.status);
                      if (tagged === true) {
                        scanResult.action = "tag";
                      }
                    } else {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is encrypted by ${scanResult.finding} (do nothing) [size=${scanResult.size}]`);
                    }
                  } else if (scanResult.finding.startsWith("Heuristics.Limits.Exceeded.")) {
                    if (process.env.TAG_FILES === "true") {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is limited by ${scanResult.finding} (tagging) [size=${scanResult.size}]`);
                      const tagged = await tag(s3, bucket, key, version, scanResult.status);
                      if (tagged === true) {
                        scanResult.action = "tag";
                      }
                    } else {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is limited by ${scanResult.finding} (do nothing) [size=${scanResult.size}]`);
                    }
                  } else {
                    if (process.env.TAG_FILES === "true") {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is ${scanResult.finding} (tagging) [size=${scanResult.size}]`);
                      const tagged = await tag(s3, bucket, key, version, scanResult.status);
                      if (tagged === true) {
                        scanResult.action = "tag";
                      }
                    } else {
                      console.log(`${toString(bucket, key, version)} could not be scanned because it is ${scanResult.finding} (do nothing) [size=${scanResult.size}]`);
                    }
                  }
                } else if (scanResult.status === "infected") {
                  if (process.env.DELETE_INFECTED_FILES === "true") {
                    console.log(`${toString(bucket, key, version)} is infected by ${scanResult.finding} (deleting) [size=${scanResult.size}]`);
                    const removed = await remove(s3, bucket, key, version);
                    if (removed === true) {
                      scanResult.action = "delete";
                    }
                  } else if (process.env.TAG_FILES === "true") {
                    console.log(`${toString(bucket, key, version)} is infected by ${scanResult.finding} (tagging) [size=${scanResult.size}]`);
                    const tagged = await tag(s3, bucket, key, version, scanResult.status);
                    if (tagged === true) {
                      scanResult.action = "tag";
                    }
                  } else {
                    console.log(`${toString(bucket, key, version)} is infected by ${scanResult.finding} (do nothing) [size=${scanResult.size}]`);
                  }
                } else {
                  throw new Error("unextepected value for status");
                }
                await publishNotification(sns, eventbridge, scanResult);
              } else {
                console.debug("Not enough scan results for final decission");
              }
            })));
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler,
            replaceSurrogatePairs,
            shorten,
            toString
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - FindingsTopicFanInLambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          SCAN_RESULTS_TABLE_NAME:
            Ref: ScanResultsTable
          FINDINGS_TOPIC_ARN:
            Fn::GetAtt:
              - FindingsTopic
              - TopicArn
          REPORT_EVENT_BRIDGE:
            Ref: ReportEventBridge
          DELETE_INFECTED_FILES:
            Ref: DeleteInfectedFiles
          CORE_STACK_NAME:
            Ref: AWS::StackName
          CORE_PLATFORM: cloudflare
      Handler: index.handler
      LoggingConfig:
        LogGroup:
          Ref: Logs
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasFindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - Ref: FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - FindingsTopicFanInLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 60
      VpcConfig:
        Ref: AWS::NoValue
  FindingsTopicFanInLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV FindingsTopic Fan-In failed. Check logs of AWS Lambda Function "
            - Ref: FindingsTopicFanInLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: FindingsTopicFanInLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  FindingsTopicFanInLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription: bucketAV FindingsTopic Fan-In throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: FindingsTopicFanInLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  FindingsTopicFanInLambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
  FindingsTopicFanInLambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription: bucketAV FindingsTopic Fan-In has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - FindingsTopicFanInLambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  FindingsTopicFanInLambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::GetAtt:
            - InfrastructureAlarmsTopic
            - TopicArn
      AlarmDescription: bucketAV FindingsTopic Fan-In has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: FindingsTopicFanInLambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  FindingsTopicFanInLambdaPermission1:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: FindingsTopicFanInLambdaFunction
      Principal: sns.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - EngineClamAV
          - Outputs.InternalFindingsTopicArn
  FindingsTopicFanInSubscription1:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - FindingsTopicFanInLambdaFunction
          - Arn
      Protocol: lambda
      TopicArn:
        Fn::GetAtt:
          - EngineClamAV
          - Outputs.InternalFindingsTopicArn
    DependsOn:
      - FindingsTopicFanInLambdaPermission1
  FindingsTopicFanInLambdaPermission2:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: FindingsTopicFanInLambdaFunction
      Principal: sns.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - EngineSophos
          - Outputs.InternalFindingsTopicArn
  FindingsTopicFanInSubscription2:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - FindingsTopicFanInLambdaFunction
          - Arn
      Protocol: lambda
      TopicArn:
        Fn::GetAtt:
          - EngineSophos
          - Outputs.InternalFindingsTopicArn
    DependsOn:
      - FindingsTopicFanInLambdaPermission2
  DashboardLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  - Ref: CloudflareApiTokenSecret
                  - Ref: CloudflareAccessKeySecretSecret
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter
                        - Ref: ServiceDiscoveryCloudflareAccessKeyId
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter
                        - Ref: ServiceDiscoveryCloudflareAccessKeySecretArn
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter
                        - Ref: ServiceDiscoveryCloudflareR2EndpointHostname
              - Effect: Allow
                Action: cloudformation:DescribeStacks
                Resource: "*"
              - Effect: Allow
                Action: ssm:GetParametersByPath
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter/bucketAV/
                      - Ref: AWS::StackName
                      - /AddOn/*
              - Effect: Allow
                Action: logs:StartQuery
                Resource:
                  Fn::GetAtt:
                    - Logs
                    - Arn
              - Effect: Allow
                Action: logs:GetQueryResults
                Resource:
                  Fn::GetAtt:
                    - Logs
                    - Arn
          PolicyName: lambda
  DashboardLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          var __defProp = Object.defineProperty;
          var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
          var __getOwnPropNames = Object.getOwnPropertyNames;
          var __hasOwnProp = Object.prototype.hasOwnProperty;
          var __export = (target, all) => {
            for (var name in all)
              __defProp(target, name, { get: all[name], enumerable: true });
          };
          var __copyProps = (to, from, except, desc) => {
            if (from && typeof from === "object" || typeof from === "function") {
              for (let key of __getOwnPropNames(from))
                if (!__hasOwnProp.call(to, key) && key !== except)
                  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
            }
            return to;
          };
          var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

          // lambda/dashboard-cloudflare.js
          var dashboard_cloudflare_exports = {};
          __export(dashboard_cloudflare_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(dashboard_cloudflare_exports);
          var import_client_cloudformation2 = require("@aws-sdk/client-cloudformation");
          var import_client_cloudwatch_logs2 = require("@aws-sdk/client-cloudwatch-logs");
          var import_client_s36 = require("@aws-sdk/client-s3");
          var import_client_ssm3 = require("@aws-sdk/client-ssm");
          var import_client_dynamodb4 = require("@aws-sdk/client-dynamodb");
          var import_client_secrets_manager2 = require("@aws-sdk/client-secrets-manager");

          // lambda/lib.js
          var import_client_cloudformation = require("@aws-sdk/client-cloudformation");
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_sns = require("@aws-sdk/client-sns");
          var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var import_client_s32 = require("@aws-sdk/client-s3");
          var import_client_ssm = require("@aws-sdk/client-ssm");
          var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
          var import_client_organizations = require("@aws-sdk/client-organizations");
          var import_credential_providers = require("@aws-sdk/credential-providers");

          // lambda/node_modules/@smithy/core/dist-es/pagination/createPaginator.js
          var makePagedClientRequest = async (CommandCtor, client, input, withCommand = (_) => _, ...args) => {
            let command = new CommandCtor(input);
            command = withCommand(command) ?? command;
            return await client.send(command, ...args);
          };
          function createPaginator(ClientCtor, CommandCtor, inputTokenName, outputTokenName, pageSizeTokenName) {
            return async function* paginateOperation(config, input, ...additionalArguments) {
              const _input = input;
              let token = config.startingToken ?? _input[inputTokenName];
              let hasNext = true;
              let page;
              while (hasNext) {
                _input[inputTokenName] = token;
                if (pageSizeTokenName) {
                  _input[pageSizeTokenName] = _input[pageSizeTokenName] ?? config.pageSize;
                }
                if (config.client instanceof ClientCtor) {
                  page = await makePagedClientRequest(CommandCtor, config.client, input, config.withCommand, ...additionalArguments);
                } else {
                  throw new Error(`Invalid client, expected instance of ${ClientCtor.name}`);
                }
                yield page;
                const prevToken = token;
                token = get(page, outputTokenName);
                hasNext = !!(token && (!config.stopOnSameToken || token !== prevToken));
              }
              return void 0;
            };
          }
          var get = (fromObject, path) => {
            let cursor = fromObject;
            const pathComponents = path.split(".");
            for (const step of pathComponents) {
              if (!cursor || typeof cursor !== "object") {
                return void 0;
              }
              cursor = cursor[step];
            }
            return cursor;
          };

          // lambda/node_modules/yocto-queue/index.js
          var Node = class {
            value;
            next;
            constructor(value) {
              this.value = value;
            }
          };
          var Queue = class {
            #head;
            #tail;
            #size;
            constructor() {
              this.clear();
            }
            enqueue(value) {
              const node = new Node(value);
              if (this.#head) {
                this.#tail.next = node;
                this.#tail = node;
              } else {
                this.#head = node;
                this.#tail = node;
              }
              this.#size++;
            }
            dequeue() {
              const current = this.#head;
              if (!current) {
                return;
              }
              this.#head = this.#head.next;
              this.#size--;
              if (!this.#head) {
                this.#tail = void 0;
              }
              return current.value;
            }
            peek() {
              if (!this.#head) {
                return;
              }
              return this.#head.value;
            }
            clear() {
              this.#head = void 0;
              this.#tail = void 0;
              this.#size = 0;
            }
            get size() {
              return this.#size;
            }
            *[Symbol.iterator]() {
              let current = this.#head;
              while (current) {
                yield current.value;
                current = current.next;
              }
            }
            *drain() {
              while (this.#head) {
                yield this.dequeue();
              }
            }
          };

          // lambda/node_modules/p-limit/index.js
          function pLimit(concurrency) {
            let rejectOnClear = false;
            if (typeof concurrency === "object") {
              ({ concurrency, rejectOnClear = false } = concurrency);
            }
            validateConcurrency(concurrency);
            if (typeof rejectOnClear !== "boolean") {
              throw new TypeError("Expected `rejectOnClear` to be a boolean");
            }
            const queue = new Queue();
            let activeCount = 0;
            const resumeNext = () => {
              if (activeCount < concurrency && queue.size > 0) {
                activeCount++;
                queue.dequeue().run();
              }
            };
            const next = () => {
              activeCount--;
              resumeNext();
            };
            const run = async (function_, resolve, arguments_) => {
              const result = (async () => function_(...arguments_))();
              resolve(result);
              try {
                await result;
              } catch {
              }
              next();
            };
            const enqueue = (function_, resolve, reject, arguments_) => {
              const queueItem = { reject };
              new Promise((internalResolve) => {
                queueItem.run = internalResolve;
                queue.enqueue(queueItem);
              }).then(run.bind(void 0, function_, resolve, arguments_));
              if (activeCount < concurrency) {
                resumeNext();
              }
            };
            const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
              enqueue(function_, resolve, reject, arguments_);
            });
            Object.defineProperties(generator, {
              activeCount: {
                get: () => activeCount
              },
              pendingCount: {
                get: () => queue.size
              },
              clearQueue: {
                value() {
                  if (!rejectOnClear) {
                    queue.clear();
                    return;
                  }
                  const abortError = AbortSignal.abort().reason;
                  while (queue.size > 0) {
                    queue.dequeue().reject(abortError);
                  }
                }
              },
              concurrency: {
                get: () => concurrency,
                set(newConcurrency) {
                  validateConcurrency(newConcurrency);
                  concurrency = newConcurrency;
                  queueMicrotask(() => {
                    while (activeCount < concurrency && queue.size > 0) {
                      resumeNext();
                    }
                  });
                }
              },
              map: {
                async value(iterable, function_) {
                  const promises = Array.from(iterable, (value, index) => this(function_, value, index));
                  return Promise.all(promises);
                }
              }
            });
            return generator;
          }
          function validateConcurrency(concurrency) {
            if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
              throw new TypeError("Expected `concurrency` to be a number from 1 and up");
            }
          }

          // lambda/fetch-retry.js
          function fetch_retry_default(fetch3, defaults) {
            defaults = defaults || {};
            if (typeof fetch3 !== "function") {
              throw new ArgumentError("fetch must be a function");
            }
            if (typeof defaults !== "object") {
              throw new ArgumentError("defaults must be an object");
            }
            if (defaults.retries !== void 0 && !isPositiveInteger(defaults.retries)) {
              throw new ArgumentError("retries must be a positive integer");
            }
            if (defaults.retryDelay !== void 0 && !isPositiveInteger(defaults.retryDelay) && typeof defaults.retryDelay !== "function") {
              throw new ArgumentError("retryDelay must be a positive integer or a function returning a positive integer");
            }
            if (defaults.retryOn !== void 0 && !Array.isArray(defaults.retryOn) && typeof defaults.retryOn !== "function") {
              throw new ArgumentError("retryOn property expects an array or function");
            }
            var baseDefaults = {
              retries: 3,
              retryDelay: 1e3,
              retryOn: []
            };
            defaults = Object.assign(baseDefaults, defaults);
            return function fetchRetry(input, init) {
              var retries = defaults.retries;
              var retryDelay = defaults.retryDelay;
              var retryOn = defaults.retryOn;
              if (init && init.retries !== void 0) {
                if (isPositiveInteger(init.retries)) {
                  retries = init.retries;
                } else {
                  throw new ArgumentError("retries must be a positive integer");
                }
              }
              if (init && init.retryDelay !== void 0) {
                if (isPositiveInteger(init.retryDelay) || typeof init.retryDelay === "function") {
                  retryDelay = init.retryDelay;
                } else {
                  throw new ArgumentError("retryDelay must be a positive integer or a function returning a positive integer");
                }
              }
              if (init && init.retryOn) {
                if (Array.isArray(init.retryOn) || typeof init.retryOn === "function") {
                  retryOn = init.retryOn;
                } else {
                  throw new ArgumentError("retryOn property expects an array or function");
                }
              }
              return new Promise(function(resolve, reject) {
                var wrappedFetch = function(attempt) {
                  var _input = typeof Request !== "undefined" && input instanceof Request ? input.clone() : input;
                  fetch3(_input, init).then(function(response) {
                    if (Array.isArray(retryOn) && retryOn.indexOf(response.status) === -1) {
                      resolve(response);
                    } else if (typeof retryOn === "function") {
                      try {
                        return Promise.resolve(retryOn(attempt, null, response)).then(function(retryOnResponse) {
                          if (retryOnResponse) {
                            retry(attempt, null, response);
                          } else {
                            resolve(response);
                          }
                        }).catch(reject);
                      } catch (error) {
                        reject(error);
                      }
                    } else {
                      if (attempt < retries) {
                        retry(attempt, null, response);
                      } else {
                        resolve(response);
                      }
                    }
                  }).catch(function(error) {
                    if (typeof retryOn === "function") {
                      try {
                        Promise.resolve(retryOn(attempt, error, null)).then(function(retryOnResponse) {
                          if (retryOnResponse) {
                            retry(attempt, error, null);
                          } else {
                            reject(error);
                          }
                        }).catch(function(error2) {
                          reject(error2);
                        });
                      } catch (error2) {
                        reject(error2);
                      }
                    } else if (attempt < retries) {
                      retry(attempt, error, null);
                    } else {
                      reject(error);
                    }
                  });
                };
                function retry(attempt, error, response) {
                  var delay = typeof retryDelay === "function" ? retryDelay(attempt, error, response) : retryDelay;
                  setTimeout(function() {
                    wrappedFetch(++attempt);
                  }, delay);
                }
                wrappedFetch(0);
              });
            };
          }
          function isPositiveInteger(value) {
            return Number.isInteger(value) && value >= 0;
          }
          function ArgumentError(message) {
            this.name = "ArgumentError";
            this.message = message;
          }

          // lambda/lib-cloudflare.js
          var BASIC_WAIT_TIME_IN_MILLIS = 300;
          var MAX_WAIT_TIME_IN_MILLIS = 5e3;
          var DEFAULT_TIMEOUT_IN_MILLIS = 1e4;
          async function fetch2(url, options) {
            options.signal = AbortSignal.timeout(DEFAULT_TIMEOUT_IN_MILLIS);
            return fetch_retry_default(global.fetch, {
              retryDelay: (attempt) => {
                const delay = Math.floor(Math.random() * Math.min(MAX_WAIT_TIME_IN_MILLIS, BASIC_WAIT_TIME_IN_MILLIS * Math.pow(2, attempt)));
                options.signal = AbortSignal.timeout(DEFAULT_TIMEOUT_IN_MILLIS + delay);
                return delay;
              },
              retryOn: (attempt, error, response) => {
                if (attempt >= 3) {
                  return false;
                }
                if (error || response.status === 429 || response.status >= 500) {
                  return true;
                }
              }
            })(url, options);
          }
          async function readR2EventNotification(apiToken, accountId, bucketName) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/event_notifications/r2/${bucketName}/configuration`, {
              method: "GET",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return [];
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
            return body.result;
          }
          async function createR2EventNotification(apiToken, accountId, bucketName, queueId) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/event_notifications/r2/${bucketName}/configuration/queues/${queueId}`, {
              method: "PUT",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiToken}`
              },
              body: JSON.stringify({
                rules: [{
                  actions: ["PutObject", "CopyObject", "CompleteMultipartUpload"]
                }]
              })
            });
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }
          async function deleteR2EventNotification(apiToken, accountId, bucketName, queueId) {
            const response = await fetch2(`https://api.cloudflare.com/client/v4/accounts/${accountId}/event_notifications/r2/${bucketName}/configuration/queues/${queueId}`, {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiToken}`
              }
            });
            if (response.status === 404) {
              return;
            }
            if (response.status !== 200) {
              console.log("response url", response.url);
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
            const body = await response.json();
            if (body.success !== true) {
              console.log("body", body);
              throw new Error(`unsuccessful request: ${body.errors.map((error) => `${error.code}: ${error.message}`).join(", ")}`);
            }
          }

          // lambda/lib.js
          function includesBucket(scheduledStack, bucketName) {
            let excludeFilterExpression = "^$";
            if (scheduledStack.params.ExcludeBucketNameFilter) {
              excludeFilterExpression = "^" + scheduledStack.params.ExcludeBucketNameFilter.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
            }
            if (bucketName.match(new RegExp(excludeFilterExpression))) {
              return false;
            }
            if (scheduledStack.params.BucketName.includes("*")) {
              const filterExpression = "^" + scheduledStack.params.BucketName.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
              return bucketName.match(new RegExp(filterExpression)) !== null;
            } else {
              return scheduledStack.params.BucketName.split(",").includes(bucketName);
            }
          }
          function hasBucket(scheduledStack, bucketName) {
            if (scheduledStack.params.BucketName === bucketName && (scheduledStack.params.ExcludeBucketNameFilter === "" || !("ExcludeBucketNameFilter" in scheduledStack.params))) {
              return true;
            }
            return false;
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function enrichR2Bucket(cloudflareApiToken, cloudflareAccountId, cloudflareQueueId, scheduledStacks, bucketName) {
            const bucket = { name: bucketName };
            let realtimeEnabled = false;
            let realtimeEventNotificationEnablePossible = true;
            let realtimeEventNotificationDisablePossible = false;
            let scheduledEnabled = false;
            let scheduledStackDisablePossible = false;
            let scheduledStackId = void 0;
            try {
              const eventNotification = await readR2EventNotification(cloudflareApiToken, cloudflareAccountId, bucketName);
              if (eventNotification?.queues?.length >= 5) {
                realtimeEventNotificationEnablePossible = false;
              }
              const queue = eventNotification?.queues?.find((queue2) => queue2.queueId.replaceAll("-", "") === cloudflareQueueId);
              if (queue !== void 0) {
                realtimeEventNotificationEnablePossible = false;
                if (queue.rules.find((rule) => rule.actions.length === 3 && rule.actions.includes("PutObject") && rule.actions.includes("CopyObject") && rule.actions.includes("CompleteMultipartUpload") && rule.prefix === "" && rule.suffix === "") !== void 0) {
                  realtimeEnabled = true;
                  realtimeEventNotificationDisablePossible = true;
                }
              }
            } catch (err) {
              console.log(err);
              bucket.errorMessage = `Can not get details for bucket ${bucket.name}: ${err.name}`;
              realtimeEventNotificationEnablePossible = false;
            }
            const scheduledStacksIncludesBucket = scheduledStacks.stacks.filter((scheduledStack) => includesBucket(scheduledStack, bucketName));
            if (scheduledStacksIncludesBucket.length > 0) {
              scheduledEnabled = true;
              scheduledStackId = scheduledStacksIncludesBucket[0].id;
            }
            const scheduledStacksHasBucket = scheduledStacks.stacks.filter((scheduledStack) => hasBucket(scheduledStack, bucketName));
            if (scheduledStacksHasBucket.length === 1) {
              scheduledEnabled = true;
              scheduledStackDisablePossible = true;
              scheduledStackId = scheduledStacksHasBucket[0].id;
            }
            return {
              ...bucket,
              realtimeEnabled,
              realtimeEventNotificationEnablePossible,
              realtimeEventNotificationDisablePossible,
              scheduledEnabled,
              scheduledStackDisablePossible,
              scheduledStackId
            };
          }
          async function getScheduledStacks(ssm, cloudformation, coreStackName) {
            const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm }, { Recursive: true, Path: `/bucketAV/${coreStackName}/AddOn/scheduled-bucket-scan/` });
            const stacks = [];
            for await (const page of paginatorGetParametersByPath) {
              const scheduledStackNames = page.Parameters.filter((p) => p.Name.endsWith("/Version")).map((p) => p.Name.split("/")[5]);
              const describeStacksDataList = await Promise.all(scheduledStackNames.map((stackName) => cloudformation.send(new import_client_cloudformation.DescribeStacksCommand({ StackName: stackName }))));
              describeStacksDataList.forEach((describeStacksData) => {
                const stack = {
                  name: describeStacksData.Stacks[0].StackName,
                  id: describeStacksData.Stacks[0].StackId,
                  params: describeStacksData.Stacks[0].Parameters?.reduce((acc, param) => {
                    acc[param.ParameterKey] = param.ParameterValue;
                    return acc;
                  }, {}) || {},
                  outputs: describeStacksData.Stacks[0].Outputs?.reduce((acc, output) => {
                    acc[output.OutputKey] = output.OutputValue;
                    return acc;
                  }, {}) || {}
                };
                if (stack.params.BucketAVStackName === coreStackName && stack.outputs.AddOn === "scheduled-bucket-scan") {
                  stacks.push(stack);
                }
              });
            }
            return {
              stacks
            };
          }
          async function listR2Buckets(s3, ssm, cloudformation, coreStackName, cloudflareApiToken, cloudflareAccountId, cloudflareQueueId) {
            const listBucketsData = await s3.send(new import_client_s3.ListBucketsCommand({}));
            const scheduledStacks = await getScheduledStacks(ssm, cloudformation, coreStackName);
            const buckets = await Promise.all(listBucketsData.Buckets.map((bucket) => enrichR2Bucket(cloudflareApiToken, cloudflareAccountId, cloudflareQueueId, scheduledStacks, bucket.Name)));
            return buckets;
          }
          async function fetchR2(ssm, secretsmanager2, coreStackName) {
            const { Parameter: { Value: cloudflareR2EndpointHostname } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareR2EndpointHostname` }));
            const { Parameter: { Value: cloudflareAccessKeyId } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeyId` }));
            const { Parameter: { Value: cloudflareAccessKeySecretArn } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeySecretArn` }));
            const { SecretString: cloudflareAccessKey } = await secretsmanager2.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: cloudflareAccessKeySecretArn }));
            return new import_client_s32.S3Client({
              region: "auto",
              endpoint: `https://${cloudflareR2EndpointHostname}`,
              credentials: {
                accessKeyId: cloudflareAccessKeyId,
                secretAccessKey: cloudflareAccessKey
              }
            });
          }
          async function fetchCloudflareApiToken(secretsmanager2, cloudflareApiTokenSecretArn) {
            const { SecretString: cloudflareApiToken } = await secretsmanager2.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: cloudflareApiTokenSecretArn }));
            return cloudflareApiToken;
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;
          function generateRoleArn(partition, accountId, roleName) {
            return `arn:${partition}:iam::${accountId}:role/${roleName}`;
          }
          function generateRoleArnFromItem(accountConnectionItem) {
            return generateRoleArn(accountConnectionItem.partition.S, accountConnectionItem.account_id.S, accountConnectionItem.role_name.S);
          }
          function generateExternalId(stackId) {
            return stackId.split("/")[2];
          }
          function generateExternalIdFromItem(accountConnectionItem) {
            return generateExternalId(accountConnectionItem.stack_id.S);
          }
          async function fetchLatest() {
            const url = "https://bucketav-release-data.s3.eu-west-1.amazonaws.com/latest.json";
            const res = await fetch(url);
            if (res.status !== 200) {
              console.log("request", url);
              console.log("response status", res.status);
              console.log("response", await res.text());
              throw new Error("unexpected status code");
            }
            return res.json();
          }
          async function fetchOutdatedAddons(defaultCloudformation2, defaultSsm2, dynamodb2, platform, latest, isCrossAccount, corePartition, coreRegion, coreAccountId, coreStackName, accountConnectionTableName) {
            const outdatedAddons = [];
            const fn = async (ssm, cloudformation, partition, region, accountId) => {
              const cloudformaionDescribeStacksLimit = pLimit(8);
              const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm }, { Recursive: true, Path: `/bucketAV/${coreStackName}/AddOn/` });
              for await (const page of paginatorGetParametersByPath) {
                const addons = page.Parameters.filter((p) => p.Name.endsWith("/Version")).map((p) => {
                  const [, , , , addonType, addonStackName] = p.Name.split("/");
                  const addonVersion = p.Value;
                  const addonId = `add-on-${addonType}-${platform}`;
                  const addon = {
                    type: addonType,
                    partition,
                    region,
                    accountId,
                    stackName: addonStackName,
                    version: addonVersion,
                    latestVersion: latest[addonId].version.substr(1),
                    releaseNotesPageUrl: latest[addonId].releaseNotesPageUrl
                  };
                  if ("template" in latest[addonId]) {
                    addon.latestTemplateUrl = latest[addonId].template;
                  }
                  if ("templates" in latest[addonId]) {
                    addon.latestTemplateUrls = latest[addonId].templates;
                  }
                  return addon;
                });
                const innerOutdatedAddons = await Promise.all(addons.filter((addon) => addon.version !== addon.latestVersion).map((addon) => cloudformaionDescribeStacksLimit(async () => {
                  const data = await cloudformation.send(new import_client_cloudformation.DescribeStacksCommand({ StackName: addon.stackName })).then((data2) => data2.Stacks[0]);
                  addon.stackId = data.StackId;
                  if (!("latestTemplateUrl" in addon)) {
                    if ("latestTemplateUrls" in addon) {
                      const engine = data?.Outputs?.find((output) => output.OutputKey === "Engine")?.OutputValue;
                      const fulfillmentOption = data?.Outputs?.find((output) => output.OutputKey === "FulfillmentOption")?.OutputValue;
                      addon.latestTemplateUrl = addon.latestTemplateUrls?.[engine]?.[fulfillmentOption];
                      delete addon.latestTemplateUrls;
                    } else {
                      throw new Error("missing latestTemplateUrl and latestTemplateUrls");
                    }
                  }
                  return addon;
                })));
                outdatedAddons.push(...innerOutdatedAddons);
              }
            };
            if (isCrossAccount === true) {
              await fn(defaultSsm2, defaultCloudformation2, corePartition, coreRegion, coreAccountId);
              const paginatorScan = await (0, import_client_dynamodb.paginateScan)({ client: dynamodb2 }, { TableName: accountConnectionTableName });
              for await (const page of paginatorScan) {
                await Promise.all(page.Items.map((item) => {
                  const externalId = generateExternalIdFromItem(item);
                  const roleArn = generateRoleArnFromItem(item);
                  const credentials = (0, import_credential_providers.fromTemporaryCredentials)({
                    params: {
                      ExternalId: externalId,
                      RoleArn: roleArn,
                      RoleSessionName: "bucketav"
                    }
                  });
                  const ssm = new import_client_ssm.SSMClient({ apiVersion: "2014-11-06", credentials });
                  const cloudformation = new import_client_cloudformation.CloudFormationClient({ apiVersion: "2006-03-01", credentials, maxAttempts: 10 });
                  return fn(ssm, cloudformation, item.partition.S, item.region.S, item.account_id.S);
                }));
              }
            } else {
              await fn(defaultSsm2, defaultCloudformation2, corePartition, coreRegion, coreAccountId);
            }
            return outdatedAddons;
          }
          async function checkVersion(cloudformation, ssm, dynamodb2, platform, latest, isCrossAccount, corePartition, coreRegion, coreAccountId, coreStackName, accountConnectionTableName) {
            const describeStacksData = await cloudformation.send(new import_client_cloudformation.DescribeStacksCommand({ StackName: coreStackName }));
            const runningEngine = describeStacksData.Stacks[0].Outputs.find((output) => output.OutputKey === "Engine").OutputValue;
            const runningVersion = describeStacksData.Stacks[0].Outputs.find((output) => output.OutputKey === "Version").OutputValue;
            const runningFulfillmentOption = describeStacksData.Stacks[0].Outputs.find((output) => output.OutputKey === "FulfillmentOption").OutputValue;
            const outdatedAddons = await fetchOutdatedAddons(cloudformation, ssm, dynamodb2, platform, latest, isCrossAccount, corePartition, coreRegion, coreAccountId, coreStackName, accountConnectionTableName);
            const coreId = coreRegion.startsWith("eusc-") ? `core-${platform}-${runningEngine}-eusc` : `core-${platform}-${runningEngine}`;
            const latestVersion = latest[coreId].version.substr(1);
            const latestTemplateUrl = latest[coreId].templates[runningFulfillmentOption];
            return {
              runningVersion,
              latestVersion,
              latestTemplateUrl,
              outdatedAddons
            };
          }

          // lambda/lib-dashboard.js
          var import_client_cloudwatch_logs = require("@aws-sdk/client-cloudwatch-logs");
          var import_client_s34 = require("@aws-sdk/client-s3");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_s35 = require("@aws-sdk/client-s3");
          var import_client_sfn = require("@aws-sdk/client-sfn");
          var import_client_dynamodb3 = require("@aws-sdk/client-dynamodb");

          // lambda/lib-refresh-bucket-cache.js
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_sns2 = require("@aws-sdk/client-sns");
          var import_client_eventbridge2 = require("@aws-sdk/client-eventbridge");
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_credential_providers2 = require("@aws-sdk/credential-providers");

          // lambda/lib-dashboard.js
          var SCAN_RESULTS_MESSAGE_PARSER_REGEX = /s3:\/\/(?<bucketName>[^\\/]+)\/(?<objectKey>.*)\s(?<rawStatus>(is clean|is infected|could not be scanned because it is|does no longer exist|not downloadable|access denied))/;
          var LOGS_QUERY_CHECKS_INTERVAL = 500;
          var MAX_LOGS_QUERY_CHECKS = 20;
          var QUERY_SCAN_RESULTS_LIMIT = 1e4;
          var sortConstants = {
            DESC: "desc",
            ASC: "asc"
          };
          var filterConstants = {
            DEFAULT: "default",
            ENABLED: "enabled",
            DISABLED: "disabled"
          };
          var statusFilterConstants = {
            ALL: "all",
            CLEAN: "clean",
            INFECTED: "infected",
            UNSCANNABLE: "unscannable"
          };
          function createUrl(region, templateUrl, stackName, params) {
            let url = "";
            if (region.startsWith("eusc-")) {
              url = `https://console.amazonaws-eusc.eu/cloudformation/home?region=${region}#/stacks/create/review?templateURL=${encodeURIComponent(templateUrl)}&stackName=${encodeURIComponent(stackName)}`;
            } else {
              url = `https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/create/review?templateURL=${encodeURIComponent(templateUrl)}&stackName=${encodeURIComponent(stackName)}`;
            }
            Object.keys(params).forEach((key) => {
              url += `&param_${key}=${encodeURIComponent(params[key])}`;
            });
            return url;
          }
          function detailsUrl(region, stackId) {
            if (region.startsWith("eusc-")) {
              return `https://console.amazonaws-eusc.eu/cloudformation/home?region=${region}#/stacks/parameters?stackId=${encodeURIComponent(stackId)}`;
            } else {
              return `https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/parameters?stackId=${encodeURIComponent(stackId)}`;
            }
          }
          function createInstructions(coreAccountId, region, optionalAccountId, templateUrl, stackName, params) {
            let html = "<ol>";
            if (optionalAccountId !== coreAccountId) {
              if (optionalAccountId === void 0) {
                html += "<li>Login to the new AWS account in a fresh browser session.</li>";
              } else {
                html += `<li>Login to AWS account ${optionalAccountId} in a fresh browser session.</li>`;
              }
            }
            html += `<li><a href="${createUrl(region, templateUrl, stackName, params)}" target="_blank">Open AWS CloudFormation</a>.</li>`;
            html += "<li>Review the parameters.</li>";
            html += "<li>Scroll to the bottom of the page.</li>";
            html += "<li>Enable <strong>I acknowledge that AWS CloudFormation might create IAM resources</strong>.</li>";
            html += "<li>Click <strong>Create stack</strong>.</li>";
            html += "</ol>";
            return html;
          }
          function deleteInstructions(coreAccountId, region, accountId, stackId) {
            let html = "<ol>";
            if (accountId !== coreAccountId) {
              html += `<li>Login to AWS account ${accountId} in fresh browser session.</li>`;
            }
            html += `<li><a href="${detailsUrl(region, stackId)}" target="_blank">Open AWS CloudFormation</a>.</li>`;
            html += "<li>Click <strong>Delete</strong>.</li>";
            html += "</ol>";
            return html;
          }
          function updateUrl(region, stackId, optionalTemplateUrl) {
            if (region.startsWith("eusc-")) {
              if (optionalTemplateUrl !== void 0) {
                return `https://console.amazonaws-eusc.eu/cloudformation/home?region=${region}#/stacks/update?stackId=${encodeURIComponent(stackId)}&templateURL=${optionalTemplateUrl}`;
              } else {
                return `https://console.amazonaws-eusc.eu/cloudformation/home?region=${region}#/stacks/update?stackId=${encodeURIComponent(stackId)}`;
              }
            } else {
              if (optionalTemplateUrl !== void 0) {
                return `https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/update?stackId=${encodeURIComponent(stackId)}&templateURL=${optionalTemplateUrl}`;
              } else {
                return `https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/update?stackId=${encodeURIComponent(stackId)}`;
              }
            }
          }
          function updateInstructions(coreAccountId, region, accountId, stackId, optionalTemplateUrl, optionalParameterInstructions) {
            let html = "<ol>";
            if (accountId !== coreAccountId) {
              html += `<li>Login to AWS account ${accountId} in fresh browser session.</li>`;
            }
            html += `<li><a href="${updateUrl(region, stackId, optionalTemplateUrl)}" target="_blank">Open AWS CloudFormation</a>.</li>`;
            html += "<li>Click <strong>Next</strong>.</li>";
            if (optionalParameterInstructions !== void 0) {
              html += `<li>${optionalParameterInstructions}</li>`;
            }
            html += "<li>Click <strong>Next</strong>.</li>";
            html += "<li>Select <strong>I acknowledge that AWS CloudFormation might create IAM resources</strong> and click <strong>Next</strong>.</li>";
            html += "<li>Click <strong>Submit</strong>.</li>";
            html += "</ol>";
            return html;
          }
          async function update(defaultCloudformation2, defaultSsm2, dynamodb2, platform, latest, crossAccount, awsPartition, awsRegion, awsAccountId, coreStackName, coreStackId, engine, fulfillment, accountConnectionTableName) {
            const { runningVersion, latestVersion, latestTemplateUrl, outdatedAddons } = await checkVersion(defaultCloudformation2, defaultSsm2, dynamodb2, platform, latest, crossAccount, awsPartition, awsRegion, awsAccountId, coreStackName, accountConnectionTableName);
            let html = 'Monthly digest of security updates, new capabilities, and best practices: <a href="https://bucketav.com/newsletter/" target="_blank">Subscribe to the bucketAV newsletter!</a>';
            html += "<hr>";
            if (latestVersion !== runningVersion) {
              html += "<p>\u26A0\uFE0F bucketAV requires an update.</p>";
              html += '<table style="width:100%">';
              html += "<thead>";
              html += "<tr>";
              html += "<th>CloudFormation<br>Stack&nbsp;Name</th>";
              html += "<th>Version</th>";
              html += "<th>&nbsp;</th>";
              html += "</tr>";
              html += "</thead>";
              html += "<tbody>";
              html += "<tr>";
              html += `<td>${coreStackName}</td>`;
              html += `<td>Running:&nbsp;${runningVersion}<br>Latest:&nbsp;${latestVersion}</td>`;
              html += '<td><a class="btn btn-primary">Update</a>';
              html += '<cwdb-action display="popup">';
              html += "<h1>Update bucketAV</h1>";
              html += "<p>bucketAV supports updates without downtime. You don\u2019t need to be afraid of updating bucketAV, even when files are scanned.</p>";
              html += updateInstructions(awsAccountId, awsRegion, awsAccountId, coreStackId, latestTemplateUrl);
              const anchor = platform === "aws" && (engine === "clamav" || engine === "sophos") ? engine : `core-${platform}-${engine}`;
              html += `</cwdb-action><br><a href="https://bucketav.com/help/release-notes/?platform=${platform}&engine=${engine}&fulfillment=${fulfillment}#${anchor}" target="_blank">Release&nbsp;Notes</a>`;
              html += "</td>";
              html += "</tr>";
              html += "</tbody>";
              html += "</table>";
            } else {
              html += "<p>\u2705 bucketAV is up-to-date.</p>";
            }
            if (latestVersion !== runningVersion && outdatedAddons.length > 0) {
              html += "<hr>";
            }
            if (outdatedAddons.length > 0) {
              html += "<p>\u26A0\uFE0F Add-Ons require updates*.</p>";
              html += '<table style="width:100%">';
              html += "<thead>";
              html += "<tr>";
              html += "<th>Add-On</th>";
              html += "<th>AWS Account</th>";
              html += "<th>CloudFormation<br>Stack&nbsp;Name</th>";
              html += "<th>Version</th>";
              html += "<th>&nbsp;</th>";
              html += "</tr>";
              html += "</thead>";
              html += "<tbody>";
              for (const outdatedAddon of outdatedAddons) {
                html += "<tr>";
                html += `<td>${outdatedAddon.type}</td>`;
                html += `<td>${outdatedAddon.accountId}</td>`;
                html += `<td>${outdatedAddon.stackName}</td>`;
                html += `<td>Running:&nbsp;${outdatedAddon.version}<br>Latest:&nbsp;${outdatedAddon.latestVersion}</td>`;
                html += '<td><a class="btn btn-primary">Update</a>';
                html += '<cwdb-action display="popup">';
                html += "<h1>Update Add-On</h1>";
                html += `<p>To update Add-On ${outdatedAddon.type} (stack name ${outdatedAddon.stackName}) in AWS account ${outdatedAddon.accountId}:</p>`;
                html += updateInstructions(awsAccountId, outdatedAddon.region, outdatedAddon.accountId, outdatedAddon.stackId, outdatedAddon.latestTemplateUrl);
                html += `</cwdb-action><br><a href="${outdatedAddon.releaseNotesPageUrl}" target="_blank">Release&nbsp;Notes</a>`;
                html += "</td>";
                html += "</tr>";
              }
              html += "</tbody>";
              html += "</table>";
            } else {
              html += "<p>\u2705 Add-Ons are up-to-date*.</p>";
            }
            html += '<p><small>* Add-Ons with <a href="https://bucketav.com/add-ons/#service-discovery" target="_blank">service discovery</a> are included in the out-of-date check. Please update older Add-Ons manually.</small></p>';
            return html;
          }
          function htmlRealtime(bucket, lambdaArn, platform, engine, fulfillment) {
            if ("errorMessage" in bucket) {
              return `\u26A0\uFE0F Error (<a>Details</a>)<cwdb-action display="popup"><h1>Real-time file scan</h1>An error occurred: ${bucket.errorMessage}.</cwdb-action>`;
            } else if (bucket.realtimeEnabled === true) {
              if (bucket.realtimeEventNotificationDisablePossible === true) {
                return `\u2705 <a class="btn">Disable</a><cwdb-action action="call" endpoint="${lambdaArn}">{"action": "disableEventNotification", "bucketName": "${bucket.name}", "bucketRegion": "${bucket.region}", "bucketAccountId": "${bucket.accountId}"}</cwdb-action>`;
              } else {
                return `\u2705 <a class="btn">Disable</a><cwdb-action display="popup"><h1>Real-time file scan</h1>It is not possible to disable real-time file scanning automatically. To manually disable real-time file scanning, please follow our <a href="https://bucketav.com/help/scan-modes/real-time-file-scan.html?platform=${platform}&engine=${engine}&fulfillment=${fulfillment}" target="_blank">documentation</a>.</cwdb-action>`;
              }
            } else {
              if (bucket.realtimeEventNotificationEnablePossible === true) {
                return `\u274C <a class="btn btn-primary">Enable</a><cwdb-action action="call" endpoint="${lambdaArn}">{"action": "enableEventNotification", "bucketName": "${bucket.name}", "bucketRegion": "${bucket.region}", "bucketAccountId": "${bucket.accountId}"}</cwdb-action>`;
              } else {
                return `\u274C <a class="btn btn-primary">Enable</a><cwdb-action display="popup"><h1>Real-time file scan</h1>It is not possible to enable real-time file scanning automatically because of an existing S3 Event Notification or EventBridge configuration. To manually enable real-time file scanning, please follow our <a href="https://bucketav.com/help/scan-modes/real-time-file-scan.html?platform=${platform}&engine=${engine}&fulfillment=${fulfillment}" target="_blank">documentation</a>.</cwdb-action>`;
              }
            }
          }
          function htmlScheduled(bucket, region, coreAccountId, coreStackName, scheduledTemplateUrl, platform, engine, fulfillment) {
            let scheduledStackName = "bucketav-scheduled-bucket-scan-";
            scheduledStackName += bucket.name.replaceAll(".", "D");
            if ("errorMessage" in bucket) {
              return `\u26A0\uFE0F Error (<a>Details</a>)<cwdb-action display="popup"><h1>Scheduled bucket scan</h1>An error occurred: ${bucket.errorMessage}.</cwdb-action>`;
            } else if (bucket.scheduledEnabled === true) {
              if (bucket.scheduledStackDisablePossible === true) {
                let html = '\u2705 <a class="btn">Disable</a>';
                html += '<cwdb-action display="popup">';
                html += "<h1>Scheduled bucket scan</h1>";
                html += `<p>To disable scheduled bucket scanning for bucket ${bucket.name}:</p>`;
                html += deleteInstructions(coreAccountId, region, coreAccountId, bucket.scheduledStackId);
                html += "</cwdb-action>";
                return html;
              } else {
                let html = '\u2705 <a class="btn">Disable</a>';
                html += '<cwdb-action display="popup">';
                html += "<h1>Scheduled bucket scan</h1>";
                html += `<p>It is not yet possible to disable scheduled bucket scanning for bucket ${bucket.name} automatically. `;
                if (bucket.scheduledStackId) {
                  html += `The CloudFormation stack ${bucket.scheduledStackId} scans this bucket.`;
                }
                html += `To manually disable scheduled bucket scanning, please follow our <a href="https://bucketav.com/help/scan-modes/scheduled-bucket-scan.html?platform=${platform}&engine=${engine}&fulfillment=${fulfillment}" target="_blank">documentation</a>.</p>`;
                html += "</cwdb-action>";
                return html;
              }
            } else {
              let html = '\u274C <a class="btn btn-primary">Enable</a>';
              html += '<cwdb-action display="popup">';
              html += "<h1>Scheduled bucket scan</h1>";
              html += `<p>To enable scheduled bucket scanning for bucket ${bucket.name}:</p>`;
              html += createInstructions(coreAccountId, region, coreAccountId, scheduledTemplateUrl, scheduledStackName, { BucketAVStackName: coreStackName, BucketName: bucket.name });
              html += "</cwdb-action>";
              return html;
            }
          }
          async function bucketsBase(event, context, buckets, scheduledTemplateUrl, platform, awsRegion, awsAccountId, crossAccount, coreStackName, engine, fulfillment) {
            const bucketSearch = event?.widgetContext?.forms?.all?.bucketSearch || event.bucketSearch || "";
            if (bucketSearch !== "") {
              buckets = buckets.filter((bucket) => bucket.name.includes(bucketSearch));
            }
            const accountSearch = event?.widgetContext?.forms?.all?.accountSearch || event.accountSearch || "";
            if (accountSearch !== "") {
              buckets = buckets.filter((bucket) => bucket.accountId.includes(accountSearch));
            }
            buckets = filterByRealtimeScan(buckets, event.widgetContext.forms?.all?.realtimeScanFilter);
            buckets = filterByScheduledScan(buckets, event.widgetContext.forms?.all?.scheduledScanFilter);
            buckets.sort((bucket1, bucket2) => bucket1.name.localeCompare(bucket2.name));
            const page = "page" in event ? event.page : 0;
            const limit = 100;
            const pageStartInclusive = page * limit;
            const pageEndExclusive = pageStartInclusive + limit;
            const tbody = buckets.slice(pageStartInclusive, pageEndExclusive).map((bucket) => {
              let html2 = "<tr>";
              html2 += `<td>${bucket.name}</td>`;
              if (platform === "aws") {
                html2 += `<td>${bucket.accountId}</td>`;
              }
              html2 += `<td>${htmlRealtime(bucket, context.invokedFunctionArn, platform, engine, fulfillment)}</td>`;
              html2 += `<td>${htmlScheduled(bucket, awsRegion, awsAccountId, coreStackName, scheduledTemplateUrl, platform, engine, fulfillment)}</td>`;
              html2 += "</tr>";
              return html2;
            }).join("");
            let html = "<form>";
            html += '<table style="width:100%">';
            if (platform === "aws") {
              if (crossAccount) {
                html += `<caption>Includes buckets from AWS region ${awsRegion}.<br>Buckets are cached and may be stale (<a>Refresh bucket cache</a>).<cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{"action": "refreshBucketCache"}</cwdb-action></caption>`;
              } else {
                html += `<caption>Includes buckets from AWS region ${awsRegion}.</caption>`;
              }
            }
            html += "<thead>";
            html += "<tr>";
            html += `<th><input type="text" name="bucketSearch" placeholder="Bucket" value="${bucketSearch}" style="width:90%;margin-bottom:0">&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
            if (platform === "aws") {
              html += `<th><input type="text" name="accountSearch" placeholder="AWS Account" value="${accountSearch}" style="width:90%;margin-bottom:0">&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
            }
            const realtimeScanSelect = buildFilterSelect("realtimeScanFilter", "Real-time file scan", event.widgetContext.forms?.all?.realtimeScanFilter);
            html += `<th>${realtimeScanSelect}&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
            const scheduledScanSelect = buildFilterSelect("scheduledScanFilter", "Scheduled bucket scan", event.widgetContext.forms?.all?.scheduledScanFilter);
            html += `<th>${scheduledScanSelect}&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
            html += "</tr>";
            html += "</thead>";
            html += `<tbody>${tbody}</tbody>`;
            if (platform === "aws" && crossAccount) {
              html += "<tfoot>";
              html += "<tr>";
              html += '<th colspan="4" style="text-align:center;">';
              if (pageStartInclusive > 0) {
                html += `<a>Previous</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{"page": ${page - 1}}</cwdb-action>`;
              }
              if (pageStartInclusive > 0 && pageEndExclusive < buckets.length) {
                html += "&nbsp;|&nbsp;";
              }
              if (pageEndExclusive < buckets.length) {
                html += `<a>Next</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{"page": ${page + 1}}</cwdb-action>`;
              }
              html += "</th>";
              html += "</tr>";
              html += "</tfoot>";
            }
            html += "</table>";
            html += "</form>";
            return html;
          }
          async function bucketsCloudflare(defaultSsm2, defaultCloudformation2, secretsmanager2, event, context, scheduledTemplateUrl, awsRegion, awsAccountId, coreStackName, cloudflareAccountId, cloudflareApiTokenSecretArn, cloudflareQueueId, engine, fulfillment) {
            const cloudflareApiToken = await fetchCloudflareApiToken(secretsmanager2, cloudflareApiTokenSecretArn);
            if (event.action === "enableEventNotification") {
              await createR2EventNotification(cloudflareApiToken, cloudflareAccountId, event.bucketName, cloudflareQueueId);
            } else if (event.action === "disableEventNotification") {
              await deleteR2EventNotification(cloudflareApiToken, cloudflareAccountId, event.bucketName, cloudflareQueueId);
            }
            const s3 = await fetchR2(defaultSsm2, secretsmanager2, coreStackName);
            const buckets = await listR2Buckets(s3, defaultSsm2, defaultCloudformation2, coreStackName, cloudflareApiToken, cloudflareAccountId, cloudflareQueueId);
            return bucketsBase(event, context, buckets, scheduledTemplateUrl, "cloudflare", awsRegion, awsAccountId, false, coreStackName, engine, fulfillment);
          }
          async function startQuery(client, logGroupName, logStreamSuffix, startTimeMillis, endTimeMillis, options = {}) {
            const bucketFilter = options.bucketSearch ? `| filter bucketName like '${options.bucketSearch}'` : "";
            const objectFilter = options.objectSearch ? `| filter objectKey like '${options.objectSearch}'` : "";
            const statusFilter = options.statusFilter && options.statusFilter !== statusFilterConstants.ALL ? `| filter status like '${options.statusFilter}'` : "";
            const sort = options.sort || sortConstants.DESC;
            const query = `fields @message
          | filter ${logStreamSuffix ? `@logStream like '${logStreamSuffix}' and ` : ""}@message like 's3://'
          | parse @message /${SCAN_RESULTS_MESSAGE_PARSER_REGEX.source}/
          | fields replace(rawStatus, 'is clean', 'clean') as status2
          | fields replace(status2, 'is infected', 'infected') as status3
          | fields replace(status3, 'could not be scanned because it is', 'unscannable') as status4
          | fields replace(status4, 'does no longer exist', 'unscannable') as status5
          | fields replace(status5, 'not downloadable', 'unscannable') as status6
          | fields replace(status6, 'access denied', 'unscannable') as status
          | filter ispresent(bucketName) and ispresent(objectKey) and ispresent(status)
          ${bucketFilter}
          ${objectFilter}
          ${statusFilter}
          | display @timestamp, @message, bucketName, objectKey, status
          | sort @timestamp ${sort}
          | limit ${QUERY_SCAN_RESULTS_LIMIT}`;
            console.debug(`using query for scan results: ${query}`);
            const startTime = Math.floor(startTimeMillis / 1e3);
            const endTime = Math.floor(endTimeMillis / 1e3);
            const { queryId } = await client.send(new import_client_cloudwatch_logs.StartQueryCommand({
              logGroupName,
              queryLanguage: "CWLI",
              queryString: query,
              startTime,
              endTime
            }));
            return queryId;
          }
          async function waitForQueryResults(client, queryId) {
            let isDone;
            let retryCount = 0;
            let result;
            do {
              if (retryCount > 0) {
                await new Promise((resolve) => setTimeout(resolve, LOGS_QUERY_CHECKS_INTERVAL));
                console.debug(`#${retryCount} retry to get query results for ${queryId}`);
              }
              result = await client.send(new import_client_cloudwatch_logs.GetQueryResultsCommand({
                queryId
              }));
              isDone = result.status !== "Running" && result.status !== "Scheduled";
            } while (!isDone && retryCount++ < MAX_LOGS_QUERY_CHECKS);
            if (!isDone && retryCount >= MAX_LOGS_QUERY_CHECKS) {
              throw new Error(`RUNNING#${queryId}`);
            }
            if (["Cancelled", "Failed", "Timeout", "Unknown"].includes(result.status)) {
              throw new Error(`Could not get scan results: ${result.status}`);
            }
            return result.results;
          }
          async function queryScanResults(client, logGroupName, logStreamSuffix, startTimeMillis, endTimeMillis, options) {
            const queryId = options?.queryId || await startQuery(client, logGroupName, logStreamSuffix, startTimeMillis, endTimeMillis, options);
            const results = await waitForQueryResults(client, queryId);
            return results.map((logRow) => ({
              timestamp: logRow.find((e) => e.field === "@timestamp")?.value,
              rawMessage: logRow.find((e) => e.field === "@message")?.value,
              bucketName: logRow.find((e) => e.field === "bucketName")?.value,
              objectKey: logRow.find((e) => e.field === "objectKey")?.value?.replace(/\s[^\s]*$/, ""),
              // Due to limitatiosn with regular expresion, remove last whitespace and everything afterwards from object key, as it is most likely the object version.
              status: logRow.find((e) => e.field === "status")?.value
            }));
          }
          function buildFilterSelectOption(label, value, isSelected) {
            return `<option value="${value}"${isSelected ? ' selected="selected"' : ""}>${label}</option>`;
          }
          function buildFilterSelect(selectName, title, selectedOptionValue) {
            let select = `<select name="${selectName}">`;
            select += buildFilterSelectOption(title, filterConstants.DEFAULT, !selectedOptionValue || selectedOptionValue === filterConstants.DEFAULT);
            select += buildFilterSelectOption(`\u2705 ${title}`, filterConstants.ENABLED, selectedOptionValue === filterConstants.ENABLED);
            select += buildFilterSelectOption(`\u274C ${title}`, filterConstants.DISABLED, selectedOptionValue === filterConstants.DISABLED);
            select += "</select>";
            return select;
          }
          function buildScanStatusDropdown(selectName, selectedOptionValue) {
            let select = `<select name="${selectName}">`;
            select += buildFilterSelectOption("All", statusFilterConstants.ALL, !selectedOptionValue || selectedOptionValue === statusFilterConstants.ALL);
            select += buildFilterSelectOption("Clean", statusFilterConstants.CLEAN, selectedOptionValue === statusFilterConstants.CLEAN);
            select += buildFilterSelectOption("Infected", statusFilterConstants.INFECTED, selectedOptionValue === statusFilterConstants.INFECTED);
            select += buildFilterSelectOption("Unscannable", statusFilterConstants.UNSCANNABLE, selectedOptionValue === statusFilterConstants.UNSCANNABLE);
            select += "</select>";
            return select;
          }
          function buildTextSearchHeader(context, columnName, placeholder, inputValue) {
            return `<th><input type="text" name="${columnName}" placeholder="${placeholder}" value="${inputValue}" style="width:90%;margin-bottom:0">&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
          }
          function createHtmlLink(link, text, openNewTab = true) {
            return `<a href="${link}"${openNewTab ? ' target="_blank" rel="noopener noreferrer"' : ""}>${text}</a>`;
          }
          function filterByRealtimeScan(buckets, filterValue) {
            if (!!filterValue && filterValue !== filterConstants.DEFAULT) {
              const isEnabled = filterValue === filterConstants.ENABLED;
              return buckets.filter((bucket) => bucket.realtimeEnabled === isEnabled);
            }
            return buckets;
          }
          function filterByScheduledScan(buckets, filterValue) {
            if (!!filterValue && filterValue !== filterConstants.DEFAULT) {
              const isEnabled = filterValue === filterConstants.ENABLED;
              return buckets.filter((bucket) => bucket.scheduledEnabled === isEnabled);
            }
            return buckets;
          }
          async function getAllReportingBuckets(ssmClient, coreStackName) {
            const bucketNames = [];
            const paginator = (0, import_client_ssm2.paginateGetParametersByPath)({ client: ssmClient }, {
              Path: `/bucketAV/${coreStackName}/AddOn/reporting/`,
              Recursive: true
            });
            for await (const page of paginator) {
              const bucketNamesOfResponse = page.Parameters?.filter((entry) => entry.Name.endsWith("/BucketName"))?.map((entry) => entry.Value);
              bucketNames.push(...bucketNamesOfResponse || []);
            }
            return bucketNames;
          }
          async function getStackInfoOfBucket(s3Client, bucketName) {
            const response = await s3Client.send(new import_client_s34.GetBucketTaggingCommand({
              Bucket: bucketName
            }));
            const stackId = response.TagSet?.find((tag) => tag.Key === "aws:cloudformation:stack-id")?.Value;
            const stackName = response.TagSet?.find((tag) => tag.Key === "aws:cloudformation:stack-name")?.Value;
            if (!stackId || !stackName) {
              throw new Error(`missing CloudFormation tags for stack information on bucket ${bucketName}`);
            }
            return {
              stackId,
              stackName
            };
          }
          async function getReportKeysOfBucket(s3Client, bucketName, startDate, endDate, reportTypeSearch = "") {
            const prefix = "report/html/bucketav_report_";
            const reports2 = [];
            let continuationToken = void 0;
            do {
              const response = await s3Client.send(new import_client_s34.ListObjectsV2Command({
                Bucket: bucketName,
                Prefix: prefix,
                ContinuationToken: continuationToken
              }));
              continuationToken = response.NextContinuationToken;
              if (!response.Contents) {
                continue;
              }
              for (const obj of response.Contents) {
                const lastModified = new Date(obj.LastModified);
                if (!(lastModified.getTime() >= startDate.getTime() && lastModified.getTime() <= endDate.getTime())) {
                  continue;
                }
                const key = obj.Key;
                const type = key.replace(prefix, "").split("_")[0];
                if (reportTypeSearch && !type.includes(reportTypeSearch)) {
                  continue;
                }
                reports2.push({ bucket: bucketName, key, type, lastModified });
              }
            } while (continuationToken !== void 0);
            return reports2;
          }
          async function getAllReports(s3Client, reportingBuckets, startTimeMillis, endTimeMillis, stackSearch = "", reportTypeSearch = "") {
            const startDate = new Date(startTimeMillis);
            const endDate = new Date(endTimeMillis);
            const reports2 = [];
            for (const bucket of reportingBuckets) {
              const { stackId, stackName } = await getStackInfoOfBucket(s3Client, bucket);
              if (stackSearch && !stackName.includes(stackSearch)) {
                continue;
              }
              const bucketReports = await getReportKeysOfBucket(s3Client, bucket, startDate, endDate, reportTypeSearch);
              reports2.push(...bucketReports.map((report) => ({ ...report, stackId, stackName })));
            }
            return reports2;
          }
          function sortByLastModified(sort) {
            return (one, two) => {
              if (sort === sortConstants.DESC) {
                return two.lastModified.getTime() - one.lastModified.getTime();
              }
              return one.lastModified.getTime() - two.lastModified.getTime();
            };
          }
          function createLinkToObject(region, bucketName, objectKeyOrPrefix) {
            if (region.startsWith("eusc-")) {
              return `https://console.amazonaws-eusc.eu/s3/object/${encodeURIComponent(bucketName)}?region=${encodeURIComponent(region)}&prefix=${encodeURIComponent(objectKeyOrPrefix)}`;
            } else {
              return `https://s3.console.aws.amazon.com/s3/object/${encodeURIComponent(bucketName)}?region=${encodeURIComponent(region)}&prefix=${encodeURIComponent(objectKeyOrPrefix)}`;
            }
          }
          function createLinkToCloudFormationStack(region, stackId) {
            if (region.startsWith("eusc-")) {
              return `https://console.amazonaws-eusc.eu/cloudformation/home?region=${encodeURIComponent(region)}#/stacks/stackinfo?stackId=${encodeURIComponent(stackId)}`;
            } else {
              return `https://console.aws.amazon.com/cloudformation/home?region=${encodeURIComponent(region)}#/stacks/stackinfo?stackId=${encodeURIComponent(stackId)}`;
            }
          }
          async function scanResults(cwLogs2, event, context, logGroupName, logStreamSuffix = void 0) {
            const startTimeMillis = event.widgetContext.timeRange?.start;
            const endTimeMillis = event.widgetContext.timeRange?.end;
            const queryId = event.widgetContext.forms?.all?.queryId;
            const bucketSearch = event.widgetContext.forms?.all?.bucketSearch || "";
            const objectSearch = event?.widgetContext.forms?.all?.objectSearch || "";
            const statusFilter = event?.widgetContext.forms?.all?.statusFilter || "";
            const sort = event.sort || event.widgetContext.forms?.all?.sort || sortConstants.DESC;
            const hiddenSortInput = `<input type="hidden" name="sort" value="${sort}" />`;
            let hiddenQueryIdInput = '<input type="hidden" name="queryId" />';
            let tbody;
            try {
              const scanResults2 = await queryScanResults(cwLogs2, logGroupName, logStreamSuffix, startTimeMillis, endTimeMillis, {
                queryId,
                bucketSearch,
                objectSearch,
                statusFilter,
                sort
              });
              tbody = scanResults2.map((result) => `<tr><td>${result.timestamp}</td><td>${result.bucketName}</td><td>${result.objectKey}</td><td>${result.status}</td></tr>`).join("");
            } catch (err) {
              if (err.message.startsWith("RUNNING#")) {
                const currentQueryId = err.message.replace("RUNNING#", "");
                tbody = `<tr><td colspan="4">Query is still running...&nbsp;<a title="Retry">\u{1F504}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></td></tr>`;
                hiddenQueryIdInput = `<input type="hidden" name="queryId" value="${currentQueryId}" />`;
              } else {
                tbody = `<tr><td colspan="4">${err.message}</td></tr>`;
              }
            }
            const otherSort = sort === sortConstants.DESC ? sortConstants.ASC : sortConstants.DESC;
            let html = `${hiddenQueryIdInput}${hiddenSortInput}<table style="width:100%">`;
            html += "<thead>";
            html += "<tr>";
            html += `<th>Timestamp&nbsp;<a title="Sort entries in ${otherSort}ending order">${sort === sortConstants.DESC ? "\u{1F53C}" : "\u{1F53D}"}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{"sort": "${otherSort}"}</cwdb-action></th>`;
            html += buildTextSearchHeader(context, "bucketSearch", "Bucket", bucketSearch);
            html += buildTextSearchHeader(context, "objectSearch", "Object", objectSearch);
            html += `<th>${buildScanStatusDropdown("statusFilter", statusFilter)}&nbsp;<a>\u{1F50D}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></th>`;
            html += "</tr>";
            html += "</thead>";
            html += "<tbody>";
            html += tbody;
            html += "</tbody>";
            html += "</table>";
            return html;
          }
          async function reports(defaultS32, defaultSsm2, event, context, awsRegion, coreStackName, platform, engine, fulfillment) {
            const startTimeMillis = event.widgetContext.timeRange?.start;
            const endTimeMillis = event.widgetContext.timeRange?.end;
            const stackSearch = event.widgetContext.forms?.all?.stackSearch || "";
            const reportTypeSearch = event.widgetContext.forms?.all?.reportTypeSearch || "";
            const sort = event.sort || event.widgetContext.forms?.all?.sort || sortConstants.DESC;
            const hiddenSortInput = `<input type="hidden" name="sort" value="${sort}" />`;
            const reportingBuckets = await getAllReportingBuckets(defaultSsm2, coreStackName);
            let tbody;
            if (reportingBuckets.length === 0) {
              tbody = `<tr><td colspan="3">Reporting add-on is not installed. Follow the <a href="https://bucketav.com/help/reporting/reports.html?platform=${platform}&engine=${engine}&fulfillment=${fulfillment}#setup" target="_blank" rel="noopener noreferrer">setup instructions</a> to see your reports.</td></tr>`;
            } else {
              const allReports = await getAllReports(defaultS32, reportingBuckets, startTimeMillis, endTimeMillis, stackSearch, reportTypeSearch);
              tbody = allReports.sort(sortByLastModified(sort)).map((report) => `<tr><td>${report.lastModified.toUTCString()}</td><td>${createHtmlLink(createLinkToCloudFormationStack(awsRegion, report.stackId), report.stackName)}</td><td>${report.type}</td><td>${createHtmlLink(createLinkToObject(awsRegion, report.bucket, report.key), "\u{1F4C1}")}</td></tr>`).join("");
            }
            const otherSort = sort === sortConstants.DESC ? sortConstants.ASC : sortConstants.DESC;
            let html = `${hiddenSortInput}<table style="width:100%">`;
            html += "<thead>";
            html += "<tr>";
            html += `<th>Creation date&nbsp;<a title="Sort entries in ${otherSort}ending order">${sort === sortConstants.DESC ? "\u{1F53C}" : "\u{1F53D}"}</a><cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{"sort": "${otherSort}"}</cwdb-action></th>`;
            html += buildTextSearchHeader(context, "stackSearch", "Stack Name", stackSearch);
            html += buildTextSearchHeader(context, "reportTypeSearch", "Type", reportTypeSearch);
            html += "<th>&nbsp;</th>";
            html += "</tr>";
            html += "</thead>";
            html += "<tbody>";
            html += tbody;
            html += "</tbody>";
            html += "</table>";
            return html;
          }

          // lambda/dashboard-cloudflare.js
          var defaultCloudformation = new import_client_cloudformation2.CloudFormationClient({ apiVersion: "2006-03-01", maxAttempts: 10 });
          var defaultS3 = new import_client_s36.S3Client({ apiVersion: "2006-03-01" });
          var defaultSsm = new import_client_ssm3.SSMClient({ apiVersion: "2014-11-06" });
          var dynamodb = new import_client_dynamodb4.DynamoDBClient({ apiVersion: "2012-08-10" });
          var secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var cwLogs = new import_client_cloudwatch_logs2.CloudWatchLogsClient({ apiVersion: "2014-03-28" });
          var CORE_STACK_NAME = process.env.CORE_STACK_NAME;
          var CORE_STACK_ID = process.env.CORE_STACK_ID;
          var ENGINE = process.env.ENGINE;
          var FULFILLMENT = process.env.FULFILLMENT;
          var AWS_PARTITION = process.env.AWS_PARTITION;
          var AWS_REGION = process.env.AWS_REGION;
          var AWS_ACCOUNT_ID = process.env.AWS_ACCOUNT_ID;
          var SCAN_LOG_GROUP_NAME = process.env.SCAN_LOG_GROUP_NAME;
          var SCAN_LOG_STREAM_SUFFIX = process.env.SCAN_LOG_STREAM_SUFFIX;
          var CLOUDFLARE_QUEUE_ID = process.env.CLOUDFLARE_QUEUE_ID;
          var CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
          var CLOUDFLARE_API_TOKEN_SECRET_ARN = process.env.CLOUDFLARE_API_TOKEN_SECRET_ARN;
          var MULTI_DEPLOYMENT = process.env.MULTI_DEPLOYMENT === "true";
          var NA_MULTI = "Not available. Please check the multi/parent dashboard!";
          async function handler(event, context) {
            console.log(`Invoke: ${JSON.stringify(event)} ${JSON.stringify(context)}`);
            const latest = await fetchLatest();
            const scheduledTemplateUrl = latest["add-on-scheduled-bucket-scan-cloudflare"].template;
            if (event.widgetContext.params.view === "update") {
              if (MULTI_DEPLOYMENT === false) {
                return await update(defaultCloudformation, defaultSsm, dynamodb, "cloudflare", latest, false, AWS_PARTITION, AWS_REGION, AWS_ACCOUNT_ID, CORE_STACK_NAME, CORE_STACK_ID, ENGINE, FULFILLMENT, void 0);
              } else {
                return NA_MULTI;
              }
            } else if (event.widgetContext.params.view === "buckets") {
              if (MULTI_DEPLOYMENT === false) {
                return await bucketsCloudflare(defaultSsm, defaultCloudformation, secretsmanager, event, context, scheduledTemplateUrl, AWS_REGION, AWS_ACCOUNT_ID, CORE_STACK_NAME, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN_SECRET_ARN, CLOUDFLARE_QUEUE_ID, ENGINE, FULFILLMENT);
              } else {
                return NA_MULTI;
              }
            } else if (event.widgetContext.params.view === "scanResults") {
              return await scanResults(cwLogs, event, context, SCAN_LOG_GROUP_NAME, SCAN_LOG_STREAM_SUFFIX);
            } else if (event.widgetContext.params.view === "reports") {
              if (MULTI_DEPLOYMENT === false) {
                return await reports(defaultS3, defaultSsm, event, context, AWS_REGION, CORE_STACK_NAME, "cloudflare", ENGINE, FULFILLMENT);
              } else {
                return NA_MULTI;
              }
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: AWS::StackName
          CORE_STACK_ID:
            Ref: AWS::StackId
          ENGINE: multi
          FULFILLMENT: dedicated-private-vpc
          AWS_PARTITION:
            Ref: AWS::Partition
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          SCAN_LOG_GROUP_NAME:
            Ref: Logs
          MULTI_DEPLOYMENT: "false"
          SCAN_LOG_STREAM_SUFFIX: ""
          CLOUDFLARE_QUEUE_ID:
            Fn::GetAtt:
              - CloudflareQueue
              - QueueId
          CLOUDFLARE_ACCOUNT_ID:
            Ref: CloudflareAccountId
          CLOUDFLARE_API_TOKEN_SECRET_ARN:
            Ref: CloudflareApiTokenSecret
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasDashboardLambdaFunctionReservedConcurrentExecutions
          - Ref: DashboardLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - DashboardLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 300
      VpcConfig:
        Ref: AWS::NoValue
  DashboardLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: DashboardLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  DashboardLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - DashboardLambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: DashboardLambdaRole
  Dashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardBody:
        Fn::Join:
          - ""
          - - '{"start":"-PT24H","widgets":[{"type":"metric","x":0,"y":0,"height":3,"width":16,"properties":{"sparkline":false,"view":"singleValue","metrics":[["AWS/SQS","NumberOfMessagesSent","QueueName","'
            - Fn::GetAtt:
                - ScanQueue
                - QueueName
            - '",{"yAxis":"right","stat":"Sum","label":"Files enqueued"}],[".","NumberOfMessagesDeleted",".",".",{"yAxis":"right","stat":"Sum","label":"Files processed"}],["'
            - Ref: AWS::StackName
            - '","clean",{"stat":"Sum","label":"Clean"}],[".","infected",{"stat":"Sum","color":"#d62728","label":"Infected"}],[".","no",{"stat":"Sum","color":"#ff7f0e","label":"Unscannable (too big, access denied)"}],[".","scanned_data",{"stat":"Sum","label":"Scanned data (GB)"}]],"region":"'
            - Ref: AWS::Region
            - '","title":"bucketAV for Cloudflare R2 powered by multiple engines (dedicated-private-vpc) - Overview","liveData":true,"setPeriodToTimeRange":true,"trend":false,"singleValueFullPrecision":false}},{"type":"custom","x":16,"y":0,"width":8,"height":8,"properties":{"endpoint":"'
            - Fn::GetAtt:
                - DashboardLambdaFunction
                - Arn
            - '","updateOn":{"refresh":true,"resize":false,"timeRange":false},"params":{"view":"update"},"title":"Update"}},{"type":"metric","x":0,"y":3,"width":4,"height":5,"properties":{"metrics":[["'
            - Ref: AWS::StackName
            - '","clean",{"stat":"Sum","label":"Clean"}],[".","infected",{"stat":"Sum","color":"#d62728","label":"Infected"}],[".","no",{"stat":"Sum","color":"#ff7f0e","label":"Unscannable (too big, access denied)"}]],"view":"timeSeries","stacked":true,"region":"'
            - Ref: AWS::Region
            - '","title":"Scan Results","period":60,"liveData":true}},{"type":"metric","x":4,"y":3,"width":4,"height":5,"properties":{"metrics":[["AWS/SQS","ApproximateNumberOfMessagesVisible","QueueName","'
            - Fn::GetAtt:
                - ScanQueue
                - QueueName
            - '",{"stat":"Maximum","label":"Queue Length"}],[".","NumberOfMessagesSent",".",".",{"yAxis":"right","stat":"Sum","label":"Files enqueued"}],[".","NumberOfMessagesDeleted",".",".",{"yAxis":"right","stat":"Sum","label":"Files processed"}]],"view":"timeSeries","stacked":false,"region":"'
            - Ref: AWS::Region
            - '","title":"Scan Queue","period":60,"liveData":true}},{"type":"alarm","x":12,"y":3,"width":4,"height":5,"properties":{"title":"Infrastructure Alarms","alarms":["'
            - Fn::GetAtt:
                - DeadLetterQueueAlarm
                - Arn
            - '","'
            - Fn::GetAtt:
                - ScanQueueOldMessagesAlarm
                - Arn
            - '"]}},{"type":"custom","x":0,"y":8,"width":8,"height":8,"properties":{"endpoint":"'
            - Fn::GetAtt:
                - DashboardLambdaFunction
                - Arn
            - '","updateOn":{"refresh":true,"resize":false,"timeRange":false},"params":{"view":"scanResults"},"title":"Scan Results"}},{"type":"custom","x":8,"y":8,"width":8,"height":8,"properties":{"endpoint":"'
            - Fn::GetAtt:
                - DashboardLambdaFunction
                - Arn
            - '","updateOn":{"refresh":true,"resize":false,"timeRange":false},"params":{"view":"reports"},"title":"Reports"}},{"type":"custom","x":16,"y":8,"width":8,"height":8,"properties":{"endpoint":"'
            - Fn::GetAtt:
                - DashboardLambdaFunction
                - Arn
            - '","updateOn":{"refresh":true,"resize":false,"timeRange":false},"params":{"view":"buckets"},"title":"Buckets"}}]}'
      DashboardName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - "-"
            - Ref: AWS::Region
    DependsOn:
      - DashboardLambdaPolicy
  ServiceDiscoveryVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 3.0.1
  ServiceDiscoveryFulfillmentOption:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /FulfillmentOption
      Type: String
      Value: dedicated-private-vpc
  ServiceDiscoveryEngine:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /Engine
      Type: String
      Value: multi
Outputs:
  InfrastructureAlarmsTopicArn:
    Description: The ARN of the Infrastructure Alarms Topic.
    Value:
      Ref: InfrastructureAlarmsTopic
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -InfrastructureAlarmsTopicArn
  InfrastructureAlarmsTopicName:
    Description: The name of the Infrastructure Alarms Topic.
    Value:
      Fn::GetAtt:
        - InfrastructureAlarmsTopic
        - TopicName
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -InfrastructureAlarmsTopicName
  ScanQueueArn:
    Description: The ARN of the Scan Queue.
    Value:
      Fn::GetAtt:
        - ScanQueue
        - Arn
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -ScanQueueArn
  ScanQueueName:
    Description: The name of the Scan Queue.
    Value:
      Fn::GetAtt:
        - ScanQueue
        - QueueName
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -ScanQueueName
  ScanQueueUrl:
    Description: The URL of the Scan Queue.
    Value:
      Ref: ScanQueue
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -ScanQueueUrl
  DeadLetterQueueArn:
    Description: The ARN of the Dead Letter Queue.
    Value:
      Fn::GetAtt:
        - DeadLetterQueue
        - Arn
  DeadLetterQueueName:
    Description: The name of the Dead Letter Queue.
    Value:
      Fn::GetAtt:
        - DeadLetterQueue
        - QueueName
  DeadLetterQueueUrl:
    Description: The URL of the Dead Letter Queue.
    Value:
      Ref: DeadLetterQueue
  CloudflareAccessKeySecretArn:
    Description: The ARN of the Cloudflare access key secret.
    Value:
      Ref: CloudflareAccessKeySecretSecret
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -CloudflareAccessKeySecretArn
  CloudflareQueueId:
    Description: The ID of the Cloudflare queue.
    Value:
      Fn::GetAtt:
        - CloudflareQueue
        - QueueId
  CloudflareQueueName:
    Description: The name of the Cloudflare queue.
    Value:
      Fn::GetAtt:
        - CloudflareQueue
        - QueueName
  FindingsTopicArn:
    Description: The ARN of the Findings Topic.
    Value:
      Ref: FindingsTopic
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -FindingsTopicArn
  FindingsTopicName:
    Description: The name of the Findings Topic.
    Value:
      Fn::GetAtt:
        - FindingsTopic
        - TopicName
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -FindingsTopicName
  FindingsTopicFanInLambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - FindingsTopicFanInLambdaDeadLetterQueue
        - QueueName
  DashboardLambdaRoleArn:
    Description: The ARN of the Dashboard Role.
    Value:
      Fn::GetAtt:
        - DashboardLambdaRole
        - Arn
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -DashboardLambdaRoleArn
  FulfillmentOption:
    Description: Fulfillment option.
    Value: dedicated-private-vpc
  Version:
    Description: bucketAV version.
    Value: 3.0.1
  Platform:
    Description: bucketAV platform.
    Value: cloudflare
  Engine:
    Description: bucketAV engine.
    Value: multi
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

