---
# 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 move infected files / quarantine Add-On for Amazon S3
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - BucketAVStackName
          - QuarantineBucketName
      - Label:
          default: Quarantine Parameters
        Parameters:
          - PrefixKeyWithBucketName
          - SourceBucketNames
          - LogsRetentionInDays
          - QuarantineUnscannableFiles
          - IgnoreObjectsExpression
      - Label:
          default: Permissions Parameters
        Parameters:
          - S3BucketRestriction
          - S3ObjectRestriction
          - KMSKeyRestriction
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - LambdaFunctionReservedConcurrentExecutions
          - LambdaVpc
          - LambdaSubnets
          - LargeObjectManageFunctionReservedConcurrentExecutions
          - LargeObjectCopyPartFunctionReservedConcurrentExecutions
          - StateMachineNameGeneratorFunctionReservedConcurrentExecutions
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)
  QuarantineBucketName:
    Type: String
    Description: Name of S3 bucket where quarantined files are stored.
  PrefixKeyWithBucketName:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Prefix quarantined files with the source bucket name.
  SourceBucketNames:
    Type: String
    Default: ""
    Description: Optinally, quarantine files from a specific source bucket(s) only (leave blank to work with all buckets; comma separate multiple buckets).
  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.
  QuarantineUnscannableFiles:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Quarantine unscannable files (too big, password protected, ...) as well.
  IgnoreObjectsExpression:
    Type: String
    Default: ^$
    Description: Optionally, ignore objects whose keys match the regular expression. Use ^$ to move all objects.
  S3BucketRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific S3 buckets (e.g. arn:aws:s3:::bucket-a,arn:aws:s3:::bucket-b or * to allow access to all S3 buckets).
  S3ObjectRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific S3 objects (e.g. arn:aws:s3:::bucket-a/*,arn:aws:s3:::bucket-b/* or * to allow access to all S3 objects).
  KMSKeyRestriction:
    Type: CommaDelimitedList
    Default: "*"
    Description: Restrict access to specific KMS keys (e.g. arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab,arn:aws:kms:us-east-1:111122223333:key/0987dcba-09fe-87dc-65ba-ab0987654321 or * to allow access to all KMS keys).
  PermissionsBoundary:
    Type: String
    Default: ""
    Description: Optional IAM policy ARN that will be used as the permissions boundary for all roles.
  LambdaFunctionReservedConcurrentExecutions:
    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.
  LargeObjectManageFunctionReservedConcurrentExecutions:
    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
  LargeObjectCopyPartFunctionReservedConcurrentExecutions:
    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
              - ""
  HasSourceBucketNames:
    Fn::Not:
      - Fn::Equals:
          - Ref: SourceBucketNames
          - ""
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasQuarantineUnscannableFiles:
    Fn::Equals:
      - Ref: QuarantineUnscannableFiles
      - "true"
  HasLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: LambdaFunctionReservedConcurrentExecutions
          - 0
  HasLargeObjectManageFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: LargeObjectManageFunctionReservedConcurrentExecutions
          - 0
  HasLargeObjectCopyPartFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: LargeObjectCopyPartFunctionReservedConcurrentExecutions
          - 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
  LargeObjectManageRole:
    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:GetObject
                  - s3:GetObjectVersion
                  - s3:GetObjectTagging
                  - s3:GetObjectVersionTagging
                  - s3:DeleteObject
                  - s3:DeleteObjectVersion
                Resource:
                  Ref: S3ObjectRestriction
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:ListBucketVersions
                Resource:
                  Ref: S3BucketRestriction
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:PutObjectAcl
                  - s3:PutObjectTagging
                  - s3:AbortMultipartUpload
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":s3:::"
                      - Ref: QuarantineBucketName
                      - /*
              - Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource:
                  Ref: KMSKeyRestriction
                Condition:
                  StringLike:
                    kms:ViaService: s3.*.amazonaws.com
              - 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: core
  LargeObjectManageFunction:
    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-move-manage.js
          var add_on_move_manage_exports = {};
          __export(add_on_move_manage_exports, {
            abort: () => abort,
            complete: () => complete,
            handler: () => handler,
            init: () => init
          });
          module.exports = __toCommonJS(add_on_move_manage_exports);
          var import_client_s33 = require("@aws-sdk/client-s3");
          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");
          var import_node_querystring = require("node:querystring");

          // 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-move-manage.js
          var defaultS3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          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;
          function range(startInclusive, endInclusive) {
            const r = [];
            for (let i = startInclusive; i <= endInclusive; i++) {
              r.push(i);
            }
            return r;
          }
          async function init(s3, event) {
            const headObjectInput = {
              Bucket: event.sourceBucket,
              Key: event.sourceKey
            };
            if (event.sourceVersion !== "") {
              headObjectInput.VersionId = event.sourceVersion;
            }
            const {
              CacheControl,
              ContentLength,
              ContentType,
              Metadata,
              StorageClass,
              WebsiteRedirectLocation
            } = await s3.send(new import_client_s33.HeadObjectCommand(headObjectInput));
            const createMultipartUploadInput = {
              ACL: "bucket-owner-full-control",
              Bucket: event.targetBucket,
              CacheControl,
              ContentType,
              Key: event.targetKey,
              Metadata,
              StorageClass,
              WebsiteRedirectLocation
            };
            if (process.env.CORE_PLATFORM === "aws") {
              const { TagSet } = await s3.send(new import_client_s33.GetObjectTaggingCommand(headObjectInput));
              const Tagging = TagSet === void 0 ? void 0 : (0, import_node_querystring.stringify)(TagSet?.reduce((agg, { Key, Value }) => {
                agg[Key] = Value;
                return agg;
              }, {}));
              createMultipartUploadInput.Tagging = Tagging;
            }
            const {
              UploadId: uploadId
            } = await s3.send(new import_client_s33.CreateMultipartUploadCommand(createMultipartUploadInput));
            const parts = Math.ceil(ContentLength / MAX_S3_COPY_SIZE);
            return {
              ...event,
              uploadId,
              parts: range(1, parts).map((partNumber) => ({
                partNumber,
                range: `bytes=${(partNumber - 1) * MAX_S3_COPY_SIZE}-${Math.min(partNumber * MAX_S3_COPY_SIZE, ContentLength) - 1}`
              }))
            };
          }
          async function complete(s3, event) {
            await s3.send(new import_client_s33.CompleteMultipartUploadCommand({
              Bucket: event.targetBucket,
              Key: event.targetKey,
              MultipartUpload: {
                Parts: event.parts.map(({ partNumber, etag }) => ({ PartNumber: partNumber, ETag: etag }))
              },
              UploadId: event.uploadId
            }));
            if (event.mode === "move") {
              const input = {
                Bucket: event.sourceBucket,
                Key: event.sourceKey
              };
              if (event.sourceVersion !== "") {
                input.VersionId = event.sourceVersion;
              }
              console.log(`Delete object: ${event.sourceBucket}/${event.sourceKey}`);
              await s3.send(new import_client_s33.DeleteObjectCommand(input));
            }
            return {};
          }
          async function abort(s3, event) {
            await s3.send(new import_client_s33.AbortMultipartUploadCommand({
              Bucket: event.targetBucket,
              Key: event.targetKey,
              UploadId: event.uploadId
            }));
            return {};
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const now = Date.now();
            if (now - bucketCacheRefreshedAt > 36e5) {
              bucketCache.clear();
              if (process.env.CORE_PLATFORM === "aws") {
                isCrossAccountPromise = fetchCrossAccount(ssm, process.env.CORE_STACK_NAME);
              }
              bucketCacheRefreshedAt = now;
            }
            const s3 = process.env.CORE_PLATFORM === "aws" ? await fetchS3(defaultS3, dynamodb, bucketCache, await isCrossAccountPromise, event.sourceBucket, `${process.env.CORE_STACK_NAME}-BucketCache`) : await fetchR2(ssm, secretsmanager, process.env.CORE_STACK_NAME);
            if (event.action === "init") {
              return init(s3, event);
            } else if (event.action === "complete") {
              return complete(s3, event);
            } else if (event.action === "abort") {
              return abort(s3, event);
            } else {
              throw new Error("unsupported axction");
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            abort,
            complete,
            handler,
            init
          });
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          CORE_PLATFORM: aws
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasLargeObjectManageFunctionReservedConcurrentExecutions
          - Ref: LargeObjectManageFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - LargeObjectManageRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 55
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::LargeObjectManageLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: LargeObjectManageLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  LargeObjectManageLambdaVpcAllowPolicy:
    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: LargeObjectManageRole
    Condition: HasLambdaVpc
  LargeObjectManageLambdaVpcDenyPolicy:
    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:
                      - LargeObjectManageFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: LargeObjectManageRole
    Condition: HasLambdaVpc
  LargeObjectManageLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: LargeObjectManageFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  LargeObjectManagePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - LargeObjectManageLogGroup
                - Arn
      PolicyName: log
      Roles:
        - Ref: LargeObjectManageRole
  LargeObjectCopyPartRole:
    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:GetObject
                  - s3:GetObjectVersion
                Resource:
                  Ref: S3ObjectRestriction
              - Effect: Allow
                Action: s3:ListBucket*
                Resource:
                  Ref: S3BucketRestriction
              - Effect: Allow
                Action: s3:PutObject
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":s3:::"
                      - Ref: QuarantineBucketName
                      - /*
              - Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource:
                  Ref: KMSKeyRestriction
                Condition:
                  StringLike:
                    kms:ViaService: s3.*.amazonaws.com
              - 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: core
  LargeObjectCopyPartFunction:
    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-move-copy-part.js
          var add_on_move_copy_part_exports = {};
          __export(add_on_move_copy_part_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_move_copy_part_exports);
          var import_client_s33 = require("@aws-sdk/client-s3");
          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
              }
            });
          }
          function saveKey(key) {
            return encodeURIComponent(key).replace(/%20/g, "+").replace(/%2F/g, "/");
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;

          // lambda/add-on-move-copy-part.js
          var CORE_PLATFORM = process.env.CORE_PLATFORM;
          var defaultS3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          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 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.sourceBucket, `${process.env.CORE_STACK_NAME}-BucketCache`) : await fetchR2(ssm, secretsmanager, process.env.CORE_STACK_NAME);
            const input = {
              Bucket: event.targetBucket,
              CopySource: `${event.sourceBucket}/${saveKey(event.sourceKey)}`,
              CopySourceRange: event.range,
              Key: event.targetKey,
              PartNumber: event.partNumber,
              UploadId: event.uploadId
            };
            if (event.sourceVersion !== "") {
              input.CopySource += `?versionId=${event.sourceVersion}`;
            }
            const {
              CopyPartResult: {
                ETag: etag
              }
            } = await s3.send(new import_client_s33.UploadPartCopyCommand(input));
            return {
              partNumber: event.partNumber,
              etag
            };
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          CORE_PLATFORM: aws
      Handler: index.handler
      MemorySize: 128
      ReservedConcurrentExecutions:
        Fn::If:
          - HasLargeObjectCopyPartFunctionReservedConcurrentExecutions
          - Ref: LargeObjectCopyPartFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - LargeObjectCopyPartRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 895
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::LargeObjectCopyPartLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: LargeObjectCopyPartLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  LargeObjectCopyPartLambdaVpcAllowPolicy:
    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: LargeObjectCopyPartRole
    Condition: HasLambdaVpc
  LargeObjectCopyPartLambdaVpcDenyPolicy:
    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:
                      - LargeObjectCopyPartFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: LargeObjectCopyPartRole
    Condition: HasLambdaVpc
  LargeObjectCopyPartLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: LargeObjectCopyPartFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  LargeObjectCopyPartPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - LargeObjectCopyPartLogGroup
                - Arn
      PolicyName: log
      Roles:
        - Ref: LargeObjectCopyPartRole
  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
  StateMachineNameGeneratorLargeObject:
    Type: Custom::StateMachineNameGenerator
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - StateMachineNameGeneratorFunction
          - Arn
      ServiceTimeout: "30"
      Suffix: -large-object
    DependsOn:
      - StateMachineNameGeneratorPolicy
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  LargeObjectStateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/vendedlogs/states/
            - Ref: AWS::StackName
            - -LargeObjectStateMachine
      RetentionInDays:
        Ref: LogsRetentionInDays
  LargeObjectStateMachineRole:
    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:
                      - LargeObjectManageFunction
                      - Arn
                  - Fn::GetAtt:
                      - LargeObjectCopyPartFunction
                      - Arn
              - Effect: Allow
                Action: states:StartExecution
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":states:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - ":stateMachine:"
                      - Ref: StateMachineNameGeneratorLargeObject
              - 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:
                    - LargeObjectStateMachineLogGroup
                    - Arn
          PolicyName: states
  LargeObjectStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: "bucketAV add-on (quarantine): Move file."
        TimeoutSeconds: 22140
        StartAt: Init
        States:
          Init:
            Type: Task
            Resource:
              Fn::GetAtt:
                - LargeObjectManageFunction
                - Arn
            Parameters:
              sourceBucket.$: $.sourceBucket
              sourceKey.$: $.sourceKey
              sourceVersion.$: $.sourceVersion
              targetBucket.$: $.targetBucket
              targetKey.$: $.targetKey
              mode.$: $.mode
              action: init
            TimeoutSeconds: 60
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                  - States.Timeout
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Next: CopyParts
          CopyParts:
            Type: Map
            ItemsPath: $.parts
            ItemSelector:
              range.$: $$.Map.Item.Value.range
              partNumber.$: $$.Map.Item.Value.partNumber
              uploadId.$: $.uploadId
              sourceBucket.$: $.sourceBucket
              sourceKey.$: $.sourceKey
              sourceVersion.$: $.sourceVersion
              targetBucket.$: $.targetBucket
              targetKey.$: $.targetKey
            Next: Complete
            ResultPath: $.parts
            ItemProcessor:
              ProcessorConfig:
                Mode: DISTRIBUTED
                ExecutionType: STANDARD
              StartAt: CopyPart
              States:
                CopyPart:
                  Type: Task
                  Resource:
                    Fn::GetAtt:
                      - LargeObjectCopyPartFunction
                      - Arn
                  TimeoutSeconds: 900
                  Retry:
                    - ErrorEquals:
                        - States.TaskFailed
                        - States.Timeout
                      IntervalSeconds: 2
                      MaxAttempts: 10
                      BackoffRate: 2
                  End: true
            MaxConcurrency: 128
            Catch:
              - ErrorEquals:
                  - States.ALL
                Next: Abort
                ResultPath: $.error
          Complete:
            Type: Task
            Resource:
              Fn::GetAtt:
                - LargeObjectManageFunction
                - Arn
            Parameters:
              sourceBucket.$: $.sourceBucket
              sourceKey.$: $.sourceKey
              sourceVersion.$: $.sourceVersion
              targetBucket.$: $.targetBucket
              targetKey.$: $.targetKey
              mode.$: $.mode
              uploadId.$: $.uploadId
              parts.$: $.parts
              action: complete
            TimeoutSeconds: 60
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                  - States.Timeout
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Catch:
              - ErrorEquals:
                  - States.ALL
                Next: Abort
                ResultPath: $.error
            Next: Succeed
          Abort:
            Type: Task
            Resource:
              Fn::GetAtt:
                - LargeObjectManageFunction
                - Arn
            Parameters:
              sourceBucket.$: $.sourceBucket
              sourceKey.$: $.sourceKey
              sourceVersion.$: $.sourceVersion
              targetBucket.$: $.targetBucket
              targetKey.$: $.targetKey
              mode.$: $.mode
              uploadId.$: $.uploadId
              action: abort
            TimeoutSeconds: 60
            Retry:
              - ErrorEquals:
                  - States.TaskFailed
                  - States.Timeout
                IntervalSeconds: 2
                MaxAttempts: 10
                BackoffRate: 2
            Next: Fail
          Succeed:
            Type: Succeed
          Fail:
            Type: Fail
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn:
                Fn::GetAtt:
                  - LargeObjectStateMachineLogGroup
                  - Arn
        IncludeExecutionData: true
        Level: ERROR
      RoleArn:
        Fn::GetAtt:
          - LargeObjectStateMachineRole
          - Arn
      StateMachineName:
        Ref: StateMachineNameGeneratorLargeObject
    DependsOn:
      - LargeObjectCopyPartPolicy
      - LargeObjectManagePolicy
  LargeObjectStateMachineAlarmExecutionsFailed:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV quarantine add-on has failed executions. Check executions of state machine "
            - Ref: LargeObjectStateMachine
            - "!"
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: LargeObjectStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsFailed
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  LargeObjectStateMachineAlarmExecutionsTimedOut:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV quarantine add-on has timed out executions. Check executions of state machine "
            - Ref: LargeObjectStateMachine
            - "!"
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: StateMachineArn
          Value:
            Ref: LargeObjectStateMachine
      EvaluationPeriods: 1
      MetricName: ExecutionsTimedOut
      Namespace: AWS/States
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  LambdaRole:
    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:GetObject*
                  - s3:DeleteObject*
                Resource:
                  Ref: S3ObjectRestriction
              - Effect: Allow
                Action: s3:ListBucket*
                Resource:
                  Ref: S3BucketRestriction
              - Effect: Allow
                Action: s3:PutObject*
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":s3:::"
                      - Ref: QuarantineBucketName
                      - /*
              - Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource:
                  Ref: KMSKeyRestriction
                Condition:
                  StringLike:
                    kms:ViaService: s3.*.amazonaws.com
              - 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
              - Effect: Allow
                Action: states:StartExecution
                Resource:
                  Ref: LargeObjectStateMachine
          PolicyName: core
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - LambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
  LambdaFunction:
    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-move.js
          var add_on_move_exports = {};
          __export(add_on_move_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_move_exports);
          var import_client_s33 = require("@aws-sdk/client-s3");
          var import_client_ssm2 = require("@aws-sdk/client-ssm");
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
          var import_client_sfn = require("@aws-sdk/client-sfn");
          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
              }
            });
          }
          function saveKey(key) {
            return encodeURIComponent(key).replace(/%20/g, "+").replace(/%2F/g, "/");
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;

          // lambda/add-on-move.js
          var CORE_PLATFORM = process.env.CORE_PLATFORM;
          var MODE = process.env.MODE || "move";
          var PREFIX_KEY_WITH_BUCKET_NAME = process.env.PREFIX_KEY_WITH_BUCKET_NAME === "true";
          var IGNORE_FOLDER_PLACEHOLDERS = process.env.IGNORE_FOLDER_PLACEHOLDERS === "true";
          var IGNORE_OBJECTS_EXPRESSION = new RegExp(process.env.IGNORE_OBJECTS_EXPRESSION);
          var defaultS3 = new import_client_s33.S3Client({ apiVersion: "2006-03-01" });
          var ssm = new import_client_ssm2.SSMClient({ apiVersion: "2014-11-06" });
          var dynamodb = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
          var sfn = new import_client_sfn.SFNClient({ apiVersion: "2016-11-23" });
          var secretsmanager = new import_client_secrets_manager2.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var bucketCache = /* @__PURE__ */ new Map();
          var bucketCacheRefreshedAt = 0;
          var isCrossAccountPromise = void 0;
          function map(record) {
            if (record.Sns.Message.startsWith("{")) {
              const message = JSON.parse(record.Sns.Message);
              return {
                messageId: record.Sns.MessageId,
                bucket: message.bucket,
                key: message.key,
                saveKey: saveKey(message.key),
                version: message.version,
                size: message.size
              };
            } else {
              return {
                messageId: record.Sns.MessageId,
                bucket: record.Sns.MessageAttributes.bucket.Value,
                key: record.Sns.MessageAttributes.key.Value,
                saveKey: saveKey(record.Sns.MessageAttributes.key.Value),
                version: "version" in record.Sns.MessageAttributes ? record.Sns.MessageAttributes.version.Value : void 0,
                size: parseInt(record.Sns.MessageAttributes.size.Value, 10)
              };
            }
          }
          function targetKey(bucket, key) {
            if (PREFIX_KEY_WITH_BUCKET_NAME === true) {
              return `${bucket}/${key}`;
            } else {
              return key;
            }
          }
          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 promises = event.Records.map(map).filter(({ bucket, key, saveKey: saveKey2 }) => {
              if (saveKey2.endsWith("/") && IGNORE_FOLDER_PLACEHOLDERS === true) {
                console.log(`Skipping folder placholder: ${bucket}/${key}`);
                return false;
              }
              if (IGNORE_OBJECTS_EXPRESSION.test(saveKey2)) {
                console.log(`Skipping object as it matches the IgnoreObjectsExpression: ${bucket}/${key}`);
                return false;
              }
              return true;
            }).map(async ({ messageId, bucket, key, saveKey: saveKey2, version, size }) => {
              if (size > MAX_S3_COPY_SIZE) {
                console.log(`Copy object in state machine: ${bucket}/${key}`);
                try {
                  await sfn.send(new import_client_sfn.StartExecutionCommand({
                    stateMachineArn: process.env.COPY_STATE_MACHINE_ARN,
                    name: messageId,
                    input: JSON.stringify({
                      sourceBucket: bucket,
                      sourceKey: key,
                      sourceVersion: version === void 0 ? "" : version,
                      // optional fields are hard to use in StateMachines, therefore we use the empty string to indicate non versioned objects
                      targetBucket: process.env.TARGET_BUCKET_NAME,
                      targetKey: targetKey(bucket, key),
                      mode: MODE
                    })
                  }));
                } catch (err) {
                  if (err.name !== "ExecutionAlreadyExists") {
                    throw err;
                  }
                }
              } else {
                const input = {
                  Bucket: process.env.TARGET_BUCKET_NAME,
                  Key: targetKey(bucket, key),
                  CopySource: `${bucket}/${saveKey2}`,
                  ACL: "bucket-owner-full-control"
                };
                if (version !== void 0) {
                  input.CopySource += `?versionId=${version}`;
                }
                const s3 = CORE_PLATFORM === "aws" ? await fetchS3(defaultS3, dynamodb, bucketCache, await isCrossAccountPromise, bucket, `${process.env.CORE_STACK_NAME}-BucketCache`) : await fetchR2(ssm, secretsmanager, process.env.CORE_STACK_NAME);
                console.log(`Copy object: ${bucket}/${key}`);
                return s3.send(new import_client_s33.CopyObjectCommand(input)).then(() => {
                  if (MODE === "move") {
                    const input2 = {
                      Bucket: bucket,
                      Key: key
                    };
                    if (version !== void 0) {
                      input2.VersionId = version;
                    }
                    console.log(`Delete object: ${bucket}/${key}`);
                    return s3.send(new import_client_s33.DeleteObjectCommand(input2));
                  } else {
                    return {};
                  }
                }).catch((err) => {
                  if (err.name === "NoSuchKey" || err.name === "NoSuchVersion") {
                    return {};
                  }
                  throw err;
                });
              }
            });
            await Promise.all(promises);
            return true;
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - LambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          PREFIX_KEY_WITH_BUCKET_NAME:
            Ref: PrefixKeyWithBucketName
          IGNORE_FOLDER_PLACEHOLDERS: "false"
          IGNORE_OBJECTS_EXPRESSION:
            Ref: IgnoreObjectsExpression
          TARGET_BUCKET_NAME:
            Ref: QuarantineBucketName
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          COPY_STATE_MACHINE_ARN:
            Ref: LargeObjectStateMachine
          CORE_PLATFORM: aws
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasLambdaFunctionReservedConcurrentExecutions
          - Ref: LambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - LambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 900
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::LambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: LambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  LambdaVpcAllowPolicy:
    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: LambdaRole
    Condition: HasLambdaVpc
  LambdaVpcDenyPolicy:
    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:
                      - LambdaFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: LambdaRole
    Condition: HasLambdaVpc
  LambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV quarantine add-on failed. Check logs of AWS Lambda Function "
            - Ref: LambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: LambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  LambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV quarantine add-on throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: LambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  LambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
  LambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV quarantine add-on has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - LambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  LambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV quarantine add-on has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: LambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 1200
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  LambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: LambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  LambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - LambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: LambdaRole
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: LambdaFunction
      Principal: sns.amazonaws.com
      SourceArn:
        Fn::ImportValue:
          Fn::Join:
            - ""
            - - Ref: BucketAVStackName
              - -FindingsTopicArn
  Subscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - LambdaFunction
          - Arn
      FilterPolicy:
        status:
          Fn::If:
            - HasQuarantineUnscannableFiles
            - - infected
              - "no"
            - - infected
        bucket:
          Fn::If:
            - HasSourceBucketNames
            - Fn::Split:
                - ","
                - Ref: SourceBucketNames
            - - anything-but:
                  - Ref: QuarantineBucketName
      Protocol: lambda
      TopicArn:
        Fn::ImportValue:
          Fn::Join:
            - ""
            - - Ref: BucketAVStackName
              - -FindingsTopicArn
    DependsOn:
      - LambdaPermission
      - LambdaPolicy
  ServiceDiscoveryVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: BucketAVStackName
            - /AddOn/quarantine/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 2.16.0
Outputs:
  LambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - LambdaDeadLetterQueue
        - QueueName
  QuarantineRoleArn:
    Description: The ARN of the Quarantine Role.
    Value:
      Fn::GetAtt:
        - LambdaRole
        - Arn
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -QuarantineRoleArn
  AddOn:
    Description: bucketAV add-on.
    Value: quarantine
  Platform:
    Description: bucketAV platform.
    Value: aws
  Version:
    Description: bucketAV add-on version.
    Value: 2.16.0
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

