---
# 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 scan-on-access Add-On for Amazon S3
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - BucketAVStackName
          - BucketName
          - BucketAccountId
      - Label:
          default: Scan on-access Parameters
        Parameters:
          - CacheTTL
          - LogsRetentionInDays
      - Label:
          default: Permissions Parameters
        Parameters:
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - S3ObjectLambdaFunctionReservedConcurrentExecutions
          - SubscriptionLambdaFunctionReservedConcurrentExecutions
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
    Description: S3 bucket name.
  BucketAccountId:
    Type: String
    Default: ""
    Description: S3 bucket account ID if the bucket is located in a different AWS account than this CloudFormation stack.
  CacheTTL:
    Type: Number
    Default: 3600
    Description: Scan results can be cached for improved performance (time to live in seconds; to disable the cache, set the value to 0).
    MaxValue: 10240
    MinValue: 0
  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.
  PermissionsBoundary:
    Type: String
    Default: ""
    Description: Optional IAM policy ARN that will be used as the permissions boundary for all roles.
  SubscriptionLambdaFunctionReservedConcurrentExecutions:
    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
  S3ObjectLambdaFunctionReservedConcurrentExecutions:
    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.
Conditions:
  HasLambdaVpc:
    Fn::And:
      - Fn::Not:
          - Fn::Equals:
              - Fn::Join:
                  - ""
                  - Ref: LambdaSubnets
              - ""
      - Fn::Not:
          - Fn::Equals:
              - Ref: LambdaVpc
              - ""
  HasBucketAccountId:
    Fn::Not:
      - Fn::Equals:
          - Ref: BucketAccountId
          - ""
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasSubscriptionLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: SubscriptionLambdaFunctionReservedConcurrentExecutions
          - 0
  HasS3ObjectLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: S3ObjectLambdaFunctionReservedConcurrentExecutions
          - 0
