---
# 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 Asynchronous HTTPS API Add-On for Amazon S3
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - BucketAVStackName
          - ApiKeys
          - DnsConfiguration
      - Label:
          default: DNS Parameters
        Parameters:
          - HostedZoneId
          - DomainName
      - Label:
          default: API Parameters
        Parameters:
          - LogsRetentionInDays
      - Label:
          default: Permissions Parameters
        Parameters:
          - S3BucketRestriction
          - S3ObjectRestriction
          - KMSKeyRestriction
          - PermissionsBoundary
      - Label:
          default: Lambda Parameters
        Parameters:
          - LambdaFunctionReservedConcurrentExecutions
          - LambdaVpc
          - LambdaSubnets
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)
  ApiKeys:
    Type: String
    Description: API keys for authenticating client requests using Bearer Authentication. Separate API keys with a comma (e.g., key1,key2).
    NoEcho: true
  DnsConfiguration:
    Type: String
    Default: API_GATEWAY
    AllowedValues:
      - AUTO_ROUTE_53
      - API_GATEWAY
    Description: Do you want to configure Route 53 and the Certificate Manager public certificate automatically or do you prefer an API Gateway domain (xxx.execute-api.yyy.amazonaws.com)?
  HostedZoneId:
    Type: String
    Default: ""
    Description: Route 53 public hosted zone ID (required if DnsConfiguration:=AUTO_ROUTE_53).
  DomainName:
    Type: String
    Default: ""
    Description: Domain name added to Route 53 public hosted zone and Certificate Manager public certificate (required if DnsConfiguration:=AUTO_ROUTE_53).
  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.
  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.
Conditions:
  HasLambdaVpc:
    Fn::And:
      - Fn::Not:
          - Fn::Equals:
              - Fn::Join:
                  - ""
                  - Ref: LambdaSubnets
              - ""
      - Fn::Not:
          - Fn::Equals:
              - Ref: LambdaVpc
              - ""
  HasDnsConfigurationAutoRoute53:
    Fn::Equals:
      - Ref: DnsConfiguration
      - AUTO_ROUTE_53
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: LambdaFunctionReservedConcurrentExecutions
          - 0
