---
# 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 scheduled bucket scan Add-On for Amazon S3
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - BucketAVStackName
          - BucketName
      - Label:
          default: Schedule Parameters
        Parameters:
          - ScheduleExpression
          - LogsRetentionInDays
          - BucketConcurrency
          - PagingBatchSize
          - PagingWaitInSeconds
          - TimeoutInSeconds
          - ExcludeBucketNameFilter
          - ExcludeScannedObjects
          - ScanIntelligentTieringStorageClass
          - ObjectPrefixFilter
      - Label:
          default: Reporting Parameters
        Parameters:
          - ReportingAddOnStackName
          - WaitForScanToFinishTimeoutInSeconds
      - Label:
          default: Permissions Parameters
        Parameters:
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - RunFunctionReservedConcurrentExecutions
          - OrchestratorFunctionReservedConcurrentExecutions
          - StateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - BucketArnGeneratorFunctionReservedConcurrentExecutions
Parameters:
  BucketAVStackName:
    Type: String
    Description: CloudFormation stack name of bucketAV (if you followed our docs, the name is bucketav, or s3-virusscan for older installations)
  BucketName:
    Type: String
    AllowedPattern: "[a-z0-9\\.\\-\\*\\,]{1,}"
    Description: Name or comma-separated names of S3 buckets that are scanned at regular intervals. Wildcard character * is supported.
  ScheduleExpression:
    Type: String
    Default: rate(1 day)
    Description: "Scan bucket daily (find details here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html)."
  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.
  BucketConcurrency:
    Type: Number
    Default: 1
    Description: How many buckets should be scanned in parallel?
    MaxValue: 25
    MinValue: 1
  PagingBatchSize:
    Type: Number
    Default: 50
    Description: The number of objects or object versions that are fetched at once (a higher number increases the likelihood of a growing scan queue).
    MaxValue: 100000
    MinValue: 10
  PagingWaitInSeconds:
    Type: Number
    Default: 30
    Description: Wait time in seconds before moving to the next page while paging trough S3 objects (a higher number reduces the likelihood of a growing scan queue).
    MaxValue: 600
    MinValue: 0
  TimeoutInSeconds:
    Type: Number
    Default: 43200
    Description: Maximum number of seconds while paging through S3 objects of a single bucket (keep the timeout below the time between two scans! For example, if you perform a full scan every day, keep the value below 24 hours = 86400 seconds).
    MaxValue: 31536000
    MinValue: 600
  ExcludeBucketNameFilter:
    Type: String
    Default: ""
    AllowedPattern: "[a-z0-9\\.\\-\\*\\,]{0,}"
    Description: Optional name or comma-separated names of S3 buckets that should be excluded from scanning. Wildcard character * is supported.
  ObjectPrefixFilter:
    Type: String
    Default: ""
    Description: Optional prefix all object keys must start with to be scanned.
  ExcludeScannedObjects:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Exclude already scanned files.
  ScanIntelligentTieringStorageClass:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: "Scan objects with the INTELLIGENT_TIERING storage class. Warning: objects archived by Intelligent-Tiering need to be restored before they can be scanned, which may cause scan jobs to fail."
  ReportingAddOnStackName:
    Type: String
    Default: ""
    Description: Optional CloudFormation stack name of the bucketAV reporting add-on (if you followed our docs, the name is bucketav-reporting)
  WaitForScanToFinishTimeoutInSeconds:
    Type: Number
    Default: 43200
    Description: Maximum number of seconds while waiting for the scan to finish before a report is generated.
    MaxValue: 604800
    MinValue: 600
  PermissionsBoundary:
    Type: String
    Default: ""
    Description: Optional IAM policy ARN that will be used as the permissions boundary for all roles.
  RunFunctionReservedConcurrentExecutions:
    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
  OrchestratorFunctionReservedConcurrentExecutions:
    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
  LambdaSubnets:
    Type: CommaDelimitedList
    Default: ""
    Description: Optionally configure Lambda functions to run in theses subnets, requires route to NAT Gateway or VPC Endpoints.
  LambdaVpc:
    Type: String
    Default: ""
    Description: Optionally configure Lambda functions to run in this VPC, requires route to NAT Gateway or VPC Endpoints.
  BucketArnGeneratorFunctionReservedConcurrentExecutions:
    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
Conditions:
  HasLambdaVpc:
    Fn::And:
      - Fn::Not:
          - Fn::Equals:
              - Fn::Join:
                  - ""
                  - Ref: LambdaSubnets
              - ""
      - Fn::Not:
          - Fn::Equals:
              - Ref: LambdaVpc
              - ""
  HasExcludeScannedObjects:
    Fn::Equals:
      - Ref: ExcludeScannedObjects
      - "true"
  HasPagingWaitInSeconds:
    Fn::Not:
      - Fn::Equals:
          - Ref: PagingWaitInSeconds
          - 0
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasScheduleExpression:
    Fn::Not:
      - Fn::Equals:
          - Ref: ScheduleExpression
          - ""
  HasExcludeBucketNameFilter:
    Fn::Not:
      - Fn::Equals:
          - Ref: ExcludeBucketNameFilter
          - ""
  HasExcludeScannedObjectsAndExcludeBucketNameFilter:
    Fn::And:
      - Condition: HasExcludeScannedObjects
      - Condition: HasExcludeBucketNameFilter
  HasReportingAddOnStackName:
    Fn::Not:
      - Fn::Equals:
          - Ref: ReportingAddOnStackName
          - ""
  HasRunFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: RunFunctionReservedConcurrentExecutions
          - 0
  HasOrchestratorFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: OrchestratorFunctionReservedConcurrentExecutions
          - 0
  HasBucketArnGeneratorFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: BucketArnGeneratorFunctionReservedConcurrentExecutions
          - 0
  HasStateMachineNameGeneratorFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: StateMachineNameGeneratorFunctionReservedConcurrentExecutions
          - 0