Resources:
  Table:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      SSESpecification:
        SSEEnabled: true
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true
  TableUserErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on user errors.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: TableName
          Value:
            Ref: Table
      EvaluationPeriods: 1
      MetricName: UserErrors
      Namespace: AWS/DynamoDB
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  TableSystemErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on system errors.
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: TableName
          Value:
            Ref: Table
      EvaluationPeriods: 1
      MetricName: SystemErrors
      Namespace: AWS/DynamoDB
      Period: 300
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  SubscriptionLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      PermissionsBoundary:
        Fn::If:
          - HasPermissionsBoundary
          - Ref: PermissionsBoundary
          - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: dynamodb:PutItem
                Resource:
                  Fn::GetAtt:
                    - Table
                    - Arn
          PolicyName: lambda
        - PolicyDocument:
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource:
                  Fn::GetAtt:
                    - SubscriptionLambdaDeadLetterQueue
                    - Arn
          PolicyName: dlq
  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
  SubscriptionLambdaFunction:
    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-scan-on-access-subscription.js
          var add_on_scan_on_access_subscription_exports = {};
          __export(add_on_scan_on_access_subscription_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_scan_on_access_subscription_exports);
          var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");

          // lambda/add-on-scan-on-access-s3-object-lambda.js
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_sqs = require("@aws-sdk/client-sqs");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var CACHE_TTL_IN_SECONDS = parseInt(process.env.CACHE_TTL, 10);
          var POLL_TIMEOUT_IN_MILLISECONDS = 1e3 * 55;
          var s3 = new import_client_s3.S3Client({ apiVersion: "2006-03-01" });
          var sqs = new import_client_sqs.SQSClient({ apiVersion: "2012-11-05" });
          var dynamodb = new import_client_dynamodb.DynamoDBClient({ apiVersion: "2012-08-10" });
          function toID(bucket, key, version) {
            if (version) {
              return `s3://${bucket}/${key}?${version}`;
            } else {
              return `s3://${bucket}/${key}`;
            }
          }

          // lambda/add-on-scan-on-access-subscription.js
          var TRACE_TTL_IN_SECONDS = 120;
          var CACHE_TTL_IN_SECONDS2 = parseInt(process.env.CACHE_TTL, 10);
          var dynamodb2 = new import_client_dynamodb2.DynamoDBClient({ apiVersion: "2012-08-10" });
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const now = Date.now();
            await Promise.all(event.Records.map((record) => {
              const promises = [];
              if (CACHE_TTL_IN_SECONDS2 > 0) {
                promises.push(dynamodb2.send(new import_client_dynamodb2.PutItemCommand({
                  TableName: process.env.TABLE_NAME,
                  Item: {
                    id: { S: toID(record.Sns.MessageAttributes.bucket.Value, record.Sns.MessageAttributes.key.Value, record.Sns.MessageAttributes?.version?.Value) },
                    status: { S: record.Sns.MessageAttributes.status.Value },
                    ..."finding" in record.Sns.MessageAttributes ? { finding: { S: record.Sns.MessageAttributes.finding.Value } } : {},
                    ttl: { N: (now / 1e3 + CACHE_TTL_IN_SECONDS2).toFixed(0) }
                  }
                })));
              }
              promises.push(dynamodb2.send(new import_client_dynamodb2.PutItemCommand({
                TableName: process.env.TABLE_NAME,
                Item: {
                  id: { S: record.Sns.MessageAttributes.trace_id.Value },
                  status: { S: record.Sns.MessageAttributes.status.Value },
                  ..."finding" in record.Sns.MessageAttributes ? { finding: { S: record.Sns.MessageAttributes.finding.Value } } : {},
                  ttl: { N: (now / 1e3 + TRACE_TTL_IN_SECONDS).toFixed(0) }
                }
              })));
              return Promise.all(promises);
            }));
            return true;
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      DeadLetterConfig:
        TargetArn:
          Fn::GetAtt:
            - SubscriptionLambdaDeadLetterQueue
            - Arn
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
          CACHE_TTL:
            Ref: CacheTTL
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasSubscriptionLambdaFunctionReservedConcurrentExecutions
          - Ref: SubscriptionLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - SubscriptionLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 60
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::SubscriptionLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: SubscriptionLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  SubscriptionLambdaVpcAllowPolicy:
    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: SubscriptionLambdaRole
    Condition: HasLambdaVpc
  SubscriptionLambdaVpcDenyPolicy:
    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:
                      - SubscriptionLambdaFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: SubscriptionLambdaRole
    Condition: HasLambdaVpc
  SubscriptionLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV scan-on-access add-on failed. Check logs of AWS Lambda Function "
            - Ref: SubscriptionLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: SubscriptionLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  SubscriptionLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: SubscriptionLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  SubscriptionLambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600
      SqsManagedSseEnabled: true
  SubscriptionLambdaDeadLetterQueueAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on has dead letter queue messages.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value:
            Fn::GetAtt:
              - SubscriptionLambdaDeadLetterQueue
              - QueueName
      EvaluationPeriods: 1
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  SubscriptionLambdaDeadLetterErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on has dead letter queue errors.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: SubscriptionLambdaFunction
      EvaluationPeriods: 1
      MetricName: DeadLetterErrors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  SubscriptionLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: SubscriptionLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  SubscriptionLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: SubscriptionLambdaFunction
      Principal: sns.amazonaws.com
      SourceArn:
        Fn::ImportValue:
          Fn::Join:
            - ""
            - - Ref: BucketAVStackName
              - -FindingsTopicArn
  SubscriptionLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - SubscriptionLambdaLogGroup
                - Arn
      PolicyName: logs
      Roles:
        - Ref: SubscriptionLambdaRole
  Subscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - SubscriptionLambdaFunction
          - Arn
      FilterPolicy:
        trace_id:
          - prefix:
              Fn::Join:
                - ""
                - - "bucketav:scan-on-access:"
                  - Ref: AWS::StackName
                  - ":"
      Protocol: lambda
      TopicArn:
        Fn::ImportValue:
          Fn::Join:
            - ""
            - - Ref: BucketAVStackName
              - -FindingsTopicArn
    DependsOn:
      - SubscriptionLambdaPermission
      - SubscriptionLambdaPolicy
  S3ObjectLambdaRole:
    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: sqs:SendMessage
                Resource:
                  Fn::ImportValue:
                    Fn::Join:
                      - ""
                      - - Ref: BucketAVStackName
                        - -ScanQueueArn
              - Effect: Allow
                Action: dynamodb:GetItem
                Resource:
                  Fn::GetAtt:
                    - Table
                    - Arn
          PolicyName: lambda
  S3ObjectLambdaFunction:
    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-scan-on-access-s3-object-lambda.js
          var add_on_scan_on_access_s3_object_lambda_exports = {};
          __export(add_on_scan_on_access_s3_object_lambda_exports, {
            handler: () => handler,
            toID: () => toID
          });
          module.exports = __toCommonJS(add_on_scan_on_access_s3_object_lambda_exports);
          var import_node_stream = require("node:stream");
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_sqs = require("@aws-sdk/client-sqs");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var CACHE_TTL_IN_SECONDS = parseInt(process.env.CACHE_TTL, 10);
          var PASSTROUGH_HEADERS = /* @__PURE__ */ new Set(["x-amz-checksum-mode", "x-amz-request-payer", "x-amz-expected-bucket-owner", "if-match", "if-modified-since", "if-none-match", "if-unmodified-since"]);
          var POLL_TIMEOUT_IN_MILLISECONDS = 1e3 * 55;
          var s3 = new import_client_s3.S3Client({ apiVersion: "2006-03-01" });
          var sqs = new import_client_sqs.SQSClient({ apiVersion: "2012-11-05" });
          var dynamodb = new import_client_dynamodb.DynamoDBClient({ apiVersion: "2012-08-10" });
          function toID(bucket, key, version) {
            if (version) {
              return `s3://${bucket}/${key}?${version}`;
            } else {
              return `s3://${bucket}/${key}`;
            }
          }
          async function respondWithError(outputRoute, outputToken, code, message) {
            await s3.send(new import_client_s3.WriteGetObjectResponseCommand({
              RequestRoute: outputRoute,
              RequestToken: outputToken,
              StatusCode: 403,
              ContentLength: 0,
              ErrorCode: code,
              ErrorMessage: message
            }));
            return { statusCode: 200 };
          }
          async function respondWithObject(outputRoute, outputToken, userRequest, searchParams, inputS3Url) {
            const headers = Object.keys(userRequest.headers).reduce((acc, key) => {
              if (PASSTROUGH_HEADERS.has(key.toLowerCase())) {
                acc[key] = userRequest.headers[key];
              }
              return acc;
            }, {});
            if (userRequest?.headers?.Range) {
              headers.Range = userRequest.headers.Range;
            } else if (searchParams?.has?.("Range")) {
              headers.Range = searchParams.get("Range");
            }
            const res = await fetch(inputS3Url, {
              method: "GET",
              headers
            });
            if (res.status !== 200 && res.status !== 206) {
              console.log("request", inputS3Url);
              console.log("response status", res.status);
              console.log("response", await res.text());
              throw new Error("unexpected status code");
            }
            await s3.send(new import_client_s3.WriteGetObjectResponseCommand({
              RequestRoute: outputRoute,
              RequestToken: outputToken,
              Body: import_node_stream.Readable.fromWeb(res.body)
            }));
            return { statusCode: 200 };
          }
          async function wait(ms) {
            return new Promise((resolve) => setTimeout(resolve, ms));
          }
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const start = Date.now();
            const { userRequest, getObjectContext } = event;
            const { outputRoute, outputToken } = getObjectContext;
            let { inputS3Url } = getObjectContext;
            const { pathname, searchParams } = new URL(userRequest.url);
            const traceId = `bucketav:scan-on-access:${process.env.ADDON_STACK_NAME}:${event.xAmzRequestId}`;
            const object = {
              bucket: process.env.BUCKET_NAME,
              key: pathname.substr(1),
              trace_id: traceId
            };
            if (searchParams.has("versionId")) {
              object.version = searchParams.get("versionId");
            }
            if (CACHE_TTL_IN_SECONDS > 0) {
              const { Item: item } = await dynamodb.send(new import_client_dynamodb.GetItemCommand({
                TableName: process.env.TABLE_NAME,
                Key: {
                  id: { S: toID(object.bucket, object.key, object.version) }
                }
              }));
              if (item !== void 0) {
                const ttl = parseInt(item.ttl.N);
                if (ttl > start / 1e3) {
                  if (item.status.S === "clean") {
                    return respondWithObject(outputRoute, outputToken, userRequest, searchParams, inputS3Url);
                  } else if (item.status.S === "infected") {
                    return respondWithError(outputRoute, outputToken, "BucketAVInfected", "The file is infected.");
                  } else if (item.status.S === "no") {
                    if (item.finding?.S === "bucketav:not-found") {
                      return respondWithError(outputRoute, outputToken, "BucketAVNotFound", "The file is not available.");
                    } else {
                      let message = "The file is unscannable";
                      if (item.finding?.S !== void 0) {
                        message += ` (${item.finding.S})`;
                      }
                      message += ".";
                      return respondWithError(outputRoute, outputToken, "BucketAVUnscannable", message);
                    }
                  }
                }
              }
            }
            await sqs.send(new import_client_sqs.SendMessageCommand({
              MessageBody: JSON.stringify({
                objects: [object]
              }),
              QueueUrl: process.env.SCAN_QUEUE_URL,
              MessageGroupId: `bucketav:scan-on-access:${process.env.ADDON_STACK_NAME.substring(0, 104)}`
              // max 128 chars
            }));
            while (Date.now() - start < POLL_TIMEOUT_IN_MILLISECONDS) {
              const { Item: item } = await dynamodb.send(new import_client_dynamodb.GetItemCommand({
                TableName: process.env.TABLE_NAME,
                Key: {
                  id: { S: traceId }
                }
              }));
              if (item === void 0) {
                await wait(1e3);
                continue;
              } else if (item.status.S === "clean") {
                return respondWithObject(outputRoute, outputToken, userRequest, searchParams, inputS3Url);
              } else if (item.status.S === "infected") {
                return respondWithError(outputRoute, outputToken, "BucketAVInfected", "The file is infected.");
              } else if (item.status.S === "no") {
                if (item.finding?.S === "bucketav:not-found") {
                  return respondWithError(outputRoute, outputToken, "BucketAVNotFound", "The file is not available.");
                } else {
                  let message = "The file is unscannable";
                  if (item.finding?.S !== void 0) {
                    message += ` (${item.finding.S})`;
                  }
                  message += ".";
                  return respondWithError(outputRoute, outputToken, "BucketAVUnscannable", message);
                }
              } else {
                throw new Error(`unexpected status: ${item.status.S}`);
              }
            }
            return respondWithError(outputRoute, outputToken, "BucketAVTimeout", "Scan result timeout.");
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler,
            toID
          });
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
          CACHE_TTL:
            Ref: CacheTTL
          ADDON_STACK_NAME:
            Ref: AWS::StackName
          BUCKET_NAME:
            Ref: BucketName
          SCAN_QUEUE_URL:
            Fn::ImportValue:
              Fn::Join:
                - ""
                - - Ref: BucketAVStackName
                  - -ScanQueueUrl
      Handler: index.handler
      MemorySize: 1769
      ReservedConcurrentExecutions:
        Fn::If:
          - HasS3ObjectLambdaFunctionReservedConcurrentExecutions
          - Ref: S3ObjectLambdaFunctionReservedConcurrentExecutions
          - Ref: AWS::NoValue
      Role:
        Fn::GetAtt:
          - S3ObjectLambdaRole
          - Arn
      Runtime: nodejs22.x
      Timeout: 60
      VpcConfig:
        Fn::If:
          - HasLambdaVpc
          - SecurityGroupIds:
              - Ref: LambdaSecurityGroup
            SubnetIds:
              Ref: LambdaSubnets
          - Ref: AWS::NoValue
    Metadata:
      bucketav::S3ObjectLambdaLambdaVpcAllowPolicy:
        Fn::If:
          - HasLambdaVpc
          - Ref: S3ObjectLambdaLambdaVpcAllowPolicy
          - Ref: AWS::NoValue
  S3ObjectLambdaLambdaVpcAllowPolicy:
    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: S3ObjectLambdaRole
    Condition: HasLambdaVpc
  S3ObjectLambdaLambdaVpcDenyPolicy:
    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:
                      - S3ObjectLambdaFunction
                      - Arn
      PolicyName: vpc-deny
      Roles:
        - Ref: S3ObjectLambdaRole
    Condition: HasLambdaVpc
  S3ObjectLambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV scan-on-access add-on failed. Check logs of AWS Lambda Function "
            - Ref: S3ObjectLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: S3ObjectLambdaFunction
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  S3ObjectLambdaThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: bucketAV scan-on-access add-on throttled.
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value:
            Ref: S3ObjectLambdaFunction
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 360
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  S3ObjectLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /aws/lambda/
            - Ref: S3ObjectLambdaFunction
      RetentionInDays:
        Ref: LogsRetentionInDays
  SupportingAccessPoint:
    Type: AWS::S3::AccessPoint
    Properties:
      Bucket:
        Ref: BucketName
      BucketAccountId:
        Fn::If:
          - HasBucketAccountId
          - Ref: BucketAccountId
          - Ref: AWS::NoValue
  S3ObjectLambdaAccessPoint:
    Type: AWS::S3ObjectLambda::AccessPoint
    Properties:
      ObjectLambdaConfiguration:
        AllowedFeatures:
          - GetObject-Range
        CloudWatchMetricsEnabled: true
        SupportingAccessPoint:
          Fn::GetAtt:
            - SupportingAccessPoint
            - Arn
        TransformationConfigurations:
          - Actions:
              - GetObject
            ContentTransformation:
              AwsLambda:
                FunctionArn:
                  Fn::GetAtt:
                    - S3ObjectLambdaFunction
                    - Arn
  S3ObjectLambdaAccessPoint5xxErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV scan-on-access add-on failed. Check logs of AWS Lambda Function "
            - Ref: S3ObjectLambdaFunction
            - "!"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: AccessPointName
          Value:
            Ref: S3ObjectLambdaAccessPoint
        - Name: LambdaARN
          Value:
            Fn::GetAtt:
              - S3ObjectLambdaFunction
              - Arn
      EvaluationPeriods: 1
      MetricName: 5xxErrors
      Namespace: AWS/S3ObjectLambda
      Period: 60
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
  S3ObjectLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              Fn::GetAtt:
                - S3ObjectLambdaLogGroup
                - Arn
          - Effect: Allow
            Action: s3-object-lambda:WriteGetObjectResponse
            Resource:
              Fn::GetAtt:
                - S3ObjectLambdaAccessPoint
                - Arn
      PolicyName: logs
      Roles:
        - Ref: S3ObjectLambdaRole
  ServiceDiscoveryVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: BucketAVStackName
            - /AddOn/scan-on-access/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 2.9.0
Outputs:
  SubscriptionLambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - SubscriptionLambdaDeadLetterQueue
        - QueueName
  S3ObjectLambdaAccessPointAlias:
    Description: The alias of the Object Lambda Access Point.
    Value:
      Fn::GetAtt:
        - S3ObjectLambdaAccessPoint
        - Alias.Value
  AddOn:
    Description: bucketAV add-on.
    Value: scan-on-access
  Platform:
    Description: bucketAV platform.
    Value: aws
  Version:
    Description: bucketAV add-on version.
    Value: 2.9.0
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