Resources:
  InternalApiKeySecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      GenerateSecretString:
        ExcludeLowercase: false
        ExcludeNumbers: false
        ExcludePunctuation: true
        ExcludeUppercase: false
        PasswordLength: 32
        RequireEachIncludedType: true
  DownloadUser:
    Type: AWS::IAM::User
    Properties:
      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:
                  - kms:Decrypt
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource:
                  Ref: KMSKeyRestriction
                Condition:
                  StringLike:
                    kms:ViaService: s3.*.amazonaws.com
          PolicyName: s3
  DownloadAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName:
        Ref: DownloadUser
  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 connect 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 connect 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
  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: sqs:SendMessage
                Resource:
                  Fn::ImportValue:
                    Fn::Join:
                      - ""
                      - - Ref: BucketAVStackName
                        - -ScanQueueArn
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                Resource:
                  Fn::GetAtt:
                    - Table
                    - Arn
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  Ref: InternalApiKeySecret
          PolicyName: sqs
  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
  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-api.js
          var add_on_api_exports = {};
          __export(add_on_api_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_api_exports);
          var import_client_sqs = require("@aws-sdk/client-sqs");
          var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
          var import_client_s3 = require("@aws-sdk/client-s3");
          var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
          var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
          var PRESIGNED_URL_EXPIRY_IN_SECONDS = 43200;
          var KEYS = new Set(process.env.API_KEYS.split(","));
          var TRACE_TTL_IN_SECONDS = 24 * 60 * 60;
          var dynamodb = new import_client_dynamodb.DynamoDBClient({ apiVersion: "2012-08-10" });
          var secretsmanager = new import_client_secrets_manager.SecretsManagerClient({ apiVersion: "2017-10-17" });
          var sqs = new import_client_sqs.SQSClient({ apiVersion: "2012-11-05" });
          var s3 = new import_client_s3.S3Client({
            apiVersion: "2006-03-01",
            credentials: {
              accessKeyId: process.env.DOWNLOAD_ACCESS_KEY_ID,
              secretAccessKey: process.env.DOWNLOAD_ACCESS_KEY_SECRET
            }
          });
          function isString(str) {
            return typeof str === "string";
          }
          function isOptionalString(str) {
            return str === void 0 || isString(str);
          }
          function isOptionalObjectWithKeyValues(obj) {
            if (obj === void 0) {
              return true;
            }
            if (typeof obj === "object" && !Array.isArray(obj) && obj !== null) {
              return Object.keys(obj).every((key) => isString(obj[key]));
            }
            return false;
          }
          function isUrl(url) {
            return isString(url) && (url.startsWith("https://") || url.startsWith("http://"));
          }
          function isOptionalUrl(str) {
            return str === void 0 || isUrl(str);
          }
          function r400(error) {
            return {
              isBase64Encoded: false,
              statusCode: 400,
              headers: { "content-type": "application/json" },
              body: JSON.stringify({ error })
            };
          }
          function r405(error) {
            return {
              isBase64Encoded: false,
              statusCode: 405,
              headers: { "content-type": "application/json" },
              body: JSON.stringify({ error })
            };
          }
          var JsonParseError = class extends Error {
            constructor(message) {
              super(message);
              this.name = "JsonParseError";
            }
          };
          function saveJsonParse(json) {
            try {
              return JSON.parse(json);
            } catch (err) {
              throw new JsonParseError("invalid JSON: " + err.message, { cause: err });
            }
          }
          var internalApiKeyRefreshedAt = 0;
          var internalApiKey;
          async function handler(event) {
            console.log(`Invoke: ${JSON.stringify(event)}`);
            const now = Date.now();
            if (now - internalApiKeyRefreshedAt > 36e5) {
              ({ SecretString: internalApiKey } = await secretsmanager.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: process.env.INTERNAL_API_KEY_SECRET_ARN })));
              internalApiKeyRefreshedAt = now;
            }
            if (!(isString(event.headers.authorization) && event.headers.authorization.startsWith("Bearer "))) {
              return {
                isBase64Encoded: false,
                statusCode: 401,
                headers: { "content-type": "application/json" },
                body: JSON.stringify({ error: "Authorization header missing or invalid" })
              };
            }
            const key = event.headers.authorization.substr(7);
            if (!KEYS.has(key) && internalApiKey !== key) {
              return {
                isBase64Encoded: false,
                statusCode: 403,
                headers: { "content-type": "application/json" },
                body: JSON.stringify({ error: "Authorization header wrong" })
              };
            }
            if (event.requestContext.http.path === "/api/v1/scan/async/result") {
              if (event.requestContext.http.method === "GET") {
                if (!isString(event.queryStringParameters?.trace_id)) {
                  return r400("trace_id missing or invalid");
                }
                const { Item: item } = await dynamodb.send(new import_client_dynamodb.GetItemCommand({
                  TableName: process.env.TABLE_NAME,
                  Key: {
                    id: { S: event.queryStringParameters.trace_id }
                  }
                }));
                if (item === void 0) {
                  return {
                    isBase64Encoded: false,
                    statusCode: 404,
                    headers: { "content-type": "application/json" },
                    body: JSON.stringify({ error: "Not found" })
                  };
                } else {
                  const body = {
                    status: item.status.S
                  };
                  if ("finding" in item) {
                    body.finding = item.finding.S;
                  }
                  if ("size" in item) {
                    body.size = parseInt(item.size.N, 10);
                  }
                  if ("realfiletype" in item) {
                    body.realfiletype = item.realfiletype.S;
                  }
                  return {
                    isBase64Encoded: false,
                    statusCode: 200,
                    headers: { "content-type": "application/json" },
                    body: JSON.stringify(body)
                  };
                }
              } else if (event.requestContext.http.method === "POST") {
                try {
                  const body = saveJsonParse(event.body);
                  if (!isString(body.trace_id)) {
                    return r400("trace_id missing or invalid");
                  }
                  if (!isString(body.status)) {
                    return r400("status missing or invalid");
                  }
                  const item = {
                    id: { S: body.trace_id },
                    status: { S: body.status },
                    ttl: { N: (now / 1e3 + TRACE_TTL_IN_SECONDS).toFixed(0) }
                  };
                  if ("finding" in body) {
                    item.finding = { S: body.finding };
                  }
                  if ("size" in body) {
                    item.size = { N: `${body.size}` };
                  }
                  if ("realfiletype" in body) {
                    item.realfiletype = { S: body.realfiletype };
                  }
                  await dynamodb.send(new import_client_dynamodb.PutItemCommand({
                    TableName: process.env.TABLE_NAME,
                    Item: item
                  }));
                  return {
                    isBase64Encoded: false,
                    statusCode: 204
                  };
                } catch (err) {
                  if (err instanceof JsonParseError) {
                    return r400(err.message);
                  } else {
                    throw err;
                  }
                }
              } else {
                return r405("Method not allowed");
              }
            } else if (event.requestContext.http.path === "/api/v1/scan/async/download") {
              if (event.requestContext.http.method === "POST") {
                try {
                  const body = saveJsonParse(event.body);
                  if (!isUrl(body.download_url)) {
                    return r400("download_url missing or invalid");
                  }
                  const download = {
                    download_url: body.download_url
                  };
                  if (!isOptionalObjectWithKeyValues(body.download_headers)) {
                    return r400("download_headers invalid");
                  } else {
                    download.download_headers = body.download_headers;
                  }
                  if (!isOptionalUrl(body.callback_url)) {
                    return r400("callback_url invalid");
                  }
                  if ("callback_url" in body) {
                    download.callback_url = body.callback_url;
                    if (!isOptionalObjectWithKeyValues(body.callback_headers)) {
                      return r400("callback_headers invalid");
                    } else {
                      download.callback_headers = body.callback_headers;
                    }
                    if (!isOptionalString(body.trace_id)) {
                      return r400("trace_id invalid");
                    } else {
                      download.trace_id = body.trace_id;
                    }
                  } else {
                    download.callback_url = `https://${event.requestContext.domainName}/api/v1/scan/async/result`;
                    download.callback_headers = { authorization: `Bearer ${internalApiKey}` };
                    if (!isString(body.trace_id)) {
                      return r400("trace_id missing or invalid");
                    } else {
                      download.trace_id = body.trace_id;
                    }
                  }
                  if (!isOptionalString(body.custom_data)) {
                    return r400("custom_data invalid");
                  } else {
                    download.custom_data = body.custom_data;
                  }
                  await sqs.send(new import_client_sqs.SendMessageCommand({
                    QueueUrl: process.env.SCAN_QUEUE_URL,
                    MessageBody: JSON.stringify({
                      downloads: [download]
                    }),
                    MessageGroupId: `bucketav:api:${process.env.ADDON_STACK_NAME.substring(0, 115)}`
                    // max 128 chars
                  }));
                  if ("callback_url" in body) {
                    return {
                      isBase64Encoded: false,
                      statusCode: 204
                    };
                  } else {
                    return {
                      isBase64Encoded: false,
                      statusCode: 201,
                      headers: { "location": `https://${event.requestContext.domainName}/api/v1/scan/async/result?trace_id=${body.trace_id}` }
                    };
                  }
                } catch (err) {
                  if (err instanceof JsonParseError) {
                    return r400(err.message);
                  } else {
                    throw err;
                  }
                }
              } else {
                return r405("Method not allowed");
              }
            } else if (event.requestContext.http.path === "/api/v1/scan/async/s3") {
              if (event.requestContext.http.method === "POST") {
                try {
                  const body = saveJsonParse(event.body);
                  if (!isString(body.bucket)) {
                    return r400("bucket missing or invalid");
                  }
                  if (!isString(body.key)) {
                    return r400("key missing or invalid");
                  }
                  if (!isOptionalString(body.version)) {
                    return r400("version invalid");
                  }
                  const params = { Bucket: body.bucket, Key: body.key };
                  if (isOptionalString(body.version)) {
                    params.VersionId = body.version;
                  }
                  const getSignedUrlFn = "_getSignedUrl" in event ? event._getSignedUrl : import_s3_request_presigner.getSignedUrl;
                  const downloadUrl = await getSignedUrlFn(s3, new import_client_s3.GetObjectCommand(params), { expiresIn: PRESIGNED_URL_EXPIRY_IN_SECONDS });
                  const download = {
                    download_url: downloadUrl
                  };
                  if (!isOptionalUrl(body.callback_url)) {
                    return r400("callback_url invalid");
                  }
                  if ("callback_url" in body) {
                    download.callback_url = body.callback_url;
                    if (!isOptionalObjectWithKeyValues(body.callback_headers)) {
                      return r400("callback_headers invalid");
                    } else {
                      download.callback_headers = body.callback_headers;
                    }
                    if (!isOptionalString(body.trace_id)) {
                      return r400("trace_id invalid");
                    } else {
                      download.trace_id = body.trace_id;
                    }
                  } else {
                    download.callback_url = `https://${event.requestContext.domainName}/api/v1/scan/async/result`;
                    download.callback_headers = { authorization: `Bearer ${internalApiKey}` };
                    if (!isString(body.trace_id)) {
                      return r400("trace_id missing or invalid");
                    } else {
                      download.trace_id = body.trace_id;
                    }
                  }
                  if (!isOptionalString(body.custom_data)) {
                    return r400("custom_data invalid");
                  } else {
                    download.custom_data = body.custom_data;
                  }
                  await sqs.send(new import_client_sqs.SendMessageCommand({
                    QueueUrl: process.env.SCAN_QUEUE_URL,
                    MessageBody: JSON.stringify({
                      downloads: [download]
                    }),
                    MessageGroupId: `bucketav:api:${process.env.ADDON_STACK_NAME.substring(0, 115)}`
                    // max 128 chars
                  }));
                  if ("callback_url" in body) {
                    return {
                      isBase64Encoded: false,
                      statusCode: 204
                    };
                  } else {
                    return {
                      isBase64Encoded: false,
                      statusCode: 201,
                      headers: { "location": `https://${event.requestContext.domainName}/api/v1/scan/async/result?trace_id=${body.trace_id}` }
                    };
                  }
                } catch (err) {
                  if (err instanceof JsonParseError) {
                    return r400(err.message);
                  } else {
                    throw err;
                  }
                }
              } else {
                return r405("Method not allowed");
              }
            } else if (event.requestContext.http.path === "/api/v1/test") {
              if (event.requestContext.http.method === "GET") {
                return {
                  isBase64Encoded: false,
                  statusCode: 204
                };
              } else {
                return r405("Method not allowed");
              }
            } else {
              return {
                isBase64Encoded: false,
                statusCode: 404,
                headers: { "content-type": "application/json" },
                body: JSON.stringify({ error: "Not found" })
              };
            }
          }
          // Annotate the CommonJS export names for ESM import in node:
          0 && (module.exports = {
            handler
          });
      Environment:
        Variables:
          SCAN_QUEUE_URL:
            Fn::ImportValue:
              Fn::Join:
                - ""
                - - Ref: BucketAVStackName
                  - -ScanQueueUrl
          API_KEYS:
            Ref: ApiKeys
          INTERNAL_API_KEY_SECRET_ARN:
            Ref: InternalApiKeySecret
          DOWNLOAD_ACCESS_KEY_ID:
            Ref: DownloadAccessKey
          DOWNLOAD_ACCESS_KEY_SECRET:
            Fn::GetAtt:
              - DownloadAccessKey
              - SecretAccessKey
          TABLE_NAME:
            Ref: Table
          ADDON_STACK_NAME:
            Ref: AWS::StackName
      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
  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
  LambdaErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription:
        Fn::Join:
          - ""
          - - "bucketAV api 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 api 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
  Api:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name:
        Ref: AWS::StackName
      ProtocolType: HTTP
      Target:
        Fn::Join:
          - ""
          - - "arn:"
            - Ref: AWS::Partition
            - ":apigateway:"
            - Ref: AWS::Region
            - :lambda:path/2015-03-31/functions/
            - Fn::GetAtt:
                - LambdaFunction
                - Arn
            - /invocations
    DependsOn:
      - LambdaPolicy
  Alarm5XXErrorTooHigh:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Fn::ImportValue:
            Fn::Join:
              - ""
              - - Ref: BucketAVStackName
                - -InfrastructureAlarmsTopicArn
      AlarmDescription: Api Gateway server-side errors captured
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: ApiId
          Value:
            Ref: Api
        - Name: Stage
          Value: $default
      EvaluationPeriods: 1
      MetricName: 5xx
      Namespace: AWS/ApiGateway
      Period: 60
      Statistic: Sum
      Threshold: 0
      TreatMissingData: notBreaching
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: LambdaFunction
      Principal: apigateway.amazonaws.com
      SourceArn:
        Fn::Join:
          - ""
          - - "arn:"
            - Ref: AWS::Partition
            - ":execute-api:"
            - Ref: AWS::Region
            - ":"
            - Ref: AWS::AccountId
            - ":"
            - Ref: Api
            - /*/$default
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      CertificateTransparencyLoggingPreference: DISABLED
      DomainName:
        Ref: DomainName
      DomainValidationOptions:
        - DomainName:
            Ref: DomainName
          HostedZoneId:
            Ref: HostedZoneId
      ValidationMethod: DNS
    Condition: HasDnsConfigurationAutoRoute53
  ApiDomainName:
    Type: AWS::ApiGatewayV2::DomainName
    Properties:
      DomainName:
        Ref: DomainName
      DomainNameConfigurations:
        - CertificateArn:
            Ref: Certificate
          EndpointType: REGIONAL
          SecurityPolicy: TLS_1_2
    Condition: HasDnsConfigurationAutoRoute53
  RecordSet:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        DNSName:
          Fn::GetAtt:
            - ApiDomainName
            - RegionalDomainName
        HostedZoneId:
          Fn::GetAtt:
            - ApiDomainName
            - RegionalHostedZoneId
      HostedZoneId:
        Ref: HostedZoneId
      Name:
        Ref: DomainName
      Type: A
    Condition: HasDnsConfigurationAutoRoute53
  ApiMapping:
    Type: AWS::ApiGatewayV2::ApiMapping
    Properties:
      ApiId:
        Ref: Api
      DomainName:
        Ref: ApiDomainName
      Stage: $default
    Condition: HasDnsConfigurationAutoRoute53
  ServiceDiscoveryVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Description: bucketAV managed value. DO NOT CHANGE!
      Name:
        Fn::Join:
          - ""
          - - /bucketAV/
            - Ref: BucketAVStackName
            - /AddOn/api/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 2.10.0
Outputs:
  ApiEndpoint:
    Description: HTTPS API endpoint.
    Value:
      Fn::If:
        - HasDnsConfigurationAutoRoute53
        - Fn::Join:
            - ""
            - - https://
              - Ref: DomainName
        - Fn::GetAtt:
            - Api
            - ApiEndpoint
  AddOn:
    Description: bucketAV add-on.
    Value: api
  Platform:
    Description: bucketAV platform.
    Value: aws
  Version:
    Description: bucketAV add-on version.
    Value: 2.10.0
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

