---
# 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 Amazon S3 powered by multiple engines (dedicated VPC; scanners run in public subnets) - Antivirus protection for Amazon S3
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - KeyName
          - InfrastructureAlarmsEmail
      - Label:
          default: Scan Parameters
        Parameters:
          - DeleteInfectedFiles
          - TagFiles
          - TagKey
          - ReportEventBridge
          - ScanDelayInSeconds
          - EngineClamAVEnableCache
          - EngineClamAVAdditionalDatabaseUrls
          - EngineSophosSophosLiveProtectionCloudLookups
          - Governance
      - 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
          - AWSAccountRestriction
          - AWSOrganizationRestriction
          - S3BucketRestriction
          - S3ObjectRestriction
          - KMSKeyRestriction
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - AccountConnectionLambdaFunctionReservedConcurrentExecutions
          - RefreshServiceDiscoveryLambdaFunctionFunctionReservedConcurrentExecutions
          - RefreshBucketCacheFunctionReservedConcurrentExecutions
          - StateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - GovernanceLambdaFunctionReservedConcurrentExecutions
          - DashboardLambdaFunctionReservedConcurrentExecutions
Parameters:
  DeleteInfectedFiles:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Automatically delete infected files.
  TagFiles:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Tag S3 object upon successful scan accordingly with values of "clean", "infected", or "no" (infected only works if DeleteInfectedFiles != true) using the tag key specified by TagKey.
  TagKey:
    Type: String
    Default: bucketav
    Description: S3 object tag key used to specify values of "clean", "infected", or "no".
  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.
  AWSAccountRestriction:
    Type: CommaDelimitedList
    Default: ""
    Description: Optional allowlist of all the AWS accounts that send S3 Notifications (e.g., 111111111111,222222222222,333333333333; only required in Multi-Account setups).
  AWSOrganizationRestriction:
    Type: CommaDelimitedList
    Default: ""
    Description: Optional allowlist of all the AWS organizations that send S3 Notifications (e.g., o-1111111111,o-2222222222,o-3333333333; only required in Multi-Account setups).
  S3BucketRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific S3 buckets (e.g. arn:aws:s3:::bucket-a,arn:aws:s3:::bucket-b or * to allow access to all S3 buckets).
  S3ObjectRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific S3 objects (e.g. arn:aws:s3:::bucket-a/*,arn:aws:s3:::bucket-b/* or * to allow access to all S3 objects).
  KMSKeyRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific KMS keys (e.g. arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab,arn:aws:kms:us-east-1:111122223333:key/0987dcba-09fe-87dc-65ba-ab0987654321 or * to allow access to all KMS keys).
  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 (e.g., access from anywhere: 0.0.0.0/0, from single public IP address 91.45.138.21/32)."
  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).
  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
  AccountConnectionLambdaFunctionReservedConcurrentExecutions:
    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
  RefreshServiceDiscoveryLambdaFunctionFunctionReservedConcurrentExecutions:
    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
  RefreshBucketCacheFunctionReservedConcurrentExecutions:
    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
  StateMachineNameGeneratorFunctionReservedConcurrentExecutions:
    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
  Governance:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable governance checks.
  GovernanceLambdaFunctionReservedConcurrentExecutions:
    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:
  HasDeleteInfectedFiles:
    Fn::Equals:
      - Ref: DeleteInfectedFiles
      - "true"
  HasTagFiles:
    Fn::Equals:
      - Ref: TagFiles
      - "true"
  HasReportEventBridge:
    Fn::Equals:
      - Ref: ReportEventBridge
      - "true"
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasAWSAccountRestriction:
    Fn::Not:
      - Fn::Equals:
          - Fn::Join:
              - ""
              - Ref: AWSAccountRestriction
          - ""
  HasAWSOrganizationRestriction:
    Fn::Not:
      - Fn::Equals:
          - Fn::Join:
              - ""
              - Ref: AWSOrganizationRestriction
          - ""
  HasInfrastructureAlarmsEmail:
    Fn::Not:
      - Fn::Equals:
          - Ref: InfrastructureAlarmsEmail
          - ""
  HasScanQueueFanOutLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: ScanQueueFanOutLambdaFunctionReservedConcurrentExecutions
          - 0
  HasFindingsTopicFanInLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: FindingsTopicFanInLambdaFunctionReservedConcurrentExecutions
          - 0
  HasCrossAccount:
    Fn::Or:
      - Fn::Not:
          - Fn::Equals:
              - Fn::Join:
                  - ","
                  - Ref: AWSAccountRestriction
              - ""
      - Fn::Not:
          - Fn::Equals:
              - Fn::Join:
                  - ","
                  - Ref: AWSOrganizationRestriction
              - ""
  HasOnScanResultsLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: OnScanResultsLambdaFunctionReservedConcurrentExecutions
          - 0
  HasAccountConnectionLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: AccountConnectionLambdaFunctionReservedConcurrentExecutions
          - 0
  HasRefreshServiceDiscoveryLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: RefreshServiceDiscoveryLambdaFunctionFunctionReservedConcurrentExecutions
          - 0
  HasRefreshBucketCacheFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: RefreshBucketCacheFunctionReservedConcurrentExecutions
          - 0
  HasStateMachineNameGeneratorFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: StateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - 0
  HasGovernance:
    Fn::Equals:
      - Ref: Governance
      - "true"
  HasGovernanceLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: GovernanceLambdaFunctionReservedConcurrentExecutions
          - 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
  ScanQueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - s3.amazonaws.com
                - sns.amazonaws.com
                - events.amazonaws.com
            Action: sqs:SendMessage
            Resource:
              Fn::GetAtt:
                - ScanQueue
                - Arn
            Condition:
              StringEquals:
                aws:SourceAccount:
                  Ref: AWS::AccountId
          - Fn::If:
              - HasAWSAccountRestriction
              - Effect: Allow
                Principal:
                  Service:
                    - s3.amazonaws.com
                    - sns.amazonaws.com
                    - events.amazonaws.com
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - ScanQueue
                    - Arn
                Condition:
                  StringEquals:
                    aws:SourceAccount:
                      Ref: AWSAccountRestriction
              - Ref: AWS::NoValue
          - Fn::If:
              - HasAWSOrganizationRestriction
              - Effect: Allow
                Principal:
                  Service:
                    - s3.amazonaws.com
                    - sns.amazonaws.com
                    - events.amazonaws.com
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - ScanQueue
                    - Arn
                Condition:
                  StringEquals:
                    aws:SourceOrgID:
                      Ref: AWSOrganizationRestriction
              - Ref: AWS::NoValue
      Queues:
        - Ref: ScanQueue
  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
  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"
        TagFiles: "false"
        ReportCleanFiles: "true"
        ReportEventBridge: "false"
        ScanDelayInSeconds: "0"
        Governance: "false"
        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
        AWSAccountRestriction:
          Fn::Join:
            - ","
            - Ref: AWSAccountRestriction
        AWSOrganizationRestriction:
          Fn::Join:
            - ","
            - Ref: AWSOrganizationRestriction
        S3BucketRestriction:
          Fn::Join:
            - ","
            - Ref: S3BucketRestriction
        S3ObjectRestriction:
          Fn::Join:
            - ","
            - Ref: S3ObjectRestriction
        KMSKeyRestriction:
          Fn::Join:
            - ","
            - Ref: KMSKeyRestriction
        PermissionsBoundary:
          Ref: PermissionsBoundary
        InternalMultiStackName:
          Ref: AWS::StackName
      TemplateURL: https://awsmp-cft-211125678794-1707910187780.s3.us-east-1.amazonaws.com/8cb37a0d-78b2-4608-a1ab-16c47bf27b46/8cb37a0d-78b2-4608-a1ab-16c47bf27b46/dedicated-public-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"
        TagFiles: "false"
        ReportCleanFiles: "true"
        ReportEventBridge: "false"
        ScanDelayInSeconds: "0"
        Governance: "false"
        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
        AWSAccountRestriction:
          Fn::Join:
            - ","
            - Ref: AWSAccountRestriction
        AWSOrganizationRestriction:
          Fn::Join:
            - ","
            - Ref: AWSOrganizationRestriction
        S3BucketRestriction:
          Fn::Join:
            - ","
            - Ref: S3BucketRestriction
        S3ObjectRestriction:
          Fn::Join:
            - ","
            - Ref: S3ObjectRestriction
        KMSKeyRestriction:
          Fn::Join:
            - ","
            - Ref: KMSKeyRestriction
        PermissionsBoundary:
          Ref: PermissionsBoundary
        InternalMultiStackName:
          Ref: AWS::StackName
      TemplateURL: https://awsmp-cft-211125678794-1707910187780.s3.us-east-1.amazonaws.com/f5f066cd-1569-4c6e-96fe-a79415fc0f75/f5f066cd-1569-4c6e-96fe-a79415fc0f75/dedicated-public-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:
                  - HasDeleteInfectedFiles
                  - Effect: Allow
                    Action: s3:DeleteObject*
                    Resource:
                      Ref: S3ObjectRestriction
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasTagFiles
                  - Effect: Allow
                    Action:
                      - s3:GetObjectTagging
                      - s3:GetObjectVersionTagging
                      - s3:PutObjectTagging
                      - s3:PutObjectVersionTagging
                    Resource:
                      Ref: S3ObjectRestriction
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: sts:AssumeRole
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - :iam::*:role/
                      - Ref: AWS::StackName
                      - -AccountConnection
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter/bucketAV/
                      - Ref: AWS::StackName
                      - /CrossAccount
              - Effect: Allow
                Action: dynamodb:GetItem
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":dynamodb:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :table/
                      - Ref: AWS::StackName
                      - -BucketCache
              - 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
          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
          TAG_FILES:
            Ref: TagFiles
          TAG_KEY:
            Ref: TagKey
          CORE_STACK_NAME:
            Ref: AWS::StackName
          CORE_PLATFORM: aws
      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
  ServiceDiscoveryOrganizationId:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /OrganizationId
      Type: String
      Value: INIT
    Condition: HasCrossAccount
  AccountConnectionTopic:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: bucketav:cloudformation:logical-id
          Value: AccountConnectionTopic
        - Key: bucketav:cloudformation:stack-id
          Value:
            Ref: AWS::StackId
        - Key: bucketav:cloudformation:stack-name
          Value:
            Ref: AWS::StackName
      TopicName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -AccountConnectionTopic
    Condition: HasCrossAccount
  AccountConnectionTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "1"
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sns:Publish
            Resource:
              Ref: AccountConnectionTopic
          - Fn::If:
              - HasAWSAccountRestriction
              - Sid: "10"
                Effect: Allow
                Principal: "*"
                Action: sns:Publish
                Resource:
                  Ref: AccountConnectionTopic
                Condition:
                  StringEquals:
                    aws:PrincipalAccount:
                      Ref: AWSAccountRestriction
              - Ref: AWS::NoValue
          - Fn::If:
              - HasAWSOrganizationRestriction
              - Sid: "20"
                Effect: Allow
                Principal: "*"
                Action: sns:Publish
                Resource:
                  Ref: AccountConnectionTopic
                Condition:
                  StringEquals:
                    aws:PrincipalOrgID:
                      Ref: AWSOrganizationRestriction
              - Ref: AWS::NoValue
      Topics:
        - Ref: AccountConnectionTopic
    Condition: HasCrossAccount
  AccountConnectionTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: account_connection_id
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: account_connection_id
          KeyType: HASH
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      SSESpecification:
        SSEEnabled: true
      TableName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -AccountConnection
    Condition: HasCrossAccount
  BucketCacheTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: bucket_name
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: bucket_name
          KeyType: HASH
      SSESpecification:
        SSEEnabled: true
      TableName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -BucketCache
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true
    Condition: HasCrossAccount
  AccountConnectionLambdaRole:
    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:
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource:
                  Fn::GetAtt:
                    - AccountConnectionTable
                    - Arn
              - Effect: Allow
                Action: dynamodb:PutItem
                Resource:
                  Fn::GetAtt:
                    - BucketCacheTable
                    - Arn
              - 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/scheduled-bucket-scan/*
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStacks
                  - s3:ListAllMyBuckets
                  - s3:GetBucketNotification
                  - sns:ListSubscriptionsByTopic
                  - events:ListRuleNamesByTarget
                  - events:DescribeRule
                  - organizations:DescribeOrganization
                Resource: "*"
              - Effect: Allow
                Action: events:DescribeEventBus
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :event-bus/default
              - Effect: Allow
                Action: sts:AssumeRole
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - :iam::*:role/
                      - Ref: AWS::StackName
                      - -AccountConnection
              - Effect: Allow
                Action: sns:Publish
                Resource:
                  Fn::GetAtt:
                    - InfrastructureAlarmsTopic
                    - TopicArn
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter
                      - Ref: ServiceDiscoveryOrganizationId
          PolicyName: main
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - AccountConnectionLambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
    Condition: HasCrossAccount
  AccountConnectionLambdaFunction:
    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/account-connection.js
          var account_connection_exports = {};
          __export(account_connection_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(account_connection_exports);
          var import_client_dynamodb3 = require("@aws-sdk/client-dynamodb");
          var import_client_cloudformation2 = require("@aws-sdk/client-cloudformation");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_sns3 = require("@aws-sdk/client-sns");
          var import_client_organizations2 = require("@aws-sdk/client-organizations");
          var import_credential_providers3 = require("@aws-sdk/credential-providers");

          // 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
          async function fetchOrganizationId(organizations) {
            try {
              const { Organization } = await organizations.send(new import_client_organizations.DescribeOrganizationCommand({}));
              return Organization.Id;
            } catch (err) {
              if (err.name === "AWSOrganizationsNotInUseException") {
                return void 0;
              } else {
                throw err;
              }
            }
          }
          async function fetchCachedOrganizationId(ssm2, coreStackName) {
            const data = await ssm2.send(new import_client_ssm.GetParameterCommand({
              Name: `/bucketAV/${coreStackName}/OrganizationId`
            }));
            if (data.Parameter.Value === "INIT" || data.Parameter.Value === "NONE") {
              return void 0;
            }
            return data.Parameter.Value;
          }
          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;
          }
          async function listBucketsLight(s3, region, accountId) {
            const allBuckets = [];
            const input = {};
            if (region !== void 0 && region !== null) {
              input.BucketRegion = region;
            }
            const paginator = (0, import_client_s3.paginateListBuckets)({ client: s3, pageSize: 1e3 }, input);
            for await (const page of paginator) {
              allBuckets.push(...page.Buckets.map((bucket) => {
                if ("BucketRegion" in bucket) {
                  return {
                    name: bucket.Name,
                    region: bucket.BucketRegion,
                    accountId
                  };
                } else {
                  return {
                    name: bucket.Name,
                    accountId,
                    errorMessage: `Can not get region for bucket ${bucket.Name}`
                  };
                }
              }));
            }
            if (region !== void 0 && region !== null) {
              return allBuckets.filter((bucket) => bucket.region === region || bucket.region === void 0);
            } else {
              return allBuckets;
            }
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function checkEventBridgeRules(eventbridge, ruleNames, bucketName) {
            const rules = await Promise.all(ruleNames.map((ruleName) => eventbridge.send(new import_client_eventbridge.DescribeRuleCommand({ Name: ruleName }))));
            return rules.filter((rule) => rule.State === "ENABLED" && rule.EventPattern).map((rule) => JSON.parse(rule.EventPattern)).find((pattern) => pattern.source.includes("aws.s3") && pattern["detail-type"].includes("Object Created") && (pattern?.detail?.bucket?.name?.includes(bucketName) || pattern?.detail?.bucket?.name === void 0)) !== void 0;
          }
          async function enrichBucket(s3, sns2, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, bucketName, bucketRegion) {
            const bucket = {
              name: bucketName,
              accountId,
              organizationId
            };
            if (bucketRegion === void 0) {
              bucket.region = void 0;
              bucket.errorMessage = `Can not get region for bucket ${bucketName}`;
            } else {
              bucket.region = bucketRegion;
            }
            if (bucket.region === region || bucket.region === void 0) {
              let realtimeEnabled = false;
              let realtimeEventNotificationEnablePossible = bucket.region !== void 0;
              let realtimeEventNotificationDisablePossible = false;
              let scheduledEnabled = false;
              let scheduledStackDisablePossible = false;
              let scheduledStackId = void 0;
              try {
                const notificationData = await s3.send(new import_client_s3.GetBucketNotificationConfigurationCommand({ Bucket: bucket.name, ExpectedBucketOwner: accountId }));
                if (notificationData?.QueueConfigurations?.find((config) => config.QueueArn === scanQueueArn && config.Events.includes("s3:ObjectCreated:*")) !== void 0) {
                  realtimeEnabled = true;
                  realtimeEventNotificationDisablePossible = true;
                }
                if (Array.isArray(notificationData?.TopicConfigurations)) {
                  await Promise.all(notificationData.TopicConfigurations.filter((config) => config.Events.includes("s3:ObjectCreated:*")).map(async (config) => {
                    const paginatorListSubscriptionsByTopic = await (0, import_client_sns.paginateListSubscriptionsByTopic)({ client: sns2, pageSize: 50 }, { TopicArn: config.TopicArn });
                    for await (const page of paginatorListSubscriptionsByTopic) {
                      if (page.Subscriptions.find((subscription) => subscription.Protocol === "sqs" && subscription.Endpoint === scanQueueArn) !== void 0) {
                        realtimeEnabled = true;
                      }
                    }
                  }));
                }
                if (notificationData?.EventBridgeConfiguration) {
                  if (accountId !== coreAccountId) {
                    let crossAccoutRule = false;
                    const paginatorListRuleNamesByTargetDefaultBus = await paginateListRuleNamesByTarget({ client: eventbridge, pageSize: 10 }, { TargetArn: `arn:${partition}:events:${region}:${coreAccountId}:event-bus/default` });
                    for await (const page of paginatorListRuleNamesByTargetDefaultBus) {
                      if (await checkEventBridgeRules(eventbridge, page.RuleNames, bucket.name) === true) {
                        crossAccoutRule = true;
                      }
                    }
                    const { Policy: defaultBusPolicyJson } = await coreEventbridge.send(new import_client_eventbridge.DescribeEventBusCommand({
                      Name: "default"
                    }));
                    if (defaultBusPolicyJson !== void 0) {
                      const defaultBusPolicy = JSON.parse(defaultBusPolicyJson);
                      if (defaultBusPolicy.Statement.find((s) => s?.Effect === "Allow" && s?.Action === "events:PutEvents" && (s?.Principal?.AWS === accountId || s?.Principal === "*" && s?.Condition?.StringEquals["aws:PrincipalOrgID"] === organizationId)) !== void 0) {
                        if (crossAccoutRule === true) {
                          realtimeEnabled = true;
                        }
                      }
                    }
                  }
                  const paginatorListRuleNamesByTargetScanQueue = await paginateListRuleNamesByTarget({ client: coreEventbridge, pageSize: 10 }, { TargetArn: scanQueueArn });
                  for await (const page of paginatorListRuleNamesByTargetScanQueue) {
                    if (await checkEventBridgeRules(coreEventbridge, page.RuleNames, bucket.name) === true) {
                      realtimeEnabled = true;
                    }
                  }
                }
                if (notificationData?.TopicConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.QueueConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.LambdaFunctionConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.EventBridgeConfiguration !== void 0) {
                  realtimeEventNotificationEnablePossible = false;
                }
              } 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, bucket.name));
              if (scheduledStacksIncludesBucket.length > 0) {
                scheduledEnabled = true;
                scheduledStackId = scheduledStacksIncludesBucket[0].id;
              }
              const scheduledStacksHasBucket = scheduledStacks.stacks.filter((scheduledStack) => hasBucket(scheduledStack, bucket.name));
              if (scheduledStacksHasBucket.length === 1) {
                scheduledEnabled = true;
                scheduledStackDisablePossible = true;
                scheduledStackId = scheduledStacksHasBucket[0].id;
              }
              return {
                ...bucket,
                realtimeEnabled,
                realtimeEventNotificationEnablePossible,
                realtimeEventNotificationDisablePossible,
                scheduledEnabled,
                scheduledStackDisablePossible,
                scheduledStackId
              };
            } else {
              return null;
            }
          }
          async function getScheduledStacks(ssm2, cloudformation2, coreStackName) {
            const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm2 }, { 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) => cloudformation2.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 listBuckets(s3, ssm2, cloudformation2, sns2, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn) {
            const lightBuckets = await listBucketsLight(s3, region, accountId);
            const scheduledStacks = await getScheduledStacks(ssm2, cloudformation2, coreStackName);
            const buckets = await Promise.all(lightBuckets.map((lightBucket) => enrichBucket(s3, sns2, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, lightBucket.name, lightBucket.region)));
            return buckets.filter((bucket) => bucket !== null);
          }
          function generateAccountConnectionId(partition, region, accountId) {
            return `${partition}:${region}:${accountId}`;
          }
          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 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/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");
          var TTL_IN_SECONDS = 3600;
          async function cacheBucket(dynamodb2, partition, region, accountId, { accountConnectionId, roleArn, externalId }, ttl, bucket, bucketCacheTableName) {
            const bucketCacheItem = {
              bucket_name: { S: bucket.name },
              bucket_account_id: { S: bucket.accountId },
              bucket_realtime_enabled: { BOOL: bucket.realtimeEnabled },
              bucket_realtime_event_notification_disable_possible: { BOOL: bucket.realtimeEventNotificationDisablePossible },
              bucket_realtime_event_notification_enable_possible: { BOOL: bucket.realtimeEventNotificationEnablePossible },
              bucket_scheduled_enabled: { BOOL: bucket.scheduledEnabled },
              bucket_scheduled_stack_disable_possible: { BOOL: bucket.scheduledStackDisablePossible },
              partition: { S: partition },
              region: { S: region },
              account_id: { S: accountId },
              ttl: { N: ttl.toString() }
            };
            if (bucket.organizationId !== void 0) {
              bucketCacheItem.bucket_organization_id = { S: bucket.organizationId };
            }
            if (accountConnectionId !== void 0) {
              bucketCacheItem.account_connection_id = { S: accountConnectionId };
              bucketCacheItem.role_arn = { S: roleArn };
              bucketCacheItem.external_id = { S: externalId };
            }
            if (bucket.errorMessage !== void 0) {
              bucketCacheItem.bucket_error_message = { S: bucket.errorMessage };
            }
            if (bucket.region !== void 0) {
              bucketCacheItem.bucket_region = { S: bucket.region };
            }
            if (bucket.scheduledStackId !== void 0) {
              bucketCacheItem.bucket_scheduled_stack_id = { S: bucket.scheduledStackId };
            }
            await dynamodb2.send(new import_client_dynamodb2.PutItemCommand({
              TableName: bucketCacheTableName,
              Item: bucketCacheItem
            }));
          }
          function getCredentials(accountConnectionId, roleArn, externalId) {
            if (accountConnectionId !== void 0 && roleArn !== void 0 && externalId !== void 0) {
              return (0, import_credential_providers2.fromTemporaryCredentials)({
                params: {
                  RoleArn: roleArn,
                  ExternalId: externalId,
                  RoleSessionName: "bucketav",
                  DurationSeconds: 3600
                }
              });
            } else {
              return (0, import_credential_providers2.fromNodeProviderChain)();
            }
          }
          function generateTtl() {
            return Math.floor(Date.now() / 1e3) + TTL_IN_SECONDS;
          }
          async function refreshAccount2(dynamodb2, ssm2, cloudformation2, partition, region, accountId, { accountConnectionId, roleArn, externalId, organizationId }, ttl, coreAccountId, coreStackName, scanQueueArn, bucketCacheTableName) {
            const credentials = getCredentials(accountConnectionId, roleArn, externalId);
            const s3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01", credentials });
            const sns2 = new import_client_sns2.SNSClient({ apiVersion: "2010-03-31", credentials });
            const eventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07", credentials });
            const coreEventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07" });
            const buckets = await listBuckets(s3, ssm2, cloudformation2, sns2, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn);
            for (const bucket of buckets) {
              await cacheBucket(dynamodb2, partition, region, accountId, { accountConnectionId, roleArn, externalId }, ttl, bucket, bucketCacheTableName);
            }
          }
          async function refreshAccount(dynamodb2, ssm2, cloudformation2, partition, region, accountId, coreAccountId, coreStackName, scanQueueArn, bucketCacheTableName, accountConnectionTableName) {
            const accountConnectionId = generateAccountConnectionId(partition, region, accountId);
            const accountConnectionData = await dynamodb2.send(new import_client_dynamodb2.GetItemCommand({
              TableName: accountConnectionTableName,
              Key: {
                account_connection_id: { S: accountConnectionId }
              },
              ConsistentRead: true
            }));
            let accountConnection = {};
            if (accountConnectionData.Item !== void 0) {
              const roleArn = generateRoleArnFromItem(accountConnectionData.Item);
              const externalId = generateExternalIdFromItem(accountConnectionData.Item);
              accountConnection.accountConnectionId = accountConnectionId;
              accountConnection.roleArn = roleArn;
              accountConnection.externalId = externalId;
              accountConnection.organizationId = accountConnectionData.Item.organization_id?.S;
            } else {
              accountConnection.organizationId = await fetchCachedOrganizationId(ssm2, coreStackName);
            }
            return refreshAccount2(dynamodb2, ssm2, cloudformation2, partition, region, accountId, accountConnection, generateTtl(), coreAccountId, coreStackName, scanQueueArn, bucketCacheTableName);
          }

          // lambda/account-connection.js
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var cloudformation = new import_client_cloudformation2.CloudFormationClient({ apiVersion: "2006-03-01", maxAttempts: 10 });
          var dynamodb = new import_client_dynamodb3.DynamoDBClient({ apiVersion: "2012-08-10" });
          var sns = new import_client_sns3.SNSClient({ apiVersion: "2010-03-31" });
          async function handler(event, context) {
            const now = Date.now();
            console.log(`Invoke: ${JSON.stringify(event)} ${JSON.stringify(context)}`);
            const bucketCacheTableName = `${process.env.CORE_STACK_NAME}-BucketCache`;
            const accountConnectionTableName = `${process.env.CORE_STACK_NAME}-AccountConnection`;
            for (const record of event.Records) {
              if (record.Sns.Type === "Notification") {
                const message = JSON.parse(record.Sns.Message);
                if ("action" in message) {
                  if (message.action === "ping") {
                    const accountConnectionId = generateAccountConnectionId(message.partition, message.region, message.accountId);
                    const accountConnectionData = await dynamodb.send(new import_client_dynamodb3.GetItemCommand({
                      TableName: accountConnectionTableName,
                      Key: {
                        account_connection_id: { S: accountConnectionId }
                      },
                      ConsistentRead: true
                    }));
                    if (accountConnectionData.Item === void 0) {
                      throw new Error("account connection not found");
                    }
                    const credentials = (0, import_credential_providers3.fromTemporaryCredentials)({
                      params: {
                        RoleArn: generateRoleArnFromItem(accountConnectionData.Item),
                        ExternalId: generateExternalIdFromItem(accountConnectionData.Item),
                        RoleSessionName: "bucketav",
                        DurationSeconds: 3600
                      }
                    });
                    const organizations = new import_client_organizations2.OrganizationsClient({ apiVersion: "2016-11-28", credentials });
                    const organizationId = await fetchOrganizationId(organizations);
                    const expressionAttributeValues = {
                      ":now": { N: now.toString() }
                    };
                    let updateExpression = "SET pinged_at=:now";
                    if (accountConnectionData.Item.organization_id?.S !== organizationId) {
                      if (organizationId === void 0) {
                        updateExpression += " REMOVE organization_id";
                      } else {
                        updateExpression += ", organization_id=:organizationId";
                        expressionAttributeValues[":organizationId"] = { S: organizationId };
                      }
                    }
                    await dynamodb.send(new import_client_dynamodb3.UpdateItemCommand({
                      TableName: accountConnectionTableName,
                      Key: {
                        account_connection_id: { S: accountConnectionId }
                      },
                      UpdateExpression: updateExpression,
                      ExpressionAttributeValues: expressionAttributeValues,
                      ConditionExpression: "attribute_exists(account_connection_id)"
                    }));
                    if (accountConnectionData.Item.organization_id?.S !== organizationId) {
                      await refreshAccount(dynamodb, ssm, cloudformation, accountConnectionData.Item.partition.S, accountConnectionData.Item.region.S, accountConnectionData.Item.account_id.S, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN, bucketCacheTableName, accountConnectionTableName);
                    }
                  } else if (message.action === "refreshAccount") {
                    await refreshAccount(dynamodb, ssm, cloudformation, message.partition, message.region, message.accountId, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN, bucketCacheTableName, accountConnectionTableName);
                  } else {
                    throw new Error("Unsupported action");
                  }
                } else if ("RequestType" in message) {
                  try {
                    if (message.RequestType === "Create") {
                      if (process.env.AWS_PARTITION === message.ResourceProperties.Partition && process.env.AWS_ACCOUNT_ID === message.ResourceProperties.AccountId) {
                        throw new Error("No need to connect the bucketAV account to itself");
                      } else {
                        const credentials = (0, import_credential_providers3.fromTemporaryCredentials)({
                          params: {
                            RoleArn: generateRoleArn(message.ResourceProperties.Partition, message.ResourceProperties.AccountId, message.ResourceProperties.RoleName),
                            ExternalId: generateExternalId(message.ResourceProperties.StackId),
                            RoleSessionName: "bucketav",
                            DurationSeconds: 3600
                          }
                        });
                        const organizations = new import_client_organizations2.OrganizationsClient({ apiVersion: "2016-11-28", credentials });
                        const organizationId = await fetchOrganizationId(organizations);
                        await dynamodb.send(new import_client_dynamodb3.PutItemCommand({
                          TableName: accountConnectionTableName,
                          Item: {
                            account_connection_id: { S: generateAccountConnectionId(message.ResourceProperties.Partition, message.ResourceProperties.Region, message.ResourceProperties.AccountId) },
                            partition: { S: message.ResourceProperties.Partition },
                            region: { S: message.ResourceProperties.Region },
                            account_id: { S: message.ResourceProperties.AccountId },
                            ...organizationId !== void 0 && { organization_id: { S: organizationId } },
                            stack_id: { S: message.ResourceProperties.StackId },
                            stack_name: { S: message.ResourceProperties.StackName },
                            stack_version: { S: message.ResourceProperties.StackVersion },
                            role_name: { S: message.ResourceProperties.RoleName },
                            created_at: { N: now.toString() },
                            updated_at: { N: now.toString() },
                            pinged_at: { N: now.toString() }
                          }
                        }));
                        await refreshAccount(dynamodb, ssm, cloudformation, message.ResourceProperties.Partition, message.ResourceProperties.Region, message.ResourceProperties.AccountId, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN, bucketCacheTableName, accountConnectionTableName);
                      }
                    } else if (message.RequestType === "Update") {
                      const id = generateAccountConnectionId(message.ResourceProperties.Partition, message.ResourceProperties.Region, message.ResourceProperties.AccountId);
                      const oldId = generateAccountConnectionId(message.OldResourceProperties.Partition, message.OldResourceProperties.Region, message.OldResourceProperties.AccountId);
                      if (id !== oldId) {
                        throw new Error("unexpected id change during update");
                      }
                      await dynamodb.send(new import_client_dynamodb3.UpdateItemCommand({
                        TableName: accountConnectionTableName,
                        Key: {
                          account_connection_id: { S: id }
                        },
                        UpdateExpression: "SET stack_version=:stack_version, role_name=:role_name, updated_at=:now",
                        ExpressionAttributeValues: {
                          ":now": { N: now.toString() },
                          ":stack_version": { S: message.ResourceProperties.StackVersion },
                          ":role_name": { S: message.ResourceProperties.RoleName }
                        },
                        ConditionExpression: "attribute_exists(account_connection_id)"
                      }));
                      await refreshAccount(dynamodb, ssm, cloudformation, message.ResourceProperties.Partition, message.ResourceProperties.Region, message.ResourceProperties.AccountId, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN, bucketCacheTableName, accountConnectionTableName);
                    } else if (message.RequestType === "Delete") {
                      await dynamodb.send(new import_client_dynamodb3.DeleteItemCommand({
                        TableName: accountConnectionTableName,
                        Key: {
                          account_connection_id: { S: generateAccountConnectionId(message.ResourceProperties.Partition, message.ResourceProperties.Region, message.ResourceProperties.AccountId) }
                        }
                      }));
                    } else {
                      throw new Error("Unsupported RequestType");
                    }
                    await cfnCustomResourceSuccess(message, message.ResourceProperties.StackId);
                  } catch (err) {
                    await cfnCustomResourceFailed(message, message.ResourceProperties.StackId);
                    throw err;
                  }
                } else if (message.source === "aws.cloudwatch") {
                  await sns.send(new import_client_sns3.PublishCommand({
                    TopicArn: process.env.INFRASTRUCTURE_ALARMS_TOPIC_ARN,
                    Message: JSON.stringify(message)
                  }));
                } else {
                  throw new Error("Unsupported message");
                }
              } else {
                throw new Error("Unsupported SNS Type");
              }
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - AccountConnectionLambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: AWS::StackName
          SCAN_QUEUE_ARN:
            Fn::GetAtt:
              - ScanQueue
              - Arn
          AWS_PARTITION:
            Ref: AWS::Partition
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          INFRASTRUCTURE_ALARMS_TOPIC_ARN:
            Fn::GetAtt:
              - InfrastructureAlarmsTopic
              - TopicArn
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasAccountConnectionLambdaFunctionReservedConcurrentExecutions
          - Ref: AccountConnectionLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - AccountConnectionLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 900
      VpcConfig:
        Ref: AWS::NoValue
    Condition: HasCrossAccount
  AccountConnectionLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV Account Connection failed. Check logs of AWS Lambda Function "
            - Ref: AccountConnectionLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: AccountConnectionLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  AccountConnectionLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV Account Connection throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: AccountConnectionLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  AccountConnectionLambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
    Condition: HasCrossAccount
  AccountConnectionLambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV Account Connection has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - AccountConnectionLambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  AccountConnectionLambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV Account Connection has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: AccountConnectionLambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  LambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: AccountConnectionLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasCrossAccount
  AccountConnectionLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - LambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: AccountConnectionLambdaRole
    Condition: HasCrossAccount
  AccountConnectionLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: AccountConnectionLambdaFunction
      Principal: sns.amazonaws.com
      SourceArn:
        Ref: AccountConnectionTopic
    Condition: HasCrossAccount
  AccountConnectionTopicSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - AccountConnectionLambdaFunction
          - Arn
      Protocol: lambda
      TopicArn:
        Ref: AccountConnectionTopic
    DependsOn:
      - AccountConnectionLambdaPermission
      - AccountConnectionLambdaPolicy
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaRole:
    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:
                  - ssm:GetParameter
                  - ssm:PutParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter
                      - Ref: ServiceDiscoveryOrganizationId
              - Effect: Allow
                Action: organizations:DescribeOrganization
                Resource: "*"
          PolicyName: lambda
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - RefreshServiceDiscoveryLambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaFunction:
    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/refresh-service-discovery.js
          var refresh_service_discovery_exports = {};
          __export(refresh_service_discovery_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(refresh_service_discovery_exports);
          var import_client_organizations2 = require("@aws-sdk/client-organizations");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");

          // 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
          async function fetchOrganizationId(organizations2) {
            try {
              const { Organization } = await organizations2.send(new import_client_organizations.DescribeOrganizationCommand({}));
              return Organization.Id;
            } catch (err) {
              if (err.name === "AWSOrganizationsNotInUseException") {
                return void 0;
              } else {
                throw err;
              }
            }
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          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/refresh-service-discovery.js
          var organizations = new import_client_organizations2.OrganizationsClient({ apiVersion: "2016-11-28" });
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          async function refresh() {
            const organizationId = await fetchOrganizationId(organizations);
            const newValue = organizationId === void 0 ? "NONE" : organizationId;
            const { Parameter: { Value: oldValue } } = await ssm.send(new import_client_ssm2.GetParameterCommand({
              Name: `/bucketAV/${process.env.CORE_STACK_NAME}/OrganizationId`
            }));
            if (oldValue !== newValue) {
              await ssm.send(new import_client_ssm2.PutParameterCommand({
                Name: `/bucketAV/${process.env.CORE_STACK_NAME}/OrganizationId`,
                Value: newValue,
                Overwrite: true
              }));
            }
          }
          async function handler(event, context) {
            console.log(`Invoke: ${JSON.stringify(event)} ${JSON.stringify(context)}`);
            if ("RequestType" in event) {
              if (event.RequestType === "Create" || event.RequestType === "Update") {
                try {
                  await refresh();
                  await cfnCustomResourceSuccess(event, process.env.CORE_STACK_NAME, {});
                } catch (err) {
                  console.log(err);
                  await cfnCustomResourceFailed(event, process.env.CORE_STACK_NAME, err.message);
                }
              } else if (event.RequestType === "Delete") {
                await cfnCustomResourceSuccess(event, process.env.CORE_STACK_NAME, {});
              } else {
                throw new Error("unsupported request type");
              }
            } else {
              await refresh();
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - RefreshServiceDiscoveryLambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: AWS::StackName
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasRefreshServiceDiscoveryLambdaFunctionReservedConcurrentExecutions
          - Ref: RefreshServiceDiscoveryLambdaFunctionFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - RefreshServiceDiscoveryLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 300
      VpcConfig:
        Ref: AWS::NoValue
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV refresh Service Discovery failed. Check logs of AWS Lambda Function "
            - Ref: RefreshServiceDiscoveryLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: RefreshServiceDiscoveryLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV refresh Service Discovery throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: RefreshServiceDiscoveryLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV refresh Service Discovery has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - RefreshServiceDiscoveryLambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV refresh Service Discovery has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: RefreshServiceDiscoveryLambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: RefreshServiceDiscoveryLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - RefreshServiceDiscoveryLambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: RefreshServiceDiscoveryLambdaRole
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaCron:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(1 day)
      Targets:
        - Arn:
            Fn::GetAtt:
              - RefreshServiceDiscoveryLambdaFunction
              - Arn
          Id: lambda
    DependsOn:
      - RefreshServiceDiscoveryLambdaPolicy
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: RefreshServiceDiscoveryLambdaFunction
      Principal: events.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - RefreshServiceDiscoveryLambdaCron
          - Arn
    Condition: HasCrossAccount
  RefreshServiceDiscovery:
    Type: Custom::RefreshServiceDiscovery
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - RefreshServiceDiscoveryLambdaFunction
          - Arn
      ServiceTimeout: "300"
      Version: 2.0.0
    DependsOn:
      - RefreshServiceDiscoveryLambdaPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Condition: HasCrossAccount
  RefreshBucketCacheRole:
    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: dynamodb:PutItem
                Resource:
                  Fn::GetAtt:
                    - BucketCacheTable
                    - Arn
              - Effect: Allow
                Action: dynamodb:Scan
                Resource:
                  Fn::GetAtt:
                    - AccountConnectionTable
                    - Arn
              - 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/scheduled-bucket-scan/*
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStacks
                  - s3:ListAllMyBuckets
                  - s3:GetBucketNotification
                  - sns:ListSubscriptionsByTopic
                  - events:ListRuleNamesByTarget
                  - events:DescribeRule
                Resource: "*"
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter
                      - Ref: ServiceDiscoveryOrganizationId
              - Effect: Allow
                Action: events:DescribeEventBus
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :event-bus/default
              - Effect: Allow
                Action: sts:AssumeRole
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - :iam::*:role/
                      - Ref: AWS::StackName
                      - -AccountConnection
          PolicyName: main
    Condition: HasCrossAccount
  RefreshBucketCacheFunction:
    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/refresh-bucket-cache.js
          var refresh_bucket_cache_exports = {};
          __export(refresh_bucket_cache_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(refresh_bucket_cache_exports);
          var import_client_dynamodb3 = require("@aws-sdk/client-dynamodb");
          var import_client_cloudformation2 = require("@aws-sdk/client-cloudformation");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");

          // 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.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
          async function fetchCachedOrganizationId(ssm2, coreStackName) {
            const data = await ssm2.send(new import_client_ssm.GetParameterCommand({
              Name: `/bucketAV/${coreStackName}/OrganizationId`
            }));
            if (data.Parameter.Value === "INIT" || data.Parameter.Value === "NONE") {
              return void 0;
            }
            return data.Parameter.Value;
          }
          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;
          }
          async function listBucketsLight(s3, region, accountId) {
            const allBuckets = [];
            const input = {};
            if (region !== void 0 && region !== null) {
              input.BucketRegion = region;
            }
            const paginator = (0, import_client_s3.paginateListBuckets)({ client: s3, pageSize: 1e3 }, input);
            for await (const page of paginator) {
              allBuckets.push(...page.Buckets.map((bucket) => {
                if ("BucketRegion" in bucket) {
                  return {
                    name: bucket.Name,
                    region: bucket.BucketRegion,
                    accountId
                  };
                } else {
                  return {
                    name: bucket.Name,
                    accountId,
                    errorMessage: `Can not get region for bucket ${bucket.Name}`
                  };
                }
              }));
            }
            if (region !== void 0 && region !== null) {
              return allBuckets.filter((bucket) => bucket.region === region || bucket.region === void 0);
            } else {
              return allBuckets;
            }
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function checkEventBridgeRules(eventbridge, ruleNames, bucketName) {
            const rules = await Promise.all(ruleNames.map((ruleName) => eventbridge.send(new import_client_eventbridge.DescribeRuleCommand({ Name: ruleName }))));
            return rules.filter((rule) => rule.State === "ENABLED" && rule.EventPattern).map((rule) => JSON.parse(rule.EventPattern)).find((pattern) => pattern.source.includes("aws.s3") && pattern["detail-type"].includes("Object Created") && (pattern?.detail?.bucket?.name?.includes(bucketName) || pattern?.detail?.bucket?.name === void 0)) !== void 0;
          }
          async function enrichBucket(s3, sns, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, bucketName, bucketRegion) {
            const bucket = {
              name: bucketName,
              accountId,
              organizationId
            };
            if (bucketRegion === void 0) {
              bucket.region = void 0;
              bucket.errorMessage = `Can not get region for bucket ${bucketName}`;
            } else {
              bucket.region = bucketRegion;
            }
            if (bucket.region === region || bucket.region === void 0) {
              let realtimeEnabled = false;
              let realtimeEventNotificationEnablePossible = bucket.region !== void 0;
              let realtimeEventNotificationDisablePossible = false;
              let scheduledEnabled = false;
              let scheduledStackDisablePossible = false;
              let scheduledStackId = void 0;
              try {
                const notificationData = await s3.send(new import_client_s3.GetBucketNotificationConfigurationCommand({ Bucket: bucket.name, ExpectedBucketOwner: accountId }));
                if (notificationData?.QueueConfigurations?.find((config) => config.QueueArn === scanQueueArn && config.Events.includes("s3:ObjectCreated:*")) !== void 0) {
                  realtimeEnabled = true;
                  realtimeEventNotificationDisablePossible = true;
                }
                if (Array.isArray(notificationData?.TopicConfigurations)) {
                  await Promise.all(notificationData.TopicConfigurations.filter((config) => config.Events.includes("s3:ObjectCreated:*")).map(async (config) => {
                    const paginatorListSubscriptionsByTopic = await (0, import_client_sns.paginateListSubscriptionsByTopic)({ client: sns, pageSize: 50 }, { TopicArn: config.TopicArn });
                    for await (const page of paginatorListSubscriptionsByTopic) {
                      if (page.Subscriptions.find((subscription) => subscription.Protocol === "sqs" && subscription.Endpoint === scanQueueArn) !== void 0) {
                        realtimeEnabled = true;
                      }
                    }
                  }));
                }
                if (notificationData?.EventBridgeConfiguration) {
                  if (accountId !== coreAccountId) {
                    let crossAccoutRule = false;
                    const paginatorListRuleNamesByTargetDefaultBus = await paginateListRuleNamesByTarget({ client: eventbridge, pageSize: 10 }, { TargetArn: `arn:${partition}:events:${region}:${coreAccountId}:event-bus/default` });
                    for await (const page of paginatorListRuleNamesByTargetDefaultBus) {
                      if (await checkEventBridgeRules(eventbridge, page.RuleNames, bucket.name) === true) {
                        crossAccoutRule = true;
                      }
                    }
                    const { Policy: defaultBusPolicyJson } = await coreEventbridge.send(new import_client_eventbridge.DescribeEventBusCommand({
                      Name: "default"
                    }));
                    if (defaultBusPolicyJson !== void 0) {
                      const defaultBusPolicy = JSON.parse(defaultBusPolicyJson);
                      if (defaultBusPolicy.Statement.find((s) => s?.Effect === "Allow" && s?.Action === "events:PutEvents" && (s?.Principal?.AWS === accountId || s?.Principal === "*" && s?.Condition?.StringEquals["aws:PrincipalOrgID"] === organizationId)) !== void 0) {
                        if (crossAccoutRule === true) {
                          realtimeEnabled = true;
                        }
                      }
                    }
                  }
                  const paginatorListRuleNamesByTargetScanQueue = await paginateListRuleNamesByTarget({ client: coreEventbridge, pageSize: 10 }, { TargetArn: scanQueueArn });
                  for await (const page of paginatorListRuleNamesByTargetScanQueue) {
                    if (await checkEventBridgeRules(coreEventbridge, page.RuleNames, bucket.name) === true) {
                      realtimeEnabled = true;
                    }
                  }
                }
                if (notificationData?.TopicConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.QueueConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.LambdaFunctionConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.EventBridgeConfiguration !== void 0) {
                  realtimeEventNotificationEnablePossible = false;
                }
              } 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, bucket.name));
              if (scheduledStacksIncludesBucket.length > 0) {
                scheduledEnabled = true;
                scheduledStackId = scheduledStacksIncludesBucket[0].id;
              }
              const scheduledStacksHasBucket = scheduledStacks.stacks.filter((scheduledStack) => hasBucket(scheduledStack, bucket.name));
              if (scheduledStacksHasBucket.length === 1) {
                scheduledEnabled = true;
                scheduledStackDisablePossible = true;
                scheduledStackId = scheduledStacksHasBucket[0].id;
              }
              return {
                ...bucket,
                realtimeEnabled,
                realtimeEventNotificationEnablePossible,
                realtimeEventNotificationDisablePossible,
                scheduledEnabled,
                scheduledStackDisablePossible,
                scheduledStackId
              };
            } else {
              return null;
            }
          }
          async function getScheduledStacks(ssm2, cloudformation2, coreStackName) {
            const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm2 }, { 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) => cloudformation2.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 listBuckets(s3, ssm2, cloudformation2, sns, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn) {
            const lightBuckets = await listBucketsLight(s3, region, accountId);
            const scheduledStacks = await getScheduledStacks(ssm2, cloudformation2, coreStackName);
            const buckets = await Promise.all(lightBuckets.map((lightBucket) => enrichBucket(s3, sns, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, lightBucket.name, lightBucket.region)));
            return buckets.filter((bucket) => bucket !== null);
          }
          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 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/lib-refresh-bucket-cache.js
          var TTL_IN_SECONDS = 3600;
          async function cacheBucket(dynamodb2, partition, region, accountId, { accountConnectionId, roleArn, externalId }, ttl, bucket, bucketCacheTableName) {
            const bucketCacheItem = {
              bucket_name: { S: bucket.name },
              bucket_account_id: { S: bucket.accountId },
              bucket_realtime_enabled: { BOOL: bucket.realtimeEnabled },
              bucket_realtime_event_notification_disable_possible: { BOOL: bucket.realtimeEventNotificationDisablePossible },
              bucket_realtime_event_notification_enable_possible: { BOOL: bucket.realtimeEventNotificationEnablePossible },
              bucket_scheduled_enabled: { BOOL: bucket.scheduledEnabled },
              bucket_scheduled_stack_disable_possible: { BOOL: bucket.scheduledStackDisablePossible },
              partition: { S: partition },
              region: { S: region },
              account_id: { S: accountId },
              ttl: { N: ttl.toString() }
            };
            if (bucket.organizationId !== void 0) {
              bucketCacheItem.bucket_organization_id = { S: bucket.organizationId };
            }
            if (accountConnectionId !== void 0) {
              bucketCacheItem.account_connection_id = { S: accountConnectionId };
              bucketCacheItem.role_arn = { S: roleArn };
              bucketCacheItem.external_id = { S: externalId };
            }
            if (bucket.errorMessage !== void 0) {
              bucketCacheItem.bucket_error_message = { S: bucket.errorMessage };
            }
            if (bucket.region !== void 0) {
              bucketCacheItem.bucket_region = { S: bucket.region };
            }
            if (bucket.scheduledStackId !== void 0) {
              bucketCacheItem.bucket_scheduled_stack_id = { S: bucket.scheduledStackId };
            }
            await dynamodb2.send(new import_client_dynamodb2.PutItemCommand({
              TableName: bucketCacheTableName,
              Item: bucketCacheItem
            }));
          }
          function getCredentials(accountConnectionId, roleArn, externalId) {
            if (accountConnectionId !== void 0 && roleArn !== void 0 && externalId !== void 0) {
              return (0, import_credential_providers2.fromTemporaryCredentials)({
                params: {
                  RoleArn: roleArn,
                  ExternalId: externalId,
                  RoleSessionName: "bucketav",
                  DurationSeconds: 3600
                }
              });
            } else {
              return (0, import_credential_providers2.fromNodeProviderChain)();
            }
          }
          function generateTtl() {
            return Math.floor(Date.now() / 1e3) + TTL_IN_SECONDS;
          }
          async function refreshAccount2(dynamodb2, ssm2, cloudformation2, partition, region, accountId, { accountConnectionId, roleArn, externalId, organizationId }, ttl, coreAccountId, coreStackName, scanQueueArn, bucketCacheTableName) {
            const credentials = getCredentials(accountConnectionId, roleArn, externalId);
            const s3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01", credentials });
            const sns = new import_client_sns2.SNSClient({ apiVersion: "2010-03-31", credentials });
            const eventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07", credentials });
            const coreEventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07" });
            const buckets = await listBuckets(s3, ssm2, cloudformation2, sns, eventbridge, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn);
            for (const bucket of buckets) {
              await cacheBucket(dynamodb2, partition, region, accountId, { accountConnectionId, roleArn, externalId }, ttl, bucket, bucketCacheTableName);
            }
          }
          async function refreshCoreAccount(dynamodb2, ssm2, cloudformation2, region, accountId, coreAccountId, coreStackName, ttl, scanQueueArn, bucketCacheTableName) {
            const organizationId = await fetchCachedOrganizationId(ssm2, coreStackName);
            await refreshAccount2(dynamodb2, ssm2, cloudformation2, "aws", region, accountId, { organizationId }, ttl, coreAccountId, coreStackName, scanQueueArn, bucketCacheTableName);
          }

          // lambda/refresh-bucket-cache.js
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var cloudformation = new import_client_cloudformation2.CloudFormationClient({ apiVersion: "2006-03-01", maxAttempts: 10 });
          var dynamodb = new import_client_dynamodb3.DynamoDBClient({ apiVersion: "2012-08-10" });
          async function handler(event, context) {
            console.log(`Invoke: ${JSON.stringify(event)} ${JSON.stringify(context)}`);
            const bucketCacheTableName = `${process.env.CORE_STACK_NAME}-BucketCache`;
            const ttl = generateTtl();
            if ("RequestType" in event) {
              if (event.RequestType === "Create" || event.RequestType === "Update") {
                try {
                  await refreshCoreAccount(dynamodb, ssm, cloudformation, process.env.AWS_REGION, process.env.AWS_ACCOUNT_ID, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, ttl, process.env.SCAN_QUEUE_ARN, bucketCacheTableName);
                  await cfnCustomResourceSuccess(event, process.env.CORE_STACK_NAME, {});
                } catch (err) {
                  console.log(err);
                  await cfnCustomResourceFailed(event, process.env.CORE_STACK_NAME, err.message);
                }
              } else if (event.RequestType === "Delete") {
                await cfnCustomResourceSuccess(event, process.env.CORE_STACK_NAME, {});
              } else {
                throw new Error("unsupported request type");
              }
            } else {
              if ("eof" in event) {
                const scanParams = {
                  TableName: `${process.env.CORE_STACK_NAME}-AccountConnection`,
                  Limit: 5,
                  ConsistentRead: true
                };
                if ("exclusiveStartKey" in event) {
                  scanParams.ExclusiveStartKey = event.exclusiveStartKey;
                }
                const scanData = await dynamodb.send(new import_client_dynamodb3.ScanCommand(scanParams));
                for (const accountConnectionItem of scanData.Items) {
                  const accountConnectionId = accountConnectionItem.account_connection_id.S;
                  const roleArn = generateRoleArnFromItem(accountConnectionItem);
                  const externalId = generateExternalIdFromItem(accountConnectionItem);
                  const organizationId = accountConnectionItem.organization_id?.S;
                  await refreshAccount2(dynamodb, ssm, cloudformation, accountConnectionItem.partition.S, accountConnectionItem.region.S, accountConnectionItem.account_id.S, { accountConnectionId, roleArn, externalId, organizationId }, ttl, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN, bucketCacheTableName);
                }
                if ("LastEvaluatedKey" in scanData) {
                  return {
                    exclusiveStartKey: scanData.LastEvaluatedKey,
                    eof: false
                  };
                } else {
                  return {
                    eof: true
                  };
                }
              } else {
                await refreshCoreAccount(dynamodb, ssm, cloudformation, process.env.AWS_REGION, process.env.AWS_ACCOUNT_ID, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, ttl, process.env.SCAN_QUEUE_ARN, bucketCacheTableName);
                return {
                  eof: false
                };
              }
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          AWS_PARTITION:
            Ref: AWS::Partition
          CORE_STACK_NAME:
            Ref: AWS::StackName
          SCAN_QUEUE_ARN:
            Fn::GetAtt:
              - ScanQueue
              - Arn
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasRefreshBucketCacheFunctionReservedConcurrentExecutions
          - Ref: RefreshBucketCacheFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - RefreshBucketCacheRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 900
      VpcConfig:
        Ref: AWS::NoValue
    Condition: HasCrossAccount
  RefreshBucketCacheLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: RefreshBucketCacheFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasCrossAccount
  RefreshBucketCachePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - RefreshBucketCacheLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: RefreshBucketCacheRole
    Condition: HasCrossAccount
  StateMachineNameGeneratorRole:
    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
    Condition: HasCrossAccount
  StateMachineNameGeneratorFunction:
    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/state-machine-name-generator.js
          var state_machine_name_generator_exports = {};
          __export(state_machine_name_generator_exports, {
            generateName: () => generateName,
            handler: () => handler
          });
          module.exports = __toCommonJS(state_machine_name_generator_exports);

          // 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");
          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");
            }
          }

          // lambda/state-machine-name-generator.js
          var MAX_LENGTH = 80;
          function generateName(stackName, stackId, suffix) {
            if (stackName.length + suffix.length > MAX_LENGTH) {
              const resumingLength = MAX_LENGTH - (stackId.length + 1);
              const maxPrefixLength = Math.floor(resumingLength / 2);
              const maxSuffixLength = resumingLength - maxPrefixLength;
              const stackNameLength = Math.min(stackName.length, resumingLength - Math.min(suffix.length, maxSuffixLength));
              const suffixLength = Math.min(suffix.length, resumingLength - Math.min(stackName.length, maxPrefixLength));
              return stackName.substr(0, stackNameLength) + "-" + stackId + suffix.substr(0, suffixLength);
            } else {
              return stackName + suffix;
            }
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const stackName = event.StackId.split("/")[1];
            const stackId = event.StackId.split("/")[2].replaceAll("-", "");
            if (event.RequestType === "Create") {
              await cfnCustomResourceSuccess(event, generateName(stackName, stackId, event.ResourceProperties.Suffix));
            } else if (event.RequestType === "Update") {
              if (event.ResourceProperties.Suffix !== event.OldResourceProperties.Suffix) {
                await cfnCustomResourceSuccess(event, generateName(stackName, stackId, event.ResourceProperties.Suffix));
              } else {
                await cfnCustomResourceSuccess(event, event.PhysicalResourceId);
              }
            } else if (event.RequestType === "Delete") {
              await cfnCustomResourceSuccess(event, event.PhysicalResourceId);
            } else {
              throw new Error("unsupported request type");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            generateName,
            handler
          });
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasStateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - Ref: StateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - StateMachineNameGeneratorRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 30
      VpcConfig:
        Ref: AWS::NoValue
    Condition: HasCrossAccount
  StateMachineNameGeneratorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: StateMachineNameGeneratorFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasCrossAccount
  StateMachineNameGeneratorPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - StateMachineNameGeneratorLogGroup
                - Arn
      PolicyName: lambda
      Roles:
        - Ref: StateMachineNameGeneratorRole
    Condition: HasCrossAccount
  StateMachineNameGeneratorRefreshBucketCache:
    Type: Custom::StateMachineNameGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - StateMachineNameGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Suffix: -refresh-bucket-cache
    DependsOn:
      - StateMachineNameGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Condition: HasCrossAccount
  RefreshBucketCacheStateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/vendedlogs/states/
            - Ref: AWS::StackName
            - -RefreshBucketCacheStateMachine
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasCrossAccount
  RefreshBucketCacheStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: lambda:InvokeFunction
                Resource:
                  Fn::GetAtt:
                    - RefreshBucketCacheFunction
                    - Arn
              - Effect: Allow
                Action:
                  - logs:CreateLogDelivery
                  - logs:GetLogDelivery
                  - logs:UpdateLogDelivery
                  - logs:DeleteLogDelivery
                  - logs:ListLogDeliveries
                  - logs:PutResourcePolicy
                  - logs:DescribeResourcePolicies
                  - logs:DescribeLogGroups
                Resource: "*"
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  Fn::GetAtt:
                    - RefreshBucketCacheStateMachineLogGroup
                    - Arn
          PolicyName: states
    Condition: HasCrossAccount
  RefreshBucketCacheStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: bucketAV (refresh bucket cache).
        TimeoutSeconds: 1800
        StartAt: RefreshBucketCache
        Version: "1.0"
        States:
          RefreshBucketCache:
            Type: Task
            Resource:
              Fn::GetAtt:
                - RefreshBucketCacheFunction
                - Arn
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Next: CheckEndOfPage
          CheckEndOfPage:
            Type: Choice
            Choices:
              - Variable: $.eof
                BooleanEquals: true
                Next: Done
            Default: RefreshBucketCache
          Done:
            Type: Succeed
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn:
                Fn::GetAtt:
                  - RefreshBucketCacheStateMachineLogGroup
                  - Arn
        IncludeExecutionData: true
        Level: ERROR
      RoleArn:
        Fn::GetAtt:
          - RefreshBucketCacheStateMachineRole
          - Arn
      StateMachineName:
        Ref: StateMachineNameGeneratorRefreshBucketCache
    DependsOn:
      - RefreshBucketCachePolicy
    Condition: HasCrossAccount
  RefreshBucketCacheStateMachineAlarmExecutionsFailed:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription:
        Fn::Join:
          - ""
          - - "Refresh bucket cache failed (state machine "
            - Ref: RefreshBucketCacheStateMachine
            - ). Please follow https://bucketav.com/help/operations/monitoring-alerting.html#refreshbucketcachestatemachinealarmexecutionsfailed
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: RefreshBucketCacheStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsFailed
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshBucketCacheStateMachineAlarmExecutionsTimedOut:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription:
        Fn::Join:
          - ""
          - - "Refresh bucket cache timed out(state machine "
            - Ref: RefreshBucketCacheStateMachine
            - ). Please follow https://bucketav.com/help/operations/monitoring-alerting.html#refreshbucketcachestatemachinealarmexecutionstimedout
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: RefreshBucketCacheStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsTimedOut
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
    Condition: HasCrossAccount
  RefreshBucketCacheCronRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: states:StartExecution
                Resource:
                  Ref: RefreshBucketCacheStateMachine
          PolicyName: events
    Condition: HasCrossAccount
  RefreshBucketCacheCron:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(30 minutes)
      Targets:
        - Arn:
            Ref: RefreshBucketCacheStateMachine
          Id: states
          Input: "{}"
          RoleArn:
            Fn::GetAtt:
              - RefreshBucketCacheCronRole
              - Arn
    Condition: HasCrossAccount
  RefreshBucketCache:
    Type: Custom::RefreshBucketCache
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - RefreshBucketCacheFunction
          - Arn
      ServiceTimeout: "900"
      Version: 2.0.0
    DependsOn:
      - RefreshBucketCachePolicy
      - RefreshBucketCacheRole
      - RefreshServiceDiscovery
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Condition: HasCrossAccount
  RefreshBucketCacheRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventSource:
            - s3.amazonaws.com
            - events.amazonaws.com
          eventName:
            - CreateBucket
            - DeleteBucket
            - PutBucketNotification
            - PutBucketNotificationConfiguration
            - PutRule
            - DeleteRule
            - PutTargets
            - RemoveTargets
            - EnableRule
            - DisableRule
            - PutPermission
            - RemovePermission
      Targets:
        - Arn:
            Ref: AccountConnectionTopic
          Id: sns
          InputTransformer:
            InputTemplate:
              Fn::Join:
                - ""
                - - '{"action": "refreshAccount", "partition": "'
                  - Ref: AWS::Partition
                  - '", "region": "'
                  - Ref: AWS::Region
                  - '", "accountId": "'
                  - Ref: AWS::AccountId
                  - '"}'
    Condition: HasCrossAccount
  GovernanceFindingTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: finding_id
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: finding_id
          KeyType: HASH
      SSESpecification:
        SSEEnabled: true
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true
    Condition: HasGovernance
  GovernanceLambdaRole:
    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:
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: dynamodb:Scan
                    Resource:
                      Fn::GetAtt:
                        - BucketCacheTable
                        - Arn
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStacks
                  - s3:ListAllMyBuckets
                  - s3:GetBucketNotification
                  - sns:ListSubscriptionsByTopic
                  - events:ListRuleNamesByTarget
                  - events:DescribeRule
                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: events:DescribeEventBus
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :event-bus/default
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: dynamodb:Scan
                    Resource:
                      Fn::GetAtt:
                        - AccountConnectionTable
                        - Arn
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: sts:AssumeRole
                    Resource:
                      Fn::Join:
                        - ""
                        - - "arn:"
                          - Ref: AWS::Partition
                          - :iam::*:role/
                          - Ref: AWS::StackName
                          - -AccountConnection
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: cloudwatch:GetMetricStatistics
                Resource: "*"
              - Effect: Allow
                Action: sns:Publish
                Resource:
                  Fn::GetAtt:
                    - InfrastructureAlarmsTopic
                    - TopicArn
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:DeleteItem
                  - dynamodb:Scan
                Resource:
                  Fn::GetAtt:
                    - GovernanceFindingTable
                    - Arn
          PolicyName: lambda
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - GovernanceLambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
    Condition: HasGovernance
  GovernanceLambdaFunction:
    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/governance.js
          var governance_exports = {};
          __export(governance_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(governance_exports);
          var import_client_cloudformation2 = require("@aws-sdk/client-cloudformation");
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_sns2 = require("@aws-sdk/client-sns");
          var import_client_eventbridge2 = require("@aws-sdk/client-eventbridge");
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_cloudwatch = require("@aws-sdk/client-cloudwatch");

          // lambda/lib-governance.js
          var ONE_DAY_IN_SECONDS = 60 * 60 * 24;
          var FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING = "bucketav:real-time-file-scan-missing";
          var FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING = "bucketav:scheduled-bucket-scan-missing";
          var FINDING_TYPE_NO_SCAN_ACTIVITY = "bucketav:no-scan-activity";
          var FINDING_TYPE_NO_SCAN_ACTIVITY_DAYS = 14;
          var FINDING_TYPE_OUTDATED_CORE = "bucketav:outdated-core";
          var FINDING_TYPE_OUTDATED_ADDONS = "bucketav:outdated-addons";
          var FINDING_DATA = {
            [FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING]: {
              findingTitle: "Real-time file scan not enabled",
              findingDescription: "The bucket is not protected by real-time file scanning.",
              remediationDescription: "To enable real-time file scanning, please follow",
              remediationUrl: "https://bucketav.com/help/scan-modes/real-time-file-scan.html"
            },
            [FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING]: {
              findingTitle: "Scheduled bucket scan not enabled",
              findingDescription: "The bucket is not protected by scheduled bucket scanning.",
              remediationDescription: "To enable scheduled-bucket scanning, please follow",
              remediationUrl: "https://bucketav.com/help/scan-modes/scheduled-bucket-scan.html"
            },
            [FINDING_TYPE_NO_SCAN_ACTIVITY]: {
              findingTitle: "No scan activity",
              findingDescription: `Your bucketAV setup is likely incomplete\u2014no scan activity has been found in the past ${FINDING_TYPE_NO_SCAN_ACTIVITY_DAYS} days.`,
              remediationDescription: "To complete your bucketAV setup, please follow",
              remediationUrl: "https://bucketav.com/help/setup-guide/",
              ttlInSeconds: ONE_DAY_IN_SECONDS * FINDING_TYPE_NO_SCAN_ACTIVITY_DAYS
            },
            [FINDING_TYPE_OUTDATED_CORE]: {
              findingTitle: "bucketAV requires an update",
              findingDescription: "Your bucketAV setup is running on outdated version.",
              remediationDescription: "To update bucketAV, please follow",
              remediationUrl: "https://bucketav.com/help/update-guide/"
            },
            [FINDING_TYPE_OUTDATED_ADDONS]: {
              findingTitle: "bucketAV Add-Ons require updates",
              findingDescription: "Your bucketAV setup is running outdated Add-On versions.",
              remediationDescription: "To update bucketAV, please follow",
              remediationUrl: "https://bucketav.com/help/update-guide/"
            }
          };

          // 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/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;
          }
          function mapCachedBucketItem(item) {
            const bucket = {
              name: item.bucket_name.S,
              accountId: item.bucket_account_id.S,
              realtimeEnabled: item.bucket_realtime_enabled.BOOL,
              realtimeEventNotificationEnablePossible: item.bucket_realtime_event_notification_enable_possible.BOOL,
              realtimeEventNotificationDisablePossible: item.bucket_realtime_event_notification_disable_possible.BOOL,
              scheduledEnabled: item.bucket_scheduled_enabled.BOOL,
              scheduledStackDisablePossible: item.bucket_scheduled_stack_disable_possible.BOOL
            };
            if ("bucket_organization_id" in item) {
              bucket.organizationId = item.bucket_organization_id.S;
            }
            if ("bucket_region" in item) {
              bucket.region = item.bucket_region.S;
            }
            if ("bucket_error_message" in item) {
              bucket.errorMessage = item.bucket_error_message.S;
            }
            if ("bucket_scheduled_stack_id" in item) {
              bucket.scheduledStackId = item.bucket_scheduled_stack_id.S;
            }
            return bucket;
          }
          async function listCachedBuckets(dynamodb2, bucketCacheTableName) {
            const paginatorScan = await (0, import_client_dynamodb.paginateScan)({ client: dynamodb2 }, { TableName: bucketCacheTableName });
            const buckets = [];
            for await (const page of paginatorScan) {
              for (const item of page.Items) {
                buckets.push(mapCachedBucketItem(item));
              }
            }
            return buckets;
          }
          async function listBucketsLight(s32, region, accountId) {
            const allBuckets = [];
            const input = {};
            if (region !== void 0 && region !== null) {
              input.BucketRegion = region;
            }
            const paginator = (0, import_client_s3.paginateListBuckets)({ client: s32, pageSize: 1e3 }, input);
            for await (const page of paginator) {
              allBuckets.push(...page.Buckets.map((bucket) => {
                if ("BucketRegion" in bucket) {
                  return {
                    name: bucket.Name,
                    region: bucket.BucketRegion,
                    accountId
                  };
                } else {
                  return {
                    name: bucket.Name,
                    accountId,
                    errorMessage: `Can not get region for bucket ${bucket.Name}`
                  };
                }
              }));
            }
            if (region !== void 0 && region !== null) {
              return allBuckets.filter((bucket) => bucket.region === region || bucket.region === void 0);
            } else {
              return allBuckets;
            }
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function checkEventBridgeRules(eventbridge2, ruleNames, bucketName) {
            const rules = await Promise.all(ruleNames.map((ruleName) => eventbridge2.send(new import_client_eventbridge.DescribeRuleCommand({ Name: ruleName }))));
            return rules.filter((rule) => rule.State === "ENABLED" && rule.EventPattern).map((rule) => JSON.parse(rule.EventPattern)).find((pattern) => pattern.source.includes("aws.s3") && pattern["detail-type"].includes("Object Created") && (pattern?.detail?.bucket?.name?.includes(bucketName) || pattern?.detail?.bucket?.name === void 0)) !== void 0;
          }
          async function enrichBucket(s32, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, bucketName, bucketRegion) {
            const bucket = {
              name: bucketName,
              accountId,
              organizationId
            };
            if (bucketRegion === void 0) {
              bucket.region = void 0;
              bucket.errorMessage = `Can not get region for bucket ${bucketName}`;
            } else {
              bucket.region = bucketRegion;
            }
            if (bucket.region === region || bucket.region === void 0) {
              let realtimeEnabled = false;
              let realtimeEventNotificationEnablePossible = bucket.region !== void 0;
              let realtimeEventNotificationDisablePossible = false;
              let scheduledEnabled = false;
              let scheduledStackDisablePossible = false;
              let scheduledStackId = void 0;
              try {
                const notificationData = await s32.send(new import_client_s3.GetBucketNotificationConfigurationCommand({ Bucket: bucket.name, ExpectedBucketOwner: accountId }));
                if (notificationData?.QueueConfigurations?.find((config) => config.QueueArn === scanQueueArn && config.Events.includes("s3:ObjectCreated:*")) !== void 0) {
                  realtimeEnabled = true;
                  realtimeEventNotificationDisablePossible = true;
                }
                if (Array.isArray(notificationData?.TopicConfigurations)) {
                  await Promise.all(notificationData.TopicConfigurations.filter((config) => config.Events.includes("s3:ObjectCreated:*")).map(async (config) => {
                    const paginatorListSubscriptionsByTopic = await (0, import_client_sns.paginateListSubscriptionsByTopic)({ client: sns2, pageSize: 50 }, { TopicArn: config.TopicArn });
                    for await (const page of paginatorListSubscriptionsByTopic) {
                      if (page.Subscriptions.find((subscription) => subscription.Protocol === "sqs" && subscription.Endpoint === scanQueueArn) !== void 0) {
                        realtimeEnabled = true;
                      }
                    }
                  }));
                }
                if (notificationData?.EventBridgeConfiguration) {
                  if (accountId !== coreAccountId) {
                    let crossAccoutRule = false;
                    const paginatorListRuleNamesByTargetDefaultBus = await paginateListRuleNamesByTarget({ client: eventbridge2, pageSize: 10 }, { TargetArn: `arn:${partition}:events:${region}:${coreAccountId}:event-bus/default` });
                    for await (const page of paginatorListRuleNamesByTargetDefaultBus) {
                      if (await checkEventBridgeRules(eventbridge2, page.RuleNames, bucket.name) === true) {
                        crossAccoutRule = true;
                      }
                    }
                    const { Policy: defaultBusPolicyJson } = await coreEventbridge.send(new import_client_eventbridge.DescribeEventBusCommand({
                      Name: "default"
                    }));
                    if (defaultBusPolicyJson !== void 0) {
                      const defaultBusPolicy = JSON.parse(defaultBusPolicyJson);
                      if (defaultBusPolicy.Statement.find((s) => s?.Effect === "Allow" && s?.Action === "events:PutEvents" && (s?.Principal?.AWS === accountId || s?.Principal === "*" && s?.Condition?.StringEquals["aws:PrincipalOrgID"] === organizationId)) !== void 0) {
                        if (crossAccoutRule === true) {
                          realtimeEnabled = true;
                        }
                      }
                    }
                  }
                  const paginatorListRuleNamesByTargetScanQueue = await paginateListRuleNamesByTarget({ client: coreEventbridge, pageSize: 10 }, { TargetArn: scanQueueArn });
                  for await (const page of paginatorListRuleNamesByTargetScanQueue) {
                    if (await checkEventBridgeRules(coreEventbridge, page.RuleNames, bucket.name) === true) {
                      realtimeEnabled = true;
                    }
                  }
                }
                if (notificationData?.TopicConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.QueueConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.LambdaFunctionConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.EventBridgeConfiguration !== void 0) {
                  realtimeEventNotificationEnablePossible = false;
                }
              } 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, bucket.name));
              if (scheduledStacksIncludesBucket.length > 0) {
                scheduledEnabled = true;
                scheduledStackId = scheduledStacksIncludesBucket[0].id;
              }
              const scheduledStacksHasBucket = scheduledStacks.stacks.filter((scheduledStack) => hasBucket(scheduledStack, bucket.name));
              if (scheduledStacksHasBucket.length === 1) {
                scheduledEnabled = true;
                scheduledStackDisablePossible = true;
                scheduledStackId = scheduledStacksHasBucket[0].id;
              }
              return {
                ...bucket,
                realtimeEnabled,
                realtimeEventNotificationEnablePossible,
                realtimeEventNotificationDisablePossible,
                scheduledEnabled,
                scheduledStackDisablePossible,
                scheduledStackId
              };
            } else {
              return null;
            }
          }
          async function getScheduledStacks(ssm2, cloudformation2, coreStackName) {
            const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm2 }, { 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) => cloudformation2.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 listBuckets(s32, ssm2, cloudformation2, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn) {
            const lightBuckets = await listBucketsLight(s32, region, accountId);
            const scheduledStacks = await getScheduledStacks(ssm2, cloudformation2, coreStackName);
            const buckets = await Promise.all(lightBuckets.map((lightBucket) => enrichBucket(s32, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, lightBucket.name, lightBucket.region)));
            return buckets.filter((bucket) => bucket !== null);
          }
          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(defaultCloudformation, defaultSsm, dynamodb2, platform, latest, isCrossAccount, corePartition, coreRegion, coreAccountId, coreStackName, accountConnectionTableName) {
            const outdatedAddons = [];
            const fn = async (ssm2, cloudformation2, partition, region, accountId) => {
              const cloudformaionDescribeStacksLimit = pLimit(8);
              const paginatorGetParametersByPath = await (0, import_client_ssm.paginateGetParametersByPath)({ client: ssm2 }, { 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 cloudformation2.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(defaultSsm, defaultCloudformation, 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 ssm2 = new import_client_ssm.SSMClient({ apiVersion: "2014-11-06", credentials });
                  const cloudformation2 = new import_client_cloudformation.CloudFormationClient({ apiVersion: "2006-03-01", credentials, maxAttempts: 10 });
                  return fn(ssm2, cloudformation2, item.partition.S, item.region.S, item.account_id.S);
                }));
              }
            } else {
              await fn(defaultSsm, defaultCloudformation, corePartition, coreRegion, coreAccountId);
            }
            return outdatedAddons;
          }
          async function checkVersion(cloudformation2, ssm2, dynamodb2, platform, latest, isCrossAccount, corePartition, coreRegion, coreAccountId, coreStackName, accountConnectionTableName) {
            const describeStacksData = await cloudformation2.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(cloudformation2, ssm2, 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/governance.js
          var cloudformation = new import_client_cloudformation2.CloudFormationClient({ apiVersion: "2006-03-01", maxAttempts: 10 });
          var s3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var sns = new import_client_sns2.SNSClient({ apiVersion: "2010-03-31" });
          var eventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07" });
          var dynamodb = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
          var cloudwatch = new import_client_cloudwatch.CloudWatchClient({ apiVersion: "2010-08-01" });
          function generateFindingId(findingType, bucket = null) {
            if (bucket === null) {
              return `bucketav:${process.env.CORE_STACK_NAME}:core:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:finding/${findingType}`;
            }
            return `bucketav:${process.env.CORE_STACK_NAME}:core:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:finding/${findingType}/${bucket.name}`;
          }
          function toString(bucketName, bucketAccountId) {
            if (process.env.CROSS_ACCOUNT === "true") {
              return `${bucketName} (${bucketAccountId})`;
            } else {
              return bucketName;
            }
          }
          function truncateValue(input, maxLength) {
            const suffix = "...";
            const str = String(input);
            const maxLengthWithoutSuffix = maxLength - suffix.length;
            if (str.length > maxLength) {
              return str.substr(0, maxLengthWithoutSuffix) + suffix;
            }
            return str;
          }
          async function publish(subject, message, data) {
            await sns.send(new import_client_sns2.PublishCommand({
              TopicArn: process.env.INFRASTRUCTURE_ALARMS_TOPIC_ARN,
              Subject: subject,
              // max 100 chars long
              Message: JSON.stringify({
                default: JSON.stringify(data),
                email: message
              }),
              MessageStructure: "json",
              MessageAttributes: {
                action: {
                  DataType: "String",
                  StringValue: data.action
                }
              }
            }));
          }
          function toActionLabel(action) {
            switch (action) {
              case "report":
                return "created";
              case "close":
                return "closed";
              case "archive":
                return "archived";
              default:
                throw new Error("unsupported action");
            }
          }
          function generateSubject(data) {
            let subject = "bucketAV Governance finding ";
            subject += truncateValue(toActionLabel(data.action), 8);
            subject += ": ";
            subject += truncateValue(process.env.CORE_STACK_NAME, 35);
            subject += " (";
            subject += truncateValue(process.env.AWS_REGION, 10);
            subject += ", ";
            subject += truncateValue(process.env.AWS_ACCOUNT_ID, 12);
            subject += ")";
            return truncateValue(subject, 100);
          }
          async function reportFinding(now, findingType) {
            const createdAt = now.toISOString();
            const findingId = generateFindingId(findingType);
            const {
              findingTitle,
              findingDescription,
              remediationDescription,
              remediationUrl,
              ttlInSeconds = null
            } = FINDING_DATA[findingType];
            const data = {
              action: "report",
              findings: [{
                id: findingId,
                type: findingType,
                createdAt,
                updatedAt: createdAt
              }]
            };
            let message = `AWS Region: ${process.env.AWS_REGION}\r
          `;
            message += `AWS Account ID: ${process.env.AWS_ACCOUNT_ID}\r
          `;
            message += `bucketAV Stack Name: ${process.env.CORE_STACK_NAME}\r
          `;
            message += "\r\n\r\n";
            message += "==================================================\r\n";
            message += `Finding created: ${findingTitle}\r
          `;
            message += "==================================================\r\n\r\n";
            message += `${findingDescription}\r
          `;
            message += "\r\n\r\n";
            message += `${remediationDescription} <${remediationUrl}>\r
          `;
            message += "\r\n\r\n";
            message += "Do you need help with this email? Reach out to us <hello@bucketav.com>\r\n";
            message += "Warmly, The bucketAV Team.\r\n";
            await publish(generateSubject(data), message, data);
            const item = {
              finding_id: { S: findingId },
              finding_type: { S: findingType },
              created_at: { S: createdAt }
            };
            if (ttlInSeconds !== null) {
              item.ttl = { N: (now.getTime() / 1e3 + ttlInSeconds).toFixed(0) };
            }
            await dynamodb.send(new import_client_dynamodb2.PutItemCommand({
              TableName: process.env.TABLE_NAME,
              Item: item
            }));
          }
          async function reportFindings(now, buckets, findingType) {
            const createdAt = now.toISOString();
            const {
              findingTitle,
              remediationDescription,
              remediationUrl,
              ttlInSeconds = null
            } = FINDING_DATA[findingType];
            if (buckets.length > 0) {
              const data = {
                action: "report",
                findings: buckets.slice(0, 500).map((bucket) => ({
                  id: generateFindingId(findingType, bucket),
                  type: findingType,
                  bucketName: bucket.name,
                  bucketAccountId: bucket.accountId,
                  createdAt,
                  updatedAt: createdAt
                })),
                truncated: buckets.length > 500
              };
              let message = `AWS Region: ${process.env.AWS_REGION}\r
          `;
              message += `AWS Account ID: ${process.env.AWS_ACCOUNT_ID}\r
          `;
              message += `bucketAV Stack Name: ${process.env.CORE_STACK_NAME}\r
          `;
              message += "\r\n\r\n";
              message += "==================================================\r\n";
              message += `Finding created: ${findingTitle}\r
          `;
              message += "==================================================\r\n\r\n";
              message += "The following buckets are not protected:\r\n\r\n";
              message += buckets.slice(0, 500).map((bucket) => toString(bucket.name, bucket.accountId)).join("\r\n");
              if (buckets.length > 500) {
                message += "...\r\n";
              }
              message += "\r\n\r\n";
              message += `${remediationDescription} <${remediationUrl}>\r
          `;
              message += "\r\n\r\n";
              message += "Do you need help with this email? Reach out to us <hello@bucketav.com>\r\n";
              message += "Warmly, The bucketAV Team.\r\n";
              await publish(generateSubject(data), message, data);
            }
            for (const bucket of buckets) {
              const item = {
                finding_id: { S: generateFindingId(findingType, bucket) },
                finding_type: { S: findingType },
                bucket_name: { S: bucket.name },
                bucket_account_id: { S: bucket.accountId },
                created_at: { S: createdAt }
              };
              if (ttlInSeconds !== null) {
                item.ttl = { N: (now.getTime() / 1e3 + ttlInSeconds).toFixed(0) };
              }
              await dynamodb.send(new import_client_dynamodb2.PutItemCommand({
                TableName: process.env.TABLE_NAME,
                Item: item
              }));
            }
          }
          async function closeFindings(now, reportedFindingsById, buckets, findingType) {
            const updatedAt = now.toISOString();
            if (buckets.length > 0) {
              const { findingTitle } = FINDING_DATA[findingType];
              const data = {
                action: "close",
                findings: buckets.slice(0, 500).map((bucket) => {
                  const id = generateFindingId(findingType, bucket);
                  return {
                    id,
                    type: findingType,
                    bucketName: bucket.name,
                    bucketAccountId: bucket.accountId,
                    createdAt: reportedFindingsById[id].createdAt,
                    updatedAt
                  };
                }),
                truncated: buckets.length > 500
              };
              let message = `AWS Region: ${process.env.AWS_REGION}\r
          `;
              message += `AWS Account ID: ${process.env.AWS_ACCOUNT_ID}\r
          `;
              message += `bucketAV Stack Name: ${process.env.CORE_STACK_NAME}\r
          `;
              message += "\r\n\r\n";
              message += "==================================================\r\n";
              message += `Finding closed: ${findingTitle}\r
          `;
              message += "==================================================\r\n\r\n";
              message += "The following buckets are protected:\r\n\r\n";
              message += buckets.slice(0, 500).map((bucket) => toString(bucket.name, bucket.accountId)).join("\r\n");
              if (buckets.length > 500) {
                message += "...\r\n";
              }
              message += "\r\n\r\n";
              message += "Do you need help with this email? Reach out to us <hello@bucketav.com>\r\n";
              message += "Warmly, The bucketAV Team.\r\n";
              await publish(generateSubject(data), message, data);
            }
            for (const bucket of buckets) {
              await dynamodb.send(new import_client_dynamodb2.DeleteItemCommand({
                TableName: process.env.TABLE_NAME,
                Key: {
                  finding_id: { S: generateFindingId(findingType, bucket) }
                }
              }));
            }
          }
          async function archiveFindings(now, reportedFindings, findingType) {
            const updatedAt = now.toISOString();
            if (reportedFindings.length > 0) {
              const { findingTitle } = FINDING_DATA[findingType];
              const data = {
                action: "archive",
                findings: reportedFindings.slice(0, 500).map((reportedFinding) => ({
                  id: reportedFinding.findingId,
                  type: findingType,
                  bucketName: reportedFinding.bucketName,
                  bucketAccountId: reportedFinding.bucketAccountId,
                  createdAt: reportedFinding.createdAt,
                  updatedAt
                })),
                truncated: reportedFindings.length > 500
              };
              let message = `AWS Region: ${process.env.AWS_REGION}\r
          `;
              message += `AWS Account ID: ${process.env.AWS_ACCOUNT_ID}\r
          `;
              message += `bucketAV Stack Name: ${process.env.CORE_STACK_NAME}\r
          `;
              message += "\r\n\r\n";
              message += "==================================================\r\n";
              message += `Finding archived: ${findingTitle}\r
          `;
              message += "==================================================\r\n\r\n";
              message += "The following buckets have been deleted and are therefore no longer unprotected:\r\n\r\n";
              message += reportedFindings.slice(0, 500).map((reportedFinding) => toString(reportedFinding.bucketName, reportedFinding.bucketAccountId)).join("\r\n");
              if (reportedFindings.length > 500) {
                message += "...\r\n";
              }
              message += "\r\n\r\n";
              message += "Do you need help with this email? Reach out to us <hello@bucketav.com>\r\n";
              message += "Warmly, The bucketAV Team.\r\n";
              await publish(generateSubject(data), message, data);
            }
            for (const reportedFinding of reportedFindings) {
              await dynamodb.send(new import_client_dynamodb2.DeleteItemCommand({
                TableName: process.env.TABLE_NAME,
                Key: {
                  finding_id: { S: reportedFinding.findingId }
                }
              }));
            }
          }
          async function reportRealTimeFileScanMissingFindings(now, buckets) {
            await reportFindings(now, buckets, FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING);
          }
          async function closeRealTimeFileScanMissingFindings(now, reportedFindingsById, buckets) {
            await closeFindings(now, reportedFindingsById, buckets, FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING);
          }
          async function archiveRealTimeFileScanMissingFindings(now, reportedFindings) {
            await archiveFindings(now, reportedFindings, FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING);
          }
          async function reportScheduledBucketScanMissingFindings(now, buckets) {
            await reportFindings(now, buckets, FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING);
          }
          async function closeScheduledBucketScanMissingFindings(now, reportedFindingsById, buckets) {
            await closeFindings(now, reportedFindingsById, buckets, FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING);
          }
          async function archiveScheduledBucketScanMissingFindings(now, reportedFindings) {
            await archiveFindings(now, reportedFindings, FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING);
          }
          async function getMetricSum(metricName, start, end) {
            const { Datapoints: datapoints } = await cloudwatch.send(new import_client_cloudwatch.GetMetricStatisticsCommand({
              Namespace: process.env.CORE_STACK_NAME,
              MetricName: metricName,
              Statistics: ["Sum"],
              StartTime: start,
              EndTime: end,
              Period: ONE_DAY_IN_SECONDS
            }));
            return datapoints?.reduce((acc, datapoint) => acc + datapoint.Sum, 0) || 0;
          }
          async function checkScanHistory(now, days) {
            const start = new Date(now.getTime() - 1e3 * ONE_DAY_IN_SECONDS * days);
            const clean = await getMetricSum("clean", start, now);
            const infected = await getMetricSum("infected", start, now);
            const no = await getMetricSum("no", start, now);
            if (clean + infected + no === 0) {
              await reportFinding(now, FINDING_TYPE_NO_SCAN_ACTIVITY);
            }
          }
          async function handler(event, context) {
            console.log(`Invoke: ${JSON.stringify(event)} ${JSON.stringify(context)}`);
            const buckets = process.env.CROSS_ACCOUNT === "true" ? await listCachedBuckets(dynamodb, `${process.env.CORE_STACK_NAME}-BucketCache`) : await listBuckets(s3, ssm, cloudformation, sns, eventbridge, eventbridge, process.env.AWS_PARTITION, process.env.AWS_REGION, process.env.AWS_ACCOUNT_ID, void 0, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, process.env.SCAN_QUEUE_ARN);
            const bucketsByName = buckets?.reduce((acc, bucket) => {
              acc[bucket.name] = bucket;
              return acc;
            }, {}) || {};
            const now = "time" in event ? new Date(event.time) : /* @__PURE__ */ new Date();
            const reportedFindings = [];
            const reportedFindingsById = {};
            const paginator = (0, import_client_dynamodb2.paginateScan)({
              client: dynamodb
            }, {
              TableName: process.env.TABLE_NAME
            });
            for await (const page of paginator) {
              for (const item of page.Items) {
                const finding = {
                  findingId: item.finding_id.S,
                  findingType: item.finding_type.S,
                  bucketName: null,
                  bucketAccountId: null,
                  createdAt: item.created_at.S
                };
                if ("bucket_name" in item) {
                  finding.bucketName = item.bucket_name.S;
                  if ("bucket_account_id" in item) {
                    finding.bucketAccountId = item.bucket_account_id.S;
                  } else {
                    finding.bucketAccountId = process.env.AWS_ACCOUNT_ID;
                  }
                }
                reportedFindings.push(finding);
                reportedFindingsById[item.finding_id.S] = finding;
              }
            }
            const latest = await fetchLatest();
            const { runningVersion, latestVersion, outdatedAddons } = await checkVersion(cloudformation, ssm, dynamodb, process.env.PLATFORM, latest, process.env.CROSS_ACCOUNT === "true", process.env.AWS_PARTITION, process.env.AWS_REGION, process.env.AWS_ACCOUNT_ID, process.env.CORE_STACK_NAME, `${process.env.CORE_STACK_NAME}-AccountConnection`);
            if (runningVersion !== latestVersion) {
              if (!(generateFindingId(FINDING_TYPE_OUTDATED_CORE) in reportedFindingsById)) {
                await reportFinding(now, FINDING_TYPE_OUTDATED_CORE);
              }
            }
            if (outdatedAddons.length > 0) {
              if (!(generateFindingId(FINDING_TYPE_OUTDATED_ADDONS) in reportedFindingsById)) {
                await reportFinding(now, FINDING_TYPE_OUTDATED_ADDONS);
              }
            }
            await reportRealTimeFileScanMissingFindings(now, buckets.filter((bucket) => bucket.realtimeEnabled === false).filter((bucket) => !(generateFindingId(FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING, bucket) in reportedFindingsById)));
            await closeRealTimeFileScanMissingFindings(now, reportedFindingsById, buckets.filter((bucket) => bucket.realtimeEnabled === true).filter((bucket) => generateFindingId(FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING, bucket) in reportedFindingsById));
            await archiveRealTimeFileScanMissingFindings(now, reportedFindings.filter((finding) => finding.findingType === FINDING_TYPE_REAL_TIME_FILE_SCAN_MISSING).filter((finding) => !(finding.bucketName in bucketsByName)));
            await reportScheduledBucketScanMissingFindings(now, buckets.filter((bucket) => bucket.scheduledEnabled === false).filter((bucket) => !(generateFindingId(FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING, bucket) in reportedFindingsById)));
            await closeScheduledBucketScanMissingFindings(now, reportedFindingsById, buckets.filter((bucket) => bucket.scheduledEnabled === true).filter((bucket) => generateFindingId(FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING, bucket) in reportedFindingsById));
            await archiveScheduledBucketScanMissingFindings(now, reportedFindings.filter((finding) => finding.findingType === FINDING_TYPE_SCHEDULED_BUCKET_SCAN_MISSING).filter((finding) => !(finding.bucketName in bucketsByName)));
            if (!(generateFindingId(FINDING_TYPE_NO_SCAN_ACTIVITY) in reportedFindingsById)) {
              await checkScanHistory(now, FINDING_TYPE_NO_SCAN_ACTIVITY_DAYS);
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - GovernanceLambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: AWS::StackName
          AWS_PARTITION:
            Ref: AWS::Partition
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          SCAN_QUEUE_ARN:
            Fn::GetAtt:
              - ScanQueue
              - Arn
          INFRASTRUCTURE_ALARMS_TOPIC_ARN:
            Fn::GetAtt:
              - InfrastructureAlarmsTopic
              - TopicArn
          TABLE_NAME:
            Ref: GovernanceFindingTable
          CROSS_ACCOUNT:
            Fn::If:
              - HasCrossAccount
              - "true"
              - "false"
          PLATFORM: aws
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasGovernanceLambdaFunctionReservedConcurrentExecutions
          - Ref: GovernanceLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - GovernanceLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 300
      VpcConfig:
        Ref: AWS::NoValue
    Condition: HasGovernance
  GovernanceLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV Governance check failed. Check logs of AWS Lambda Function "
            - Ref: GovernanceLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: GovernanceLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasGovernance
  GovernanceLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV Governance check throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: GovernanceLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasGovernance
  GovernanceLambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
    Condition: HasGovernance
  GovernanceLambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV overnance check has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - GovernanceLambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasGovernance
  GovernanceLambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: InfrastructureAlarmsTopic
      AlarmDescription: bucketAV overnance check has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: GovernanceLambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
    Condition: HasGovernance
  GovernanceLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: GovernanceLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasGovernance
  GovernanceLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - GovernanceLambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: GovernanceLambdaRole
    Condition: HasGovernance
  GovernanceLambdaCron:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(1 day)
      Targets:
        - Arn:
            Fn::GetAtt:
              - GovernanceLambdaFunction
              - Arn
          Id: lambda
    DependsOn:
      - GovernanceLambdaPolicy
    Condition: HasGovernance
  GovernanceLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: GovernanceLambdaFunction
      Principal: events.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - GovernanceLambdaCron
          - Arn
    Condition: HasGovernance
  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:
                  - s3:ListAllMyBuckets
                  - s3:GetBucketNotification
                  - s3:GetBucketTagging
                  - sns:ListSubscriptionsByTopic
                  - events:ListRuleNamesByTarget
                  - events:DescribeRule
                Resource: "*"
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: ssm:GetParameter
                    Resource:
                      Fn::Join:
                        - ""
                        - - "arn:"
                          - Ref: AWS::Partition
                          - ":ssm:"
                          - Ref: AWS::Region
                          - ":"
                          - Ref: AWS::AccountId
                          - :parameter
                          - Ref: ServiceDiscoveryOrganizationId
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: events:DescribeEventBus
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :event-bus/default
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: dynamodb:Scan
                    Resource:
                      - Fn::GetAtt:
                          - AccountConnectionTable
                          - Arn
                      - Fn::GetAtt:
                          - BucketCacheTable
                          - Arn
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: dynamodb:GetItem
                    Resource:
                      Fn::GetAtt:
                        - AccountConnectionTable
                        - Arn
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action:
                      - dynamodb:GetItem
                      - dynamodb:PutItem
                    Resource:
                      Fn::GetAtt:
                        - BucketCacheTable
                        - Arn
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: sts:AssumeRole
                    Resource:
                      Fn::Join:
                        - ""
                        - - "arn:"
                          - Ref: AWS::Partition
                          - :iam::*:role/
                          - Ref: AWS::StackName
                          - -AccountConnection
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasCrossAccount
                  - Effect: Allow
                    Action: states:StartExecution
                    Resource:
                      Fn::GetAtt:
                        - RefreshBucketCacheStateMachine
                        - Arn
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: s3:PutBucketNotification
                Resource:
                  Ref: S3BucketRestriction
              - 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-aws.js
          var dashboard_aws_exports = {};
          __export(dashboard_aws_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(dashboard_aws_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_sns3 = require("@aws-sdk/client-sns");
          var import_client_eventbridge3 = require("@aws-sdk/client-eventbridge");
          var import_client_sfn2 = require("@aws-sdk/client-sfn");

          // 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/lib.js
          async function fetchCachedOrganizationId(ssm, coreStackName) {
            const data = await ssm.send(new import_client_ssm.GetParameterCommand({
              Name: `/bucketAV/${coreStackName}/OrganizationId`
            }));
            if (data.Parameter.Value === "INIT" || data.Parameter.Value === "NONE") {
              return void 0;
            }
            return data.Parameter.Value;
          }
          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;
          }
          function mapCachedBucketItem(item) {
            const bucket = {
              name: item.bucket_name.S,
              accountId: item.bucket_account_id.S,
              realtimeEnabled: item.bucket_realtime_enabled.BOOL,
              realtimeEventNotificationEnablePossible: item.bucket_realtime_event_notification_enable_possible.BOOL,
              realtimeEventNotificationDisablePossible: item.bucket_realtime_event_notification_disable_possible.BOOL,
              scheduledEnabled: item.bucket_scheduled_enabled.BOOL,
              scheduledStackDisablePossible: item.bucket_scheduled_stack_disable_possible.BOOL
            };
            if ("bucket_organization_id" in item) {
              bucket.organizationId = item.bucket_organization_id.S;
            }
            if ("bucket_region" in item) {
              bucket.region = item.bucket_region.S;
            }
            if ("bucket_error_message" in item) {
              bucket.errorMessage = item.bucket_error_message.S;
            }
            if ("bucket_scheduled_stack_id" in item) {
              bucket.scheduledStackId = item.bucket_scheduled_stack_id.S;
            }
            return bucket;
          }
          async function listCachedBuckets(dynamodb2, bucketCacheTableName) {
            const paginatorScan = await (0, import_client_dynamodb.paginateScan)({ client: dynamodb2 }, { TableName: bucketCacheTableName });
            const buckets = [];
            for await (const page of paginatorScan) {
              for (const item of page.Items) {
                buckets.push(mapCachedBucketItem(item));
              }
            }
            return buckets;
          }
          async function listBucketsLight(s3, region, accountId) {
            const allBuckets = [];
            const input = {};
            if (region !== void 0 && region !== null) {
              input.BucketRegion = region;
            }
            const paginator = (0, import_client_s3.paginateListBuckets)({ client: s3, pageSize: 1e3 }, input);
            for await (const page of paginator) {
              allBuckets.push(...page.Buckets.map((bucket) => {
                if ("BucketRegion" in bucket) {
                  return {
                    name: bucket.Name,
                    region: bucket.BucketRegion,
                    accountId
                  };
                } else {
                  return {
                    name: bucket.Name,
                    accountId,
                    errorMessage: `Can not get region for bucket ${bucket.Name}`
                  };
                }
              }));
            }
            if (region !== void 0 && region !== null) {
              return allBuckets.filter((bucket) => bucket.region === region || bucket.region === void 0);
            } else {
              return allBuckets;
            }
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function checkEventBridgeRules(eventbridge2, ruleNames, bucketName) {
            const rules = await Promise.all(ruleNames.map((ruleName) => eventbridge2.send(new import_client_eventbridge.DescribeRuleCommand({ Name: ruleName }))));
            return rules.filter((rule) => rule.State === "ENABLED" && rule.EventPattern).map((rule) => JSON.parse(rule.EventPattern)).find((pattern) => pattern.source.includes("aws.s3") && pattern["detail-type"].includes("Object Created") && (pattern?.detail?.bucket?.name?.includes(bucketName) || pattern?.detail?.bucket?.name === void 0)) !== void 0;
          }
          async function enrichBucket(s3, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, bucketName, bucketRegion) {
            const bucket = {
              name: bucketName,
              accountId,
              organizationId
            };
            if (bucketRegion === void 0) {
              bucket.region = void 0;
              bucket.errorMessage = `Can not get region for bucket ${bucketName}`;
            } else {
              bucket.region = bucketRegion;
            }
            if (bucket.region === region || bucket.region === void 0) {
              let realtimeEnabled = false;
              let realtimeEventNotificationEnablePossible = bucket.region !== void 0;
              let realtimeEventNotificationDisablePossible = false;
              let scheduledEnabled = false;
              let scheduledStackDisablePossible = false;
              let scheduledStackId = void 0;
              try {
                const notificationData = await s3.send(new import_client_s3.GetBucketNotificationConfigurationCommand({ Bucket: bucket.name, ExpectedBucketOwner: accountId }));
                if (notificationData?.QueueConfigurations?.find((config) => config.QueueArn === scanQueueArn && config.Events.includes("s3:ObjectCreated:*")) !== void 0) {
                  realtimeEnabled = true;
                  realtimeEventNotificationDisablePossible = true;
                }
                if (Array.isArray(notificationData?.TopicConfigurations)) {
                  await Promise.all(notificationData.TopicConfigurations.filter((config) => config.Events.includes("s3:ObjectCreated:*")).map(async (config) => {
                    const paginatorListSubscriptionsByTopic = await (0, import_client_sns.paginateListSubscriptionsByTopic)({ client: sns2, pageSize: 50 }, { TopicArn: config.TopicArn });
                    for await (const page of paginatorListSubscriptionsByTopic) {
                      if (page.Subscriptions.find((subscription) => subscription.Protocol === "sqs" && subscription.Endpoint === scanQueueArn) !== void 0) {
                        realtimeEnabled = true;
                      }
                    }
                  }));
                }
                if (notificationData?.EventBridgeConfiguration) {
                  if (accountId !== coreAccountId) {
                    let crossAccoutRule = false;
                    const paginatorListRuleNamesByTargetDefaultBus = await paginateListRuleNamesByTarget({ client: eventbridge2, pageSize: 10 }, { TargetArn: `arn:${partition}:events:${region}:${coreAccountId}:event-bus/default` });
                    for await (const page of paginatorListRuleNamesByTargetDefaultBus) {
                      if (await checkEventBridgeRules(eventbridge2, page.RuleNames, bucket.name) === true) {
                        crossAccoutRule = true;
                      }
                    }
                    const { Policy: defaultBusPolicyJson } = await coreEventbridge.send(new import_client_eventbridge.DescribeEventBusCommand({
                      Name: "default"
                    }));
                    if (defaultBusPolicyJson !== void 0) {
                      const defaultBusPolicy = JSON.parse(defaultBusPolicyJson);
                      if (defaultBusPolicy.Statement.find((s) => s?.Effect === "Allow" && s?.Action === "events:PutEvents" && (s?.Principal?.AWS === accountId || s?.Principal === "*" && s?.Condition?.StringEquals["aws:PrincipalOrgID"] === organizationId)) !== void 0) {
                        if (crossAccoutRule === true) {
                          realtimeEnabled = true;
                        }
                      }
                    }
                  }
                  const paginatorListRuleNamesByTargetScanQueue = await paginateListRuleNamesByTarget({ client: coreEventbridge, pageSize: 10 }, { TargetArn: scanQueueArn });
                  for await (const page of paginatorListRuleNamesByTargetScanQueue) {
                    if (await checkEventBridgeRules(coreEventbridge, page.RuleNames, bucket.name) === true) {
                      realtimeEnabled = true;
                    }
                  }
                }
                if (notificationData?.TopicConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.QueueConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.LambdaFunctionConfigurations?.find((config) => config.Events.filter((event) => event.startsWith("s3:ObjectCreated:")).length > 0)) {
                  realtimeEventNotificationEnablePossible = false;
                }
                if (notificationData?.EventBridgeConfiguration !== void 0) {
                  realtimeEventNotificationEnablePossible = false;
                }
              } 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, bucket.name));
              if (scheduledStacksIncludesBucket.length > 0) {
                scheduledEnabled = true;
                scheduledStackId = scheduledStacksIncludesBucket[0].id;
              }
              const scheduledStacksHasBucket = scheduledStacks.stacks.filter((scheduledStack) => hasBucket(scheduledStack, bucket.name));
              if (scheduledStacksHasBucket.length === 1) {
                scheduledEnabled = true;
                scheduledStackDisablePossible = true;
                scheduledStackId = scheduledStacksHasBucket[0].id;
              }
              return {
                ...bucket,
                realtimeEnabled,
                realtimeEventNotificationEnablePossible,
                realtimeEventNotificationDisablePossible,
                scheduledEnabled,
                scheduledStackDisablePossible,
                scheduledStackId
              };
            } else {
              return null;
            }
          }
          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 getBucket(s3, ssm, cloudformation, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, bucketName, bucketRegion) {
            const scheduledStacks = await getScheduledStacks(ssm, cloudformation, coreStackName);
            const bucket = await enrichBucket(s3, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, bucketName, bucketRegion);
            if (bucket === null) {
              throw new Error("bucket region does not match region");
            } else {
              return bucket;
            }
          }
          async function listBuckets(s3, ssm, cloudformation, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn) {
            const lightBuckets = await listBucketsLight(s3, region, accountId);
            const scheduledStacks = await getScheduledStacks(ssm, cloudformation, coreStackName);
            const buckets = await Promise.all(lightBuckets.map((lightBucket) => enrichBucket(s3, sns2, eventbridge2, coreEventbridge, partition, region, accountId, organizationId, coreAccountId, coreStackName, scanQueueArn, scheduledStacks, lightBucket.name, lightBucket.region)));
            return buckets.filter((bucket) => bucket !== null);
          }
          async function fetchS3(defaultS32, dynamodb2, bucketCache, isCrossAccount, bucketName, bucketCacheTableName) {
            if (isCrossAccount === true) {
              const cachedS3 = bucketCache.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}`);
                  bucketCache.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);
                    bucketCache.set(bucketName, s3);
                    return s3;
                  } else {
                    bucketCache.set(bucketName, defaultS32);
                    return defaultS32;
                  }
                }
              }
            } else {
              return defaultS32;
            }
          }
          function generateAccountConnectionId(partition, region, accountId) {
            return `${partition}:${region}:${accountId}`;
          }
          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");
          var TTL_IN_SECONDS = 3600;
          async function cacheBucket(dynamodb2, partition, region, accountId, { accountConnectionId, roleArn, externalId }, ttl, bucket, bucketCacheTableName) {
            const bucketCacheItem = {
              bucket_name: { S: bucket.name },
              bucket_account_id: { S: bucket.accountId },
              bucket_realtime_enabled: { BOOL: bucket.realtimeEnabled },
              bucket_realtime_event_notification_disable_possible: { BOOL: bucket.realtimeEventNotificationDisablePossible },
              bucket_realtime_event_notification_enable_possible: { BOOL: bucket.realtimeEventNotificationEnablePossible },
              bucket_scheduled_enabled: { BOOL: bucket.scheduledEnabled },
              bucket_scheduled_stack_disable_possible: { BOOL: bucket.scheduledStackDisablePossible },
              partition: { S: partition },
              region: { S: region },
              account_id: { S: accountId },
              ttl: { N: ttl.toString() }
            };
            if (bucket.organizationId !== void 0) {
              bucketCacheItem.bucket_organization_id = { S: bucket.organizationId };
            }
            if (accountConnectionId !== void 0) {
              bucketCacheItem.account_connection_id = { S: accountConnectionId };
              bucketCacheItem.role_arn = { S: roleArn };
              bucketCacheItem.external_id = { S: externalId };
            }
            if (bucket.errorMessage !== void 0) {
              bucketCacheItem.bucket_error_message = { S: bucket.errorMessage };
            }
            if (bucket.region !== void 0) {
              bucketCacheItem.bucket_region = { S: bucket.region };
            }
            if (bucket.scheduledStackId !== void 0) {
              bucketCacheItem.bucket_scheduled_stack_id = { S: bucket.scheduledStackId };
            }
            await dynamodb2.send(new import_client_dynamodb2.PutItemCommand({
              TableName: bucketCacheTableName,
              Item: bucketCacheItem
            }));
          }
          function getCredentials(accountConnectionId, roleArn, externalId) {
            if (accountConnectionId !== void 0 && roleArn !== void 0 && externalId !== void 0) {
              return (0, import_credential_providers2.fromTemporaryCredentials)({
                params: {
                  RoleArn: roleArn,
                  ExternalId: externalId,
                  RoleSessionName: "bucketav",
                  DurationSeconds: 3600
                }
              });
            } else {
              return (0, import_credential_providers2.fromNodeProviderChain)();
            }
          }
          function generateTtl() {
            return Math.floor(Date.now() / 1e3) + TTL_IN_SECONDS;
          }
          async function refreshBucket(dynamodb2, ssm, cloudformation, partition, region, accountId, coreAccountId, coreStackName, scanQueueArn, bucketName, bucketRegion, bucketCacheTableName, accountConnectionTableName) {
            const accountConnectionId = generateAccountConnectionId(partition, region, accountId);
            const accountConnectionData = await dynamodb2.send(new import_client_dynamodb2.GetItemCommand({
              TableName: accountConnectionTableName,
              Key: {
                account_connection_id: { S: accountConnectionId }
              },
              ConsistentRead: true
            }));
            let accountConnection = {};
            let credentials;
            if (accountConnectionData.Item !== void 0) {
              const roleArn = generateRoleArnFromItem(accountConnectionData.Item);
              const externalId = generateExternalIdFromItem(accountConnectionData.Item);
              credentials = getCredentials(accountConnectionId, roleArn, externalId);
              accountConnection.accountConnectionId = accountConnectionId;
              accountConnection.roleArn = roleArn;
              accountConnection.externalId = externalId;
              accountConnection.organizationId = accountConnectionData.Item.organization_id?.S;
            } else {
              accountConnection.organizationId = await fetchCachedOrganizationId(ssm, coreStackName);
              credentials = getCredentials(void 0, void 0, void 0);
            }
            const s3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01", credentials });
            const sns2 = new import_client_sns2.SNSClient({ apiVersion: "2010-03-31", credentials });
            const eventbridge2 = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07", credentials });
            const coreEventbridge = new import_client_eventbridge2.EventBridgeClient({ apiVersion: "2015-10-07" });
            const bucket = await getBucket(s3, ssm, cloudformation, sns2, eventbridge2, coreEventbridge, partition, region, accountId, accountConnection.organizationId, coreAccountId, coreStackName, scanQueueArn, bucketName, bucketRegion);
            await cacheBucket(dynamodb2, partition, region, accountId, accountConnection, generateTtl(), bucket, bucketCacheTableName);
          }

          // 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 bucketsAWS(defaultS32, defaultSsm2, defaultCloudformation2, sns2, eventbridge2, dynamodb2, sfn2, event, context, scheduledTemplateUrl, awsPartition, awsRegion, awsAccountId, crossAccount, coreStackName, scanQueueArn, refreshBucketCacheStateMachineArn, bucketCacheTableName, accountConnectionTableName, engine, fulfillment) {
            if (event.action === "enableEventNotification") {
              const s3 = await fetchS3(defaultS32, dynamodb2, /* @__PURE__ */ new Map(), crossAccount, event.bucketName, bucketCacheTableName);
              const notificationConfiguration = await s3.send(new import_client_s35.GetBucketNotificationConfigurationCommand({ Bucket: event.bucketName, ExpectedBucketOwner: event.bucketAccountId }));
              if (!("QueueConfigurations" in notificationConfiguration)) {
                notificationConfiguration.QueueConfigurations = [];
              }
              notificationConfiguration.QueueConfigurations.push({
                Id: "bucketav",
                Events: ["s3:ObjectCreated:*"],
                QueueArn: scanQueueArn
              });
              await s3.send(new import_client_s35.PutBucketNotificationConfigurationCommand({ Bucket: event.bucketName, NotificationConfiguration: notificationConfiguration, ExpectedBucketOwner: event.bucketAccountId }));
              if (crossAccount) {
                await refreshBucket(dynamodb2, defaultSsm2, defaultCloudformation2, awsPartition, event.bucketRegion, event.bucketAccountId, awsAccountId, coreStackName, scanQueueArn, event.bucketName, event.bucketRegion, bucketCacheTableName, accountConnectionTableName);
              }
            } else if (event.action === "disableEventNotification") {
              const s3 = await fetchS3(defaultS32, dynamodb2, /* @__PURE__ */ new Map(), crossAccount, event.bucketName, bucketCacheTableName);
              const notificationConfiguration = await s3.send(new import_client_s35.GetBucketNotificationConfigurationCommand({ Bucket: event.bucketName, ExpectedBucketOwner: event.bucketAccountId }));
              notificationConfiguration.QueueConfigurations = notificationConfiguration.QueueConfigurations.filter((config) => !(config.QueueArn === scanQueueArn && config.Events.includes("s3:ObjectCreated:*")));
              await s3.send(new import_client_s35.PutBucketNotificationConfigurationCommand({ Bucket: event.bucketName, NotificationConfiguration: notificationConfiguration, ExpectedBucketOwner: event.bucketAccountId }));
              if (crossAccount) {
                await refreshBucket(dynamodb2, defaultSsm2, defaultCloudformation2, awsPartition, event.bucketRegion, event.bucketAccountId, awsAccountId, coreStackName, scanQueueArn, event.bucketName, event.bucketRegion, bucketCacheTableName, accountConnectionTableName);
              }
            } else if (event.action === "refreshBucketCache" && crossAccount) {
              await sfn2.send(new import_client_sfn.StartExecutionCommand({
                stateMachineArn: refreshBucketCacheStateMachineArn
              }));
              return `<p>\u{1F504} Bucket cache refresh initiated. Please wait a minute and then <a>reload</a>.<cwdb-action action="call" endpoint="${context.invokedFunctionArn}">{}</cwdb-action></p>`;
            }
            const buckets = crossAccount ? await listCachedBuckets(dynamodb2, bucketCacheTableName) : await listBuckets(defaultS32, defaultSsm2, defaultCloudformation2, sns2, eventbridge2, eventbridge2, awsPartition, awsRegion, awsAccountId, void 0, awsAccountId, coreStackName, scanQueueArn);
            return bucketsBase(event, context, buckets, scheduledTemplateUrl, "aws", awsRegion, awsAccountId, crossAccount, coreStackName, engine, fulfillment);
          }
          async function accounts(dynamodb2, latest, awsRegion, awsAccountId, crossAccount, coreStackName, coreStackId, accountConnectionTableName, engine, fulfillment) {
            if (crossAccount) {
              const accountConnectionTemplateUrl = latest["add-on-account-connection-aws"].template;
              const paginatorScan = await (0, import_client_dynamodb3.paginateScan)({ client: dynamodb2 }, { TableName: accountConnectionTableName });
              const accounts2 = [];
              for await (const page of paginatorScan) {
                page.Items.forEach((item) => {
                  accounts2.push({
                    accountId: item.account_id.S,
                    region: item.region.S,
                    stackId: item.stack_id.S
                  });
                });
              }
              accounts2.sort((account1, account2) => account1.accountId.localeCompare(account2.accountId));
              const tbody = accounts2.map((account) => {
                let html2 = "<tr>";
                html2 += `<td>${account.accountId}</td>`;
                html2 += '<td><a class="btn">Disconnect</a>';
                html2 += '<cwdb-action display="popup">';
                html2 += "<h1>Disconnect</h1>";
                html2 += `<p>To disconnect AWS account ${account.accountId}:</p>`;
                html2 += deleteInstructions(awsAccountId, account.region, account.accountId, account.stackId);
                html2 += "</cwdb-action>";
                html2 += "</td>";
                html2 += "</tr>";
                return html2;
              }).join("");
              let html = '<table style="width:100%">';
              html += "<thead>";
              html += "<tr>";
              html += "<th>AWS account</th>";
              html += "<th>&nbsp;</th>";
              html += "</tr>";
              html += "</thead>";
              html += "<tbody>";
              html += "<tr>";
              html += `<td>${awsAccountId}</td>`;
              html += "<td>Runs bucketAV, always connected;</td>";
              html += "</tr>";
              html += tbody;
              html += "</tbody>";
              html += "<tfoot>";
              html += "<tr>";
              html += "<th>&nbsp;</th>";
              html += '<th><a class="btn btn-primary">Connect AWS account</a>';
              html += '<cwdb-action display="popup">';
              html += "<h1>Connect</h1>";
              html += "<p>Connecting a new AWS account with bucketAV is a two-step process:</p>";
              html += "<ol>";
              html += "<li>Configure bucketAV to trust the new AWS account.</li>";
              html += "<li>Connect the new AWS account to bucketAV.</li>";
              html += "</ol>";
              html += "<h2>Trust AWS account</h2>";
              html += "<p>To trust the new AWS account, in this AWS account:</p>";
              html += "<ol>";
              html += updateInstructions(awsAccountId, awsRegion, awsAccountId, coreStackId, void 0, 'Modify the <strong>AWSOrganizationRestriction</strong> or <strong>AWSAccountRestriction</strong> parameter. <a href="https://bucketav.com/help/operations/multi-account-setup.html#prepare-bucketav" target="_blank">Learn more</a>!</li>');
              html += "</ol>";
              html += "<h2>Connect AWS account</h2>";
              html += `<p><small>If you plan to connect many AWS accounts we recommend to use AWS CloudFormation StackSets. <a href="https://bucketav.com/help/operations/multi-account-setup.html?platform=aws&engine=${engine}&fulfillment=${fulfillment}#connect-iam-stacksets" target="_blank">Learn more</a>!</small></p>`;
              html += "<p>To connect the new AWS account:</p>";
              html += createInstructions(awsAccountId, awsRegion, void 0, accountConnectionTemplateUrl, "bucketav-account-connection", { BucketAVStackName: coreStackName, BucketAVAccountId: awsAccountId });
              html += "</cwdb-action>";
              html += "</th>";
              html += "</tr>";
              html += "</tfoot>";
              html += "</table>";
              return html;
            } else {
              return `You can use bucketAV with multiple AWS accounts. <a href="https://bucketav.com/help/operations/multi-account-setup.html?platform=aws&engine=${engine}&fulfillment=${fulfillment}" target="_blank">Learn more</a>!`;
            }
          }
          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-aws.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 sns = new import_client_sns3.SNSClient({ apiVersion: "2010-03-31" });
          var eventbridge = new import_client_eventbridge3.EventBridgeClient({ apiVersion: "2015-10-07" });
          var sfn = new import_client_sfn2.SFNClient({ apiVersion: "2016-11-23" });
          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 SCAN_QUEUE_ARN = process.env.SCAN_QUEUE_ARN;
          var CROSS_ACCOUNT = process.env.CROSS_ACCOUNT === "true";
          var MULTI_DEPLOYMENT = process.env.MULTI_DEPLOYMENT === "true";
          var REFRESH_BUCKET_CACHE_STATE_MACHINE_ARN = process.env.REFRESH_BUCKET_CACHE_STATE_MACHINE_ARN;
          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 accountConnectionTableName = `${CORE_STACK_NAME}-AccountConnection`;
            const latest = await fetchLatest();
            const scheduledTemplateUrl = latest["add-on-scheduled-bucket-scan-aws"].template;
            if (event.widgetContext.params.view === "update") {
              if (MULTI_DEPLOYMENT === false) {
                return await update(defaultCloudformation, defaultSsm, dynamodb, "aws", latest, CROSS_ACCOUNT, AWS_PARTITION, AWS_REGION, AWS_ACCOUNT_ID, CORE_STACK_NAME, CORE_STACK_ID, ENGINE, FULFILLMENT, accountConnectionTableName);
              } else {
                return NA_MULTI;
              }
            } else if (event.widgetContext.params.view === "buckets") {
              if (MULTI_DEPLOYMENT === false) {
                return await bucketsAWS(defaultS3, defaultSsm, defaultCloudformation, sns, eventbridge, dynamodb, sfn, event, context, scheduledTemplateUrl, AWS_PARTITION, AWS_REGION, AWS_ACCOUNT_ID, CROSS_ACCOUNT, CORE_STACK_NAME, SCAN_QUEUE_ARN, REFRESH_BUCKET_CACHE_STATE_MACHINE_ARN, `${CORE_STACK_NAME}-BucketCache`, accountConnectionTableName, ENGINE, FULFILLMENT);
              } else {
                return NA_MULTI;
              }
            } else if (event.widgetContext.params.view === "accounts") {
              if (MULTI_DEPLOYMENT === false) {
                return await accounts(dynamodb, latest, AWS_REGION, AWS_ACCOUNT_ID, CROSS_ACCOUNT, CORE_STACK_NAME, CORE_STACK_ID, accountConnectionTableName, 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, "aws", 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-public-vpc
          AWS_PARTITION:
            Ref: AWS::Partition
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          SCAN_LOG_GROUP_NAME:
            Ref: Logs
          SCAN_QUEUE_ARN:
            Fn::GetAtt:
              - ScanQueue
              - Arn
          CROSS_ACCOUNT:
            Fn::If:
              - HasCrossAccount
              - "true"
              - "false"
          MULTI_DEPLOYMENT: "false"
          SCAN_LOG_STREAM_SUFFIX: ""
          REFRESH_BUCKET_CACHE_STATE_MACHINE_ARN:
            Fn::If:
              - HasCrossAccount
              - Fn::GetAtt:
                  - RefreshBucketCacheStateMachine
                  - Arn
              - Ref: AWS::NoValue
      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":"custom","x":16,"y":16,"width":8,"height":8,"properties":{"endpoint":"'
            - Fn::GetAtt:
                - DashboardLambdaFunction
                - Arn
            - '","updateOn":{"refresh":true,"resize":false,"timeRange":false},"params":{"view":"accounts"},"title":"AWS accounts"}},{"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 Amazon S3 powered by multiple engines (dedicated-public-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.3
  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-public-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
  ServiceDiscoveryCrossAccount:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: AWS::StackName
            - /CrossAccount
      Type: String
      Value:
        Fn::If:
          - HasCrossAccount
          - "true"
          - "false"
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
  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
  AccountConnectionLambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - AccountConnectionLambdaDeadLetterQueue
        - QueueName
    Condition: HasCrossAccount
  RefreshServiceDiscoveryLambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - RefreshServiceDiscoveryLambdaDeadLetterQueue
        - QueueName
    Condition: HasCrossAccount
  GovernanceLambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - GovernanceLambdaDeadLetterQueue
        - QueueName
    Condition: HasGovernance
  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-public-vpc
  Version:
    Description: bucketAV version.
    Value: 3.0.3
  Platform:
    Description: bucketAV platform.
    Value: aws
  Engine:
    Description: bucketAV engine.
    Value: multi
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