Resources:
  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -lambda
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow outgoing HTTPS traffic.
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      VpcId:
        Ref: LambdaVpc
    Condition: HasLambdaVpc
  BucketArnGeneratorRole:
    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
  BucketArnGeneratorFunction:
    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/add-on-scheduled-bucket-scan-bucket-arn-generator.js
          var add_on_scheduled_bucket_scan_bucket_arn_generator_exports = {};
          __export(add_on_scheduled_bucket_scan_bucket_arn_generator_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_scheduled_bucket_scan_bucket_arn_generator_exports);
          var BUCKET_NAME = process.env.BUCKET_NAME;
          var EXCLUDE_BUCKET_NAME_FILTER = process.env.EXCLUDE_BUCKET_NAME_FILTER;
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const allowBucketArns = BUCKET_NAME.split(",").map((b) => "arn:aws:s3:::" + b);
            const denyBucketArns = EXCLUDE_BUCKET_NAME_FILTER.split(",").map((b) => "arn:aws:s3:::" + b);
            const response = await fetch(event.ResponseURL, {
              method: "PUT",
              headers: {
                "Content-Type": "application/json"
              },
              body: JSON.stringify({
                Status: "SUCCESS",
                PhysicalResourceId: "BucketArnGenerator",
                StackId: event.StackId,
                RequestId: event.RequestId,
                LogicalResourceId: event.LogicalResourceId,
                NoEcho: false,
                Data: {
                  AllowBucketArns: allowBucketArns,
                  AllowObjectArns: allowBucketArns.map((bucketArn) => bucketArn + "/*"),
                  DenyBucketArns: denyBucketArns,
                  DenyObjectArns: denyBucketArns.map((bucketArn) => bucketArn + "/*")
                }
              })
            });
            if (response.status !== 200) {
              console.log("response status", response.status);
              console.log("response", await response.text());
              throw new Error("unexpected status code");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          BUCKET_NAME:
            Ref: BucketName
          EXCLUDE_BUCKET_NAME_FILTER:
            Ref: ExcludeBucketNameFilter
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasBucketArnGeneratorFunctionReservedConcurrentExecutions
          - Ref: BucketArnGeneratorFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - BucketArnGeneratorRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 30
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::BucketArnGeneratorLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: BucketArnGeneratorLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  BucketArnGeneratorLambdaVpcAllowPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
            Resource:
              - "*"
      PolicyName: vpc-allow
      Roles:
        - Ref: BucketArnGeneratorRole
    Condition: HasLambdaVpc
  BucketArnGeneratorLambdaVpcDenyPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Deny
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
            Resource: "*"
            Condition:
              ArnEquals:
                lambda:SourceFunctionArn:
                  - Fn::GetAtt:
                      - BucketArnGeneratorFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: BucketArnGeneratorRole
    Condition: HasLambdaVpc
  BucketArnGeneratorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: BucketArnGeneratorFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  BucketArnGeneratorPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - BucketArnGeneratorLogGroup
                - Arn
      PolicyName: lambda
      Roles:
        - Ref: BucketArnGeneratorRole
  BucketArnGenerator:
    Type: Custom::BucketArnGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - BucketArnGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Version: 2.10.3
      BucketName:
        Ref: BucketName
      ExcludeBucketNameFilter:
        Ref: ExcludeBucketNameFilter
    DependsOn:
      - BucketArnGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  RunRole:
    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:ListBucketVersions
                Resource:
                  Fn::GetAtt:
                    - BucketArnGenerator
                    - AllowBucketArns
              - Fn::If:
                  - HasExcludeBucketNameFilter
                  - Effect: Deny
                    Action: s3:ListBucketVersions
                    Resource:
                      Fn::GetAtt:
                        - BucketArnGenerator
                        - DenyBucketArns
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasExcludeScannedObjects
                  - Effect: Allow
                    Action:
                      - s3:GetObjectTagging
                      - s3:GetObjectVersionTagging
                    Resource:
                      Fn::GetAtt:
                        - BucketArnGenerator
                        - AllowObjectArns
                  - Ref: AWS::NoValue
              - Fn::If:
                  - HasExcludeScannedObjectsAndExcludeBucketNameFilter
                  - Effect: Deny
                    Action:
                      - s3:GetObjectTagging
                      - s3:GetObjectVersionTagging
                    Resource:
                      Fn::GetAtt:
                        - BucketArnGenerator
                        - DenyObjectArns
                  - Ref: AWS::NoValue
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::ImportValue:
                    Fn::Join:
                      - ""
                      - - Ref: BucketAVStackName
                        - -ScanQueueArn
              - Effect: Allow
                Action: sts:AssumeRole
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - :iam::*:role/
                      - Ref: BucketAVStackName
                      - -AccountConnection
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter/bucketAV/
                      - Ref: BucketAVStackName
                      - /CrossAccount
              - Effect: Allow
                Action: dynamodb:GetItem
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":dynamodb:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :table/
                      - Ref: BucketAVStackName
                      - -BucketCache
          PolicyName: run
  RunFunction:
    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/add-on-scheduled-bucket-scan-run.js
          var add_on_scheduled_bucket_scan_run_exports = {};
          __export(add_on_scheduled_bucket_scan_run_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_scheduled_bucket_scan_run_exports);
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_client_sqs = require("@aws-sdk/client-sqs");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_secrets_manager2 = require("@aws-sdk/client-secrets-manager");

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

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

          // lambda/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/add-on-scheduled-bucket-scan-run.js
          var CORE_PLATFORM = process.env.CORE_PLATFORM;
          var STORAGE_CLASSES = /* @__PURE__ */ new Set(["GLACIER_IR", "ONEZONE_IA", "REDUCED_REDUNDANCY", "STANDARD_IA", "STANDARD"]);
          if (process.env.SCAN_INTELLIGENT_TIERING_STORAGE_CLASS === "true") {
            STORAGE_CLASSES.add("INTELLIGENT_TIERING");
          }
          var SCAN_QUEUE_URL = process.env.SCAN_QUEUE_URL;
          var PAGING_BATCH_SIZE = parseInt(process.env.PAGING_BATCH_SIZE, 10);
          var OBJECT_PREFIX_FILTER = process.env.OBJECT_PREFIX_FILTER;
          var EXCLUDE_SCANNED_OBJECTS = process.env.EXCLUDE_SCANNED_OBJECTS === "true";
          var S3_CONCURRENCY = 10;
          var SQS_CONCURRENCY = 10;
          var S3_BATCH_SIZE = 1e3;
          var SQS_BATCH_SIZE = 10;
          var S3_PAGING_BATCH_SIZE = Math.min(PAGING_BATCH_SIZE, S3_BATCH_SIZE);
          var defaultS3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          var sqs = new import_client_sqs.SQSClient({ apiVersion: "2012-11-05" });
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var dynamodb = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
          var secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var bucketCache = /* @__PURE__ */ new Map();
          var bucketCacheRefreshedAt = 0;
          var isCrossAccountPromise = void 0;
          async function enqueue(bucket, traceId, items) {
            const batches = [];
            for (let i = 0; i < items.length; i += SQS_BATCH_SIZE) {
              batches.push(items.slice(i, i + SQS_BATCH_SIZE));
            }
            const iterator = batches.entries();
            const doWork = async (iterator2) => {
              for (const [, batch] of iterator2) {
                const data = await sqs.send(new import_client_sqs.SendMessageBatchCommand({
                  QueueUrl: SCAN_QUEUE_URL,
                  Entries: batch.map((item, i) => ({
                    Id: i.toString(),
                    MessageBody: JSON.stringify({
                      objects: [{
                        bucket,
                        key: encodeURIComponent(item.Key),
                        ..."VersionId" in item && { version: item.VersionId },
                        trace_id: traceId
                      }]
                    }),
                    MessageGroupId: `bucketav:scheduled-bucket-scan:${process.env.ADDON_STACK_NAME.substring(0, 97)}`
                    // max 128 chars
                  }))
                }));
                if (data.Failed && data.Failed.length > 0) {
                  throw new Error(`SQS errors: ${data.Failed.map((failure) => `#${failure.Id}: ${failure.Message}`).join(", ")}`);
                }
              }
            };
            const workers = Array(Math.min(batches.length, SQS_CONCURRENCY)).fill(iterator).map(doWork);
            await Promise.allSettled(workers);
          }
          async function pageVersions(s3, event, maxKeys, nextKeyMarker, nextVersionIdMarker) {
            const params = {
              Bucket: event.bucket,
              MaxKeys: maxKeys
            };
            if (nextKeyMarker !== void 0 && nextVersionIdMarker !== void 0) {
              params.KeyMarker = nextKeyMarker;
              params.VersionIdMarker = nextVersionIdMarker;
            }
            if (OBJECT_PREFIX_FILTER) {
              params.Prefix = OBJECT_PREFIX_FILTER;
            }
            const data = await s3.send(new import_client_s33.ListObjectVersionsCommand(params));
            if (!data.Versions) {
              data.Versions = [];
            }
            if (!data.DeleteMarkers) {
              data.DeleteMarkers = [];
            }
            if (data.Versions.length + data.DeleteMarkers.length < maxKeys) {
              console.log(`Fetched ${data.Versions.length} versions and ${data.DeleteMarkers.length} delete markers. Asked for ${maxKeys}.`);
            }
            data.Versions = data.Versions.filter((version) => STORAGE_CLASSES.has(version.StorageClass));
            if (EXCLUDE_SCANNED_OBJECTS) {
              const iterator = data.Versions.entries();
              const scanned = Array(data.Versions.length);
              const doWork = async (iterator2) => {
                for (const [i, version] of iterator2) {
                  const data2 = await s3.send(new import_client_s33.GetObjectTaggingCommand({
                    Bucket: event.bucket,
                    Key: version.Key,
                    VersionId: version.VersionId
                  }));
                  scanned[i] = data2?.TagSet?.find((tagset) => tagset.Key === event.tagKey) !== void 0;
                }
              };
              const workers = Array(Math.min(data.Versions.length, S3_CONCURRENCY)).fill(iterator).map(doWork);
              await Promise.allSettled(workers);
              const unscannedVersions = data.Versions.filter((version, i) => scanned[i] === false);
              data.Versions = unscannedVersions;
            }
            await enqueue(event.bucket, event.traceId, data.Versions);
            if (data.IsTruncated === true) {
              return { eof: false, nextKeyMarker: data.NextKeyMarker, nextVersionIdMarker: data.NextVersionIdMarker };
            } else {
              return { eof: true };
            }
          }
          async function pageObjects(s3, event, maxKeys, nextContinuationToken) {
            const params = {
              Bucket: event.bucket,
              MaxKeys: maxKeys
            };
            if (nextContinuationToken !== void 0) {
              params.ContinuationToken = nextContinuationToken;
            }
            if (OBJECT_PREFIX_FILTER) {
              params.Prefix = OBJECT_PREFIX_FILTER;
            }
            const data = await s3.send(new import_client_s33.ListObjectsV2Command(params));
            if (data.KeyCount > 0) {
              await enqueue(event.bucket, event.traceId, data.Contents.filter((content) => STORAGE_CLASSES.has(content.StorageClass)));
            }
            if (data.IsTruncated === true) {
              return { eof: false, nextContinuationToken: data.nextContinuationToken };
            } else {
              return { eof: true };
            }
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            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 s3 = CORE_PLATFORM === "aws" ? await fetchS3(defaultS3, dynamodb, bucketCache, await isCrossAccountPromise, event.bucket, `${process.env.CORE_STACK_NAME}-BucketCache`) : await fetchR2(ssm, secretsmanager, process.env.CORE_STACK_NAME);
            let out = event;
            let keys = 0;
            while (true) {
              const maxKeys = Math.min(S3_PAGING_BATCH_SIZE, PAGING_BATCH_SIZE - keys);
              keys += maxKeys;
              if (CORE_PLATFORM === "aws") {
                out = await pageVersions(s3, event, maxKeys, out.nextKeyMarker, out.nextVersionIdMarker);
              } else if (CORE_PLATFORM === "cloudflare") {
                out = await pageObjects(s3, event, maxKeys, out.nextContinuationToken);
              } else {
                throw new Error("unsupported platform");
              }
              if (keys === PAGING_BATCH_SIZE || out.eof === true) {
                break;
              }
            }
            return Object.assign({}, event, out);
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          SCAN_QUEUE_URL:
            Fn::ImportValue:
              Fn::Join:
                - ""
                - - Ref: BucketAVStackName
                  - -ScanQueueUrl
          PAGING_BATCH_SIZE:
            Ref: PagingBatchSize
          OBJECT_PREFIX_FILTER:
            Ref: ObjectPrefixFilter
          EXCLUDE_SCANNED_OBJECTS:
            Ref: ExcludeScannedObjects
          SCAN_INTELLIGENT_TIERING_STORAGE_CLASS:
            Ref: ScanIntelligentTieringStorageClass
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          CORE_PLATFORM: aws
          ADDON_STACK_NAME:
            Ref: AWS::StackName
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasRunFunctionReservedConcurrentExecutions
          - Ref: RunFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - RunRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 900
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::RunLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: RunLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  RunLambdaVpcAllowPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
            Resource:
              - "*"
      PolicyName: vpc-allow
      Roles:
        - Ref: RunRole
    Condition: HasLambdaVpc
  RunLambdaVpcDenyPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Deny
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
            Resource: "*"
            Condition:
              ArnEquals:
                lambda:SourceFunctionArn:
                  - Fn::GetAtt:
                      - RunFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: RunRole
    Condition: HasLambdaVpc
  RunLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: RunFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  RunPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - RunLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: RunRole
  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
  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:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::StateMachineNameGeneratorFunctionLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: StateMachineNameGeneratorFunctionLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  StateMachineNameGeneratorFunctionLambdaVpcAllowPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
            Resource:
              - "*"
      PolicyName: vpc-allow
      Roles:
        - Ref: StateMachineNameGeneratorRole
    Condition: HasLambdaVpc
  StateMachineNameGeneratorFunctionLambdaVpcDenyPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Deny
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
            Resource: "*"
            Condition:
              ArnEquals:
                lambda:SourceFunctionArn:
                  - Fn::GetAtt:
                      - StateMachineNameGeneratorFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: StateMachineNameGeneratorRole
    Condition: HasLambdaVpc
  StateMachineNameGeneratorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: StateMachineNameGeneratorFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  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
  StateMachineNameGenerator:
    Type: Custom::StateMachineNameGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - StateMachineNameGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Suffix: ""
    DependsOn:
      - StateMachineNameGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/vendedlogs/states/
            - Ref: AWS::StackName
            - -StateMachine
      RetentionInDays:
        Ref: LogsRetentionInDays
  StateMachineRole:
    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:
                    - RunFunction
                    - 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:
                    - StateMachineLogGroup
                    - Arn
          PolicyName: states
  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: "bucketAV add-on (scheduled bucket scan): Enqueues all objects/versions of a single bucket."
        TimeoutSeconds:
          Ref: TimeoutInSeconds
        StartAt: Run
        Version: "1.0"
        States:
          Run:
            Type: Task
            Resource:
              Fn::GetAtt:
                - RunFunction
                - Arn
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Next: CheckEndOfPage
          CheckEndOfPage:
            Type: Choice
            Choices:
              - Variable: $.eof
                BooleanEquals: true
                Next: Done
            Default:
              Fn::If:
                - HasPagingWaitInSeconds
                - Wait
                - Run
          Wait:
            Fn::If:
              - HasPagingWaitInSeconds
              - Type: Wait
                Seconds:
                  Ref: PagingWaitInSeconds
                Next: Run
              - Ref: AWS::NoValue
          Done:
            Type: Succeed
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn:
                Fn::GetAtt:
                  - StateMachineLogGroup
                  - Arn
        IncludeExecutionData: true
        Level: ERROR
      RoleArn:
        Fn::GetAtt:
          - StateMachineRole
          - Arn
      StateMachineName:
        Ref: StateMachineNameGenerator
    DependsOn:
      - RunPolicy
  StateMachineNameGeneratorWaitForScanToFinish:
    Type: Custom::StateMachineNameGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - StateMachineNameGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Suffix: -waiter
    DependsOn:
      - StateMachineNameGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  WaitForScanToFinishStateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/vendedlogs/states/
            - Ref: AWS::StackName
            - -WaitForScanToFinishStateMachine
      RetentionInDays:
        Ref: LogsRetentionInDays
    Condition: HasReportingAddOnStackName
  WaitForScanToFinishStateMachineRole:
    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: sqs:GetQueueAttributes
                Resource:
                  Fn::ImportValue:
                    Fn::Join:
                      - ""
                      - - Ref: BucketAVStackName
                        - -ScanQueueArn
              - 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:
                    - WaitForScanToFinishStateMachineLogGroup
                    - Arn
          PolicyName: states
    Condition: HasReportingAddOnStackName
  WaitForScanToFinishStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: "bucketAV add-on (scheduled bucket scan): Wait for scan to finish"
        TimeoutSeconds:
          Ref: WaitForScanToFinishTimeoutInSeconds
        StartAt: Wait
        Version: "1.0"
        States:
          Fetch:
            Type: Task
            Resource: arn:aws:states:::aws-sdk:sqs:getQueueAttributes
            Next: Check
            ResultPath: $.scanQueueLength
            ResultSelector:
              total.$: States.MathAdd(States.StringToJson($.Attributes.ApproximateNumberOfMessages), States.MathAdd(States.StringToJson($.Attributes.ApproximateNumberOfMessagesDelayed), States.StringToJson($.Attributes.ApproximateNumberOfMessagesNotVisible)))
            Parameters:
              AttributeNames:
                - ApproximateNumberOfMessages
                - ApproximateNumberOfMessagesDelayed
                - ApproximateNumberOfMessagesNotVisible
              QueueUrl:
                Fn::ImportValue:
                  Fn::Join:
                    - ""
                    - - Ref: BucketAVStackName
                      - -ScanQueueUrl
          Check:
            Type: Choice
            Choices:
              - Variable: $.scanQueueLength.total
                NumericGreaterThanPath: $.scanQueueLengthBeforeScan
                Next: Wait
            Default: Done
          Wait:
            Type: Wait
            Seconds: 60
            Next: Fetch
          Done:
            Type: Succeed
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn:
                Fn::GetAtt:
                  - WaitForScanToFinishStateMachineLogGroup
                  - Arn
        IncludeExecutionData: true
        Level: ERROR
      RoleArn:
        Fn::GetAtt:
          - WaitForScanToFinishStateMachineRole
          - Arn
      StateMachineName:
        Ref: StateMachineNameGeneratorWaitForScanToFinish
    Condition: HasReportingAddOnStackName
  OrchestratorRole:
    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
                Resource: "*"
              - Effect: Allow
                Action: ssm:GetParameter
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter/bucketAV/
                      - Ref: BucketAVStackName
                      - /CrossAccount
              - Effect: Allow
                Action: dynamodb:Scan
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":dynamodb:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :table/
                      - Ref: BucketAVStackName
                      - -BucketCache
              - Effect: Allow
                Action: cloudformation:DescribeStacks
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":cloudformation:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :stack/
                      - Ref: BucketAVStackName
                      - /*
          PolicyName: core
  OrchestratorFunction:
    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/add-on-scheduled-bucket-scan-orchestrator.js
          var add_on_scheduled_bucket_scan_orchestrator_exports = {};
          __export(add_on_scheduled_bucket_scan_orchestrator_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_scheduled_bucket_scan_orchestrator_exports);
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_client_cloudformation2 = require("@aws-sdk/client-cloudformation");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_secrets_manager2 = require("@aws-sdk/client-secrets-manager");

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

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

          // lambda/lib.js
          async function listCachedBucketsLight(dynamodb, bucketCacheTableName) {
            const paginatorScan = await (0, import_client_dynamodb.paginateScan)({ client: dynamodb }, { TableName: bucketCacheTableName });
            const buckets = [];
            for await (const page of paginatorScan) {
              for (const item of page.Items) {
                const bucket = {
                  name: item.bucket_name.S,
                  accountId: item.bucket_account_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;
                }
                buckets.push(bucket);
              }
            }
            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;
            }
          }
          async function listR2BucketsLight(s3) {
            const { Buckets: buckets } = await s3.send(new import_client_s3.ListBucketsCommand({}));
            return buckets.map((bucket) => ({
              name: bucket.Name
            }));
          }
          var paginateListRuleNamesByTarget = createPaginator(import_client_eventbridge.EventBridgeClient, import_client_eventbridge.ListRuleNamesByTargetCommand, "NextToken", "NextToken", "Limit");
          async function fetchCrossAccount(ssm, coreStackName) {
            try {
              const data = await ssm.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 fetchR2(ssm, secretsmanager, coreStackName) {
            const { Parameter: { Value: cloudflareR2EndpointHostname } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareR2EndpointHostname` }));
            const { Parameter: { Value: cloudflareAccessKeyId } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeyId` }));
            const { Parameter: { Value: cloudflareAccessKeySecretArn } } = await ssm.send(new import_client_ssm.GetParameterCommand({ Name: `/bucketAV/${coreStackName}/CloudflareAccessKeySecretArn` }));
            const { SecretString: cloudflareAccessKey } = await secretsmanager.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/add-on-scheduled-bucket-scan-orchestrator.js
          var BUCKET_NAME = process.env.BUCKET_NAME;
          var CORE_PLATFORM = process.env.CORE_PLATFORM;
          var CORE_STACK_NAME = process.env.CORE_STACK_NAME;
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const result = {};
            const ssm = new import_client_ssm2.SSMClient({ apiVersion: "2018-10-26" });
            if (CORE_PLATFORM === "aws") {
              const s3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
              const cloudformation = new import_client_cloudformation2.CloudFormationClient({ apiVersion: "2010-05-15" });
              const dynamodb = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
              const data = await cloudformation.send(new import_client_cloudformation2.DescribeStacksCommand({
                StackName: CORE_STACK_NAME
              }));
              const tagKey = data?.Stacks[0]?.Parameters?.find((p) => p.ParameterKey === "TagKey")?.ParameterValue;
              if (tagKey === void 0) {
                throw new Error("tagKey not defined");
              }
              result.tagKey = tagKey;
              const isCrossAccount = CORE_PLATFORM === "aws" ? await fetchCrossAccount(ssm, CORE_STACK_NAME) : false;
              if (BUCKET_NAME.includes("*")) {
                const buckets = await (isCrossAccount === true ? listCachedBucketsLight(dynamodb, `${CORE_STACK_NAME}-BucketCache`) : listBucketsLight(s3, process.env.AWS_REGION, process.env.AWS_ACCOUNT_ID));
                const filterExpression = "^" + BUCKET_NAME.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
                result.buckets = buckets.filter((bucket) => bucket.name.match(new RegExp(filterExpression))).map((bucket) => bucket.name);
              } else {
                result.buckets = BUCKET_NAME.split(",");
              }
              const excludeFilterExpression = "^" + process.env.EXCLUDE_BUCKET_NAME_FILTER.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
              result.buckets = result.buckets.filter((bucketName) => !bucketName.match(new RegExp(excludeFilterExpression)));
              return result;
            } else if (CORE_PLATFORM === "cloudflare") {
              const secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
              const s3 = await fetchR2(ssm, secretsmanager, CORE_STACK_NAME);
              result.tagKey = "";
              if (BUCKET_NAME.includes("*")) {
                const buckets = await listR2BucketsLight(s3);
                const filterExpression = "^" + BUCKET_NAME.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
                result.buckets = buckets.filter((bucket) => bucket.name.match(new RegExp(filterExpression))).map((bucket) => bucket.name);
              } else {
                result.buckets = BUCKET_NAME.split(",");
              }
              const excludeFilterExpression = "^" + process.env.EXCLUDE_BUCKET_NAME_FILTER.replaceAll(".", ".").replaceAll("-", "-").replaceAll("*", ".*").replaceAll(",", "$|^") + "$";
              result.buckets = result.buckets.filter((bucketName) => !bucketName.match(new RegExp(excludeFilterExpression)));
              return result;
            } else {
              throw new Error("unsupported platform");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          CORE_PLATFORM: aws
          BUCKET_NAME:
            Ref: BucketName
          EXCLUDE_BUCKET_NAME_FILTER:
            Ref: ExcludeBucketNameFilter
          AWS_ACCOUNT_ID:
            Ref: AWS::AccountId
          SCAN_QUEUE_ARN:
            Fn::ImportValue:
              Fn::Join:
                - ""
                - - Ref: BucketAVStackName
                  - -ScanQueueArn
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasOrchestratorFunctionReservedConcurrentExecutions
          - Ref: OrchestratorFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - OrchestratorRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 900
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::OrchestratorLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: OrchestratorLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  OrchestratorLambdaVpcAllowPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
            Resource:
              - "*"
      PolicyName: vpc-allow
      Roles:
        - Ref: OrchestratorRole
    Condition: HasLambdaVpc
  OrchestratorLambdaVpcDenyPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Deny
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSubnets
              - ec2:DeleteNetworkInterface
              - ec2:AssignPrivateIpAddresses
              - ec2:UnassignPrivateIpAddresses
            Resource: "*"
            Condition:
              ArnEquals:
                lambda:SourceFunctionArn:
                  - Fn::GetAtt:
                      - OrchestratorFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: OrchestratorRole
    Condition: HasLambdaVpc
  OrchestratorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: OrchestratorFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  OrchestratorPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - OrchestratorLogGroup
                - Arn
      PolicyName: log
      Roles:
        - Ref: OrchestratorRole
  StateMachineNameGeneratorOrchestrator:
    Type: Custom::StateMachineNameGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - StateMachineNameGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Suffix: -orchestrator
    DependsOn:
      - StateMachineNameGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  OrchestratorStateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/vendedlogs/states/
            - Ref: AWS::StackName
            - -OrchestratorStateMachine
      RetentionInDays:
        Ref: LogsRetentionInDays
  OrchestratorStateMachineRole:
    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:
                    - OrchestratorFunction
                    - Arn
              - Effect: Allow
                Action: states:StartExecution
                Resource:
                  - Fn::GetAtt:
                      - StateMachine
                      - Arn
                  - Fn::If:
                      - HasReportingAddOnStackName
                      - Fn::GetAtt:
                          - WaitForScanToFinishStateMachine
                          - Arn
                      - Ref: AWS::NoValue
                  - Fn::If:
                      - HasReportingAddOnStackName
                      - Fn::ImportValue:
                          Fn::Join:
                            - ""
                            - - Ref: ReportingAddOnStackName
                              - -StateMachineArn
                      - Ref: AWS::NoValue
              - Effect: Allow
                Action:
                  - states:DescribeExecution
                  - states:StopExecution
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":states:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":execution:"
                        - Fn::GetAtt:
                            - StateMachine
                            - Name
                        - :*
                  - Fn::If:
                      - HasReportingAddOnStackName
                      - Fn::Join:
                          - ""
                          - - "arn:"
                            - Ref: AWS::Partition
                            - ":states:"
                            - Ref: AWS::Region
                            - ":"
                            - Ref: AWS::AccountId
                            - ":execution:"
                            - Fn::GetAtt:
                                - WaitForScanToFinishStateMachine
                                - Name
                            - :*
                      - Ref: AWS::NoValue
                  - Fn::If:
                      - HasReportingAddOnStackName
                      - Fn::Join:
                          - ""
                          - - Fn::ImportValue:
                                Fn::Join:
                                  - ""
                                  - - Ref: ReportingAddOnStackName
                                    - -StateMachineArn
                            - :*
                      - Ref: AWS::NoValue
              - Effect: Allow
                Action:
                  - events:PutTargets
                  - events:PutRule
                  - events:DescribeRule
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :rule/StepFunctionsGetEventsForStepFunctionsExecutionRule
              - Fn::If:
                  - HasReportingAddOnStackName
                  - Effect: Allow
                    Action: sqs:GetQueueAttributes
                    Resource:
                      Fn::ImportValue:
                        Fn::Join:
                          - ""
                          - - Ref: BucketAVStackName
                            - -ScanQueueArn
                  - Ref: AWS::NoValue
              - 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:
                    - OrchestratorStateMachineLogGroup
                    - Arn
          PolicyName: states
  OrchestratorStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: "bucketAV add-on (scheduled bucket scan): Orchestrates a scan run."
        StartAt: ListBuckets
        States:
          ListBuckets:
            Type: Task
            Resource:
              Fn::GetAtt:
                - OrchestratorFunction
                - Arn
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Next:
              Fn::If:
                - HasReportingAddOnStackName
                - FetchScanQueueLength
                - ScanBuckets
          FetchScanQueueLength:
            Fn::If:
              - HasReportingAddOnStackName
              - Type: Task
                Resource: arn:aws:states:::aws-sdk:sqs:getQueueAttributes
                Next: ScanBuckets
                ResultPath: $.scanQueueLengthBeforeScan
                ResultSelector:
                  total.$: States.MathAdd(States.StringToJson($.Attributes.ApproximateNumberOfMessages), States.MathAdd(States.StringToJson($.Attributes.ApproximateNumberOfMessagesDelayed), States.StringToJson($.Attributes.ApproximateNumberOfMessagesNotVisible)))
                Parameters:
                  AttributeNames:
                    - ApproximateNumberOfMessages
                    - ApproximateNumberOfMessagesDelayed
                    - ApproximateNumberOfMessagesNotVisible
                  QueueUrl:
                    Fn::ImportValue:
                      Fn::Join:
                        - ""
                        - - Ref: BucketAVStackName
                          - -ScanQueueUrl
              - Ref: AWS::NoValue
          ScanBuckets:
            Type: Map
            ItemsPath: $.buckets
            ItemSelector:
              bucket.$: $$.Map.Item.Value
              tagKey.$: $.tagKey
              traceId.$:
                Fn::Join:
                  - ""
                  - - "States.Format('bucketav:"
                    - Ref: BucketAVStackName
                    - ":"
                    - Ref: AWS::StackName
                    - ":"
                    - Ref: AWS::Region
                    - ":"
                    - Ref: AWS::AccountId
                    - :bucket-scan/{}', $$.Execution.Name)
            Next:
              Fn::If:
                - HasReportingAddOnStackName
                - WaitForScanToFinish
                - Done
            ResultPath: $.results
            ItemProcessor:
              ProcessorConfig:
                Mode: INLINE
              StartAt: ScanBucket
              States:
                ScanBucket:
                  Type: Task
                  Resource: arn:aws:states:::states:startExecution.sync:2
                  Parameters:
                    StateMachineArn:
                      Fn::GetAtt:
                        - StateMachine
                        - Arn
                    Input:
                      bucket.$: $.bucket
                      tagKey.$: $.tagKey
                      traceId.$: $.traceId
                  TimeoutSeconds:
                    Ref: TimeoutInSeconds
                  Retry:
                    - ErrorEquals:
                        - States.TaskFailed
                      IntervalSeconds: 30
                      MaxAttempts: 3
                      BackoffRate: 2
                  End: true
            MaxConcurrency:
              Ref: BucketConcurrency
          WaitForScanToFinish:
            Fn::If:
              - HasReportingAddOnStackName
              - Type: Task
                Resource: arn:aws:states:::states:startExecution.sync:2
                Next: WaitForReportData
                Parameters:
                  StateMachineArn:
                    Ref: WaitForScanToFinishStateMachine
                  Input:
                    scanQueueLengthBeforeScan.$: $.scanQueueLengthBeforeScan.total
                Catch:
                  - ErrorEquals:
                      - States.Timeout
                    Next: WaitForReportData
              - Ref: AWS::NoValue
          WaitForReportData:
            Fn::If:
              - HasReportingAddOnStackName
              - Type: Wait
                Seconds: 420
                Next: GenerateReport
              - Ref: AWS::NoValue
          GenerateReport:
            Fn::If:
              - HasReportingAddOnStackName
              - Type: Task
                Resource: arn:aws:states:::states:startExecution.sync:2
                Next: Done
                Parameters:
                  StateMachineArn:
                    Fn::ImportValue:
                      Fn::Join:
                        - ""
                        - - Ref: ReportingAddOnStackName
                          - -StateMachineArn
                  Input:
                    begin.$: $$.Execution.StartTime
                    end.$: $$.State.EnteredTime
                    traceId.$:
                      Fn::Join:
                        - ""
                        - - "States.Format('bucketav:"
                          - Ref: BucketAVStackName
                          - ":"
                          - Ref: AWS::StackName
                          - ":"
                          - Ref: AWS::Region
                          - ":"
                          - Ref: AWS::AccountId
                          - :bucket-scan/{}', $$.Execution.Name)
              - Ref: AWS::NoValue
          Done:
            Type: Succeed
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn:
                Fn::GetAtt:
                  - OrchestratorStateMachineLogGroup
                  - Arn
        IncludeExecutionData: true
        Level: ERROR
      RoleArn:
        Fn::GetAtt:
          - OrchestratorStateMachineRole
          - Arn
      StateMachineName:
        Ref: StateMachineNameGeneratorOrchestrator
    DependsOn:
      - OrchestratorPolicy
  OrchestratorStateMachineAlarmExecutionsFailed:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV scheduled-bucket-scan add-on has failed executions. Check executions of state machine "
            - Ref: OrchestratorStateMachine
            - "!"
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: OrchestratorStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsFailed
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  OrchestratorStateMachineAlarmExecutionsTimedOut:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV scheduled-bucket-scan add-on has timed out executions. Check executions of state machine "
            - Ref: OrchestratorStateMachine
            - "!"
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: OrchestratorStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsTimedOut
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  CronRole:
    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: OrchestratorStateMachine
          PolicyName: events
    Condition: HasScheduleExpression
  CronRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression:
        Ref: ScheduleExpression
      Targets:
        - Arn:
            Ref: OrchestratorStateMachine
          Id: states
          InputTransformer:
            InputTemplate: "{}"
          RoleArn:
            Fn::GetAtt:
              - CronRole
              - Arn
    Condition: HasScheduleExpression
  CronRuleAlarmFailedInvocationsTooHigh:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scheduled-bucket-scan add-on can not be triggert.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: RuleName
          Value:
            Ref: CronRule
      EvaluationPeriods: 1
      MetricName: FailedInvocations
      Namespace: AWS/Events
      Period: 60
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
    Condition: HasScheduleExpression
  ServiceDiscoveryOrchestratorStateMachineName:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: BucketAVStackName
            - /AddOn/scheduled-bucket-scan/
            - Ref: AWS::StackName
            - /OrchestratorStateMachineName
      Type: String
      Value:
        Fn::GetAtt:
          - OrchestratorStateMachine
          - Name
  ServiceDiscoveryVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: BucketAVStackName
            - /AddOn/scheduled-bucket-scan/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 2.20.0
Outputs:
  RoleArn:
    Description: The ARN of the IAM Role listing the S3 bucket.
    Value:
      Fn::GetAtt:
        - RunRole
        - Arn
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -RoleArn
  RunFunctionName:
    Description: The name of the run Lambda function.
    Value:
      Ref: RunFunction
  OrchestratorFunctionName:
    Description: The name of the orchestrator Lambda function.
    Value:
      Ref: OrchestratorFunction
  BucketArnGeneratorFunctionName:
    Description: The name of the bucket ARN generator Lambda function.
    Value:
      Ref: BucketArnGeneratorFunction
  AddOn:
    Description: bucketAV add-on.
    Value: scheduled-bucket-scan
  Platform:
    Description: bucketAV platform.
    Value: aws
  Version:
    Description: bucketAV add-on version.
    Value: 2.20.0
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

