---
# 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 delete infected files Add-On for Cloudflare R2
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required Parameters
        Parameters:
          - BucketAVStackName
      - Label:
          default: Delete Parameters
        Parameters:
          - SourceBucketNames
          - LogsRetentionInDays
          - IgnoreObjectsExpression
          - IgnoreFindings
      - Label:
          default: Permissions Parameters
        Parameters:
          - 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)
  SourceBucketNames:
    Type: String
    Default: ""
    Description: Optinally, delete 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.
  IgnoreObjectsExpression:
    Type: String
    Default: ^$
    Description: Optionally, ignore objects whose keys match the regular expression. Use ^$ to delete all objects.
  IgnoreFindings:
    Type: CommaDelimitedList
    Default: ""
    Description: Optionally, ignore objects whose finding matches one of the expressions; the * wildcard character matches any sequence of characters (e.g., Win.Test.*,*EICAR*; leave blank to delete objects with any finding).
  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
              - ""
  HasSourceBucketNames:
    Fn::Not:
      - Fn::Equals:
          - Ref: SourceBucketNames
          - ""
  HasPermissionsBoundary:
    Fn::Not:
      - Fn::Equals:
          - Ref: PermissionsBoundary
          - ""
  HasLambdaFunctionReservedConcurrentExecutions:
    Fn::Not:
      - Fn::Equals:
          - Ref: LambdaFunctionReservedConcurrentExecutions
          - 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
  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: ssm:GetParameter
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: BucketAVStackName
                        - /CloudflareR2EndpointHostname
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: BucketAVStackName
                        - /CloudflareAccessKeyId
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ssm:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :parameter/bucketAV/
                        - Ref: BucketAVStackName
                        - /CloudflareAccessKeySecretArn
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  Fn::ImportValue:
                    Fn::Join:
                      - ""
                      - - Ref: BucketAVStackName
                        - -CloudflareAccessKeySecretArn
          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-delete-infected.js
          var add_on_delete_infected_exports = {};
          __export(add_on_delete_infected_exports, {
            handler: () => handler
          });
          module.exports = __toCommonJS(add_on_delete_infected_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/legacy-root-exports/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, "/");
          }
          function compileIgnoreFindings(commaSeparatedPatterns) {
            if (commaSeparatedPatterns === void 0 || commaSeparatedPatterns === "") {
              return [];
            }
            return commaSeparatedPatterns.split(",").filter((pattern) => pattern !== "").map((pattern) => {
              const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
              return new RegExp(`^${escaped}$`);
            });
          }
          function isIgnoredFinding(ignoreFindings, finding) {
            if (finding === void 0) {
              return false;
            }
            return ignoreFindings.some((ignoreFinding) => ignoreFinding.test(finding));
          }
          var MAX_S3_COPY_SIZE = 5 * 1024 * 1024 * 1024;

          // lambda/add-on-delete-infected.js
          var CORE_PLATFORM = process.env.CORE_PLATFORM;
          var IGNORE_OBJECTS_EXPRESSION = new RegExp(process.env.IGNORE_OBJECTS_EXPRESSION);
          var IGNORE_FINDINGS = compileIgnoreFindings(process.env.IGNORE_FINDINGS);
          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 map(record) {
            if (record.Sns.Message.startsWith("{")) {
              const message = JSON.parse(record.Sns.Message);
              return {
                bucket: message.bucket,
                key: message.key,
                saveKey: saveKey(message.key),
                version: message.version,
                finding: message.finding
              };
            } else {
              return {
                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,
                finding: "finding" in record.Sns.MessageAttributes ? record.Sns.MessageAttributes.finding.Value : 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 promises = event.Records.map(map).filter(({ bucket, key, saveKey: saveKey2, finding }) => {
              if (IGNORE_OBJECTS_EXPRESSION.test(saveKey2)) {
                console.log(`Skipping object as it matches the IgnoreObjectsExpression: ${bucket}/${key}`);
                return false;
              }
              if (isIgnoredFinding(IGNORE_FINDINGS, finding)) {
                console.log(`Skipping object as finding ${finding} matches IgnoreFindings: ${bucket}/${key}`);
                return false;
              }
              return true;
            }).map(async ({ bucket, key, version }) => {
              const input = {
                Bucket: bucket,
                Key: key
              };
              if (version !== void 0) {
                input.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(`Delete object: ${bucket}/${key}`);
              return s3.send(new import_client_s33.DeleteObjectCommand(input)).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:
          IGNORE_OBJECTS_EXPRESSION:
            Ref: IgnoreObjectsExpression
          IGNORE_FINDINGS:
            Fn::Join:
              - ","
              - Ref: IgnoreFindings
          CORE_STACK_NAME:
            Ref: BucketAVStackName
          CORE_PLATFORM: cloudflare
      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 delete-infected 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 delete-infected 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 delete-infected 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 delete-infected 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:
        Fn::If:
          - HasSourceBucketNames
          - status:
              - infected
            bucket:
              Fn::Split:
                - ","
                - Ref: SourceBucketNames
          - status:
              - infected
      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/delete-infected/
            - Ref: AWS::StackName
            - /Version
      Type: String
      Value: 1.0.0
Outputs:
  LambdaDeadLetterQueueName:
    Description: The name of the Dead Letter Queue used by the Lambda function.
    Value:
      Fn::GetAtt:
        - LambdaDeadLetterQueue
        - QueueName
  DeleteInfectedRoleArn:
    Description: The ARN of the Delete Infected Role.
    Value:
      Fn::GetAtt:
        - LambdaRole
        - Arn
    Export:
      Name:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -DeleteInfectedRoleArn
  AddOn:
    Description: bucketAV add-on.
    Value: delete-infected
  Platform:
    Description: bucketAV platform.
    Value: cloudflare
  Version:
    Description: bucketAV add-on version.
    Value: 1.0.0
  StackName:
    Description: Stack name.
    Value:
      Ref: AWS::StackName

