constructor()

in src/index.ts [176:530]


  constructor(scope: Construct, id: string, props: ServerlessClamscanProps) {
    super(scope, id);

    if (!props.onResult) {
      this.resultBus = new EventBus(this, 'ScanResultBus');
      this.resultDest = new EventBridgeDestination(this.resultBus);
      this.infectedRule = new Rule(this, 'InfectedRule', {
        eventBus: this.resultBus,
        description: 'Event for when a file is marked INFECTED',
        eventPattern: {
          detail: {
            responsePayload: {
              source: ['serverless-clamscan'],
              status: ['INFECTED'],
            },
          },
        },
      });
      this.cleanRule = new Rule(this, 'CleanRule', {
        eventBus: this.resultBus,
        description: 'Event for when a file is marked CLEAN',
        eventPattern: {
          detail: {
            responsePayload: {
              source: ['serverless-clamscan'],
              status: ['CLEAN'],
            },
          },
        },
      });
    } else {
      this.resultDest = props.onResult;
    }

    if (!props.onError) {
      this.errorDeadLetterQueue = new Queue(this, 'ScanErrorDeadLetterQueue', {
        encryption: QueueEncryption.KMS_MANAGED,
      });
      this.errorQueue = new Queue(this, 'ScanErrorQueue', {
        encryption: QueueEncryption.KMS_MANAGED,
        deadLetterQueue: {
          maxReceiveCount: 3,
          queue: this.errorDeadLetterQueue,
        },
      });
      this.errorDest = new SqsDestination(this.errorQueue);
      const cfnDlq = this.errorDeadLetterQueue.node.defaultChild as CfnQueue;
      cfnDlq.addMetadata('cdk_nag', {
        rules_to_suppress: [
          { id: 'AwsSolutions-SQS3', reason: 'This queue is a DLQ.' },
        ],
      });
    } else {
      this.errorDest = props.onError;
    }

    const vpc = new Vpc(this, 'ScanVPC', {
      subnetConfiguration: [
        {
          subnetType: SubnetType.PRIVATE_ISOLATED,
          name: 'Isolated',
        },
      ],
    });

    vpc.addFlowLog('FlowLogs');

    this._s3Gw = vpc.addGatewayEndpoint('S3Endpoint', {
      service: GatewayVpcEndpointAwsService.S3,
    });

    const fileSystem = new FileSystem(this, 'ScanFileSystem', {
      vpc: vpc,
      encrypted: props.efsEncryption === false ? false : true,
      lifecyclePolicy: LifecyclePolicy.AFTER_7_DAYS,
      performanceMode: PerformanceMode.GENERAL_PURPOSE,
      removalPolicy: RemovalPolicy.DESTROY,
      securityGroup: new SecurityGroup(this, 'ScanFileSystemSecurityGroup', {
        vpc: vpc,
        allowAllOutbound: false,
      }),
    });

    const lambda_ap = fileSystem.addAccessPoint('ScanLambdaAP', {
      createAcl: {
        ownerGid: '1000',
        ownerUid: '1000',
        permissions: '755',
      },
      posixUser: {
        gid: '1000',
        uid: '1000',
      },
      path: this._efsRootPath,
    });

    const logs_bucket = props.defsBucketAccessLogsConfig?.logsBucket;
    const logs_bucket_prefix = props.defsBucketAccessLogsConfig?.logsPrefix;
    if (logs_bucket === true || logs_bucket === undefined) {
      this.defsAccessLogsBucket = new Bucket(
        this,
        'VirusDefsAccessLogsBucket',
        {
          encryption: BucketEncryption.S3_MANAGED,
          removalPolicy: RemovalPolicy.RETAIN,
          serverAccessLogsPrefix: 'access-logs-bucket-logs',
          blockPublicAccess: {
            blockPublicAcls: true,
            blockPublicPolicy: true,
            ignorePublicAcls: true,
            restrictPublicBuckets: true,
          },
        },
      );
      this.defsAccessLogsBucket.addToResourcePolicy(
        new PolicyStatement({
          effect: Effect.DENY,
          actions: ['s3:*'],
          resources: [
            this.defsAccessLogsBucket.arnForObjects('*'),
            this.defsAccessLogsBucket.bucketArn,
          ],
          principals: [new AnyPrincipal()],
          conditions: {
            Bool: {
              'aws:SecureTransport': false,
            },
          },
        }),
      );
    } else if (logs_bucket != false) {
      this.defsAccessLogsBucket = logs_bucket;
    }

    const defs_bucket = new Bucket(this, 'VirusDefsBucket', {
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      serverAccessLogsBucket: this.defsAccessLogsBucket,
      serverAccessLogsPrefix:
        logs_bucket === false ? undefined : logs_bucket_prefix,
      blockPublicAccess: {
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
      },
    });

    defs_bucket.addToResourcePolicy(
      new PolicyStatement({
        effect: Effect.DENY,
        actions: ['s3:*'],
        resources: [defs_bucket.arnForObjects('*'), defs_bucket.bucketArn],
        principals: [new AnyPrincipal()],
        conditions: {
          Bool: {
            'aws:SecureTransport': false,
          },
        },
      }),
    );
    defs_bucket.addToResourcePolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ['s3:GetObject', 's3:ListBucket'],
        resources: [defs_bucket.arnForObjects('*'), defs_bucket.bucketArn],
        principals: [new AnyPrincipal()],
        conditions: {
          StringEquals: {
            'aws:SourceVpce': this._s3Gw.vpcEndpointId,
          },
        },
      }),
    );
    defs_bucket.addToResourcePolicy(
      new PolicyStatement({
        effect: Effect.DENY,
        actions: ['s3:PutBucketPolicy', 's3:DeleteBucketPolicy'],
        resources: [defs_bucket.bucketArn],
        notPrincipals: [new AccountRootPrincipal()],
      }),
    );
    this._s3Gw.addToPolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ['s3:GetObject', 's3:ListBucket'],
        resources: [defs_bucket.arnForObjects('*'), defs_bucket.bucketArn],
        principals: [new AnyPrincipal()],
      }),
    );

    this._scanFunction = new DockerImageFunction(this, 'ServerlessClamscan', {
      code: DockerImageCode.fromImageAsset(
        path.join(__dirname, '../assets/lambda/code/scan'),
        {
          buildArgs: {
            // Only force update the docker layer cache once a day
            CACHE_DATE: new Date().toDateString(),
          },
          extraHash: Date.now().toString(),
        },
      ),
      onSuccess: this.resultDest,
      onFailure: this.errorDest,
      filesystem: LambdaFileSystem.fromEfsAccessPoint(
        lambda_ap,
        this._efsMountPath,
      ),
      vpc: vpc,
      vpcSubnets: { subnets: vpc.isolatedSubnets },
      allowAllOutbound: false,
      timeout: Duration.minutes(15),
      memorySize: 10240,
      environment: {
        EFS_MOUNT_PATH: this._efsMountPath,
        EFS_DEF_PATH: this._efsDefsPath,
        DEFS_URL: defs_bucket.virtualHostedUrlForObject(),
        POWERTOOLS_METRICS_NAMESPACE: 'serverless-clamscan',
        POWERTOOLS_SERVICE_NAME: 'virus-scan',
      },
    });
    if (this._scanFunction.role) {
      const cfnScanRole = this._scanFunction.role.node.defaultChild as CfnRole;
      cfnScanRole.addMetadata('cdk_nag', {
        rules_to_suppress: [
          {
            id: 'AwsSolutions-IAM4',
            reason:
              'The AWSLambdaBasicExecutionRole does not provide permissions beyond uploading logs to CloudWatch. The AWSLambdaVPCAccessExecutionRole is required for functions with VPC access to manage elastic network interfaces.',
          },
        ],
      });
      const cfnScanRoleChildren = this._scanFunction.role.node.children;
      for (const child of cfnScanRoleChildren) {
        const resource = child.node.defaultChild as CfnResource;
        if (resource != undefined && resource.cfnResourceType == 'AWS::IAM::Policy') {
          resource.addMetadata('cdk_nag', {
            rules_to_suppress: [
              {
                id: 'AwsSolutions-IAM5',
                reason:
                  'The EFS mount point permissions are controlled through a condition which limit the scope of the * resources.',
              },
            ],
          });
        }
      }
    }
    this._scanFunction.connections.allowToAnyIpv4(
      Port.tcp(443),
      'Allow outbound HTTPS traffic for S3 access.',
    );
    defs_bucket.grantRead(this._scanFunction);

    const download_defs = new DockerImageFunction(this, 'DownloadDefs', {
      code: DockerImageCode.fromImageAsset(
        path.join(__dirname, '../assets/lambda/code/download_defs'),
        {
          buildArgs: {
            // Only force update the docker layer cache once a day
            CACHE_DATE: new Date().toDateString(),
          },
          extraHash: Date.now().toString(),
        },
      ),
      timeout: Duration.minutes(5),
      memorySize: 1024,
      environment: {
        DEFS_BUCKET: defs_bucket.bucketName,
        POWERTOOLS_SERVICE_NAME: 'freshclam-update',
      },
    });
    const stack = Stack.of(this);

    if (download_defs.role) {
      const download_defs_role = `arn:${stack.partition}:sts::${stack.account}:assumed-role/${download_defs.role.roleName}/${download_defs.functionName}`;
      const download_defs_assumed_principal = new ArnPrincipal(
        download_defs_role,
      );
      defs_bucket.addToResourcePolicy(
        new PolicyStatement({
          effect: Effect.DENY,
          actions: ['s3:PutObject*'],
          resources: [defs_bucket.arnForObjects('*')],
          notPrincipals: [download_defs.role, download_defs_assumed_principal],
        }),
      );
      defs_bucket.grantReadWrite(download_defs);
      const cfnDownloadRole = download_defs.role.node.defaultChild as CfnRole;
      cfnDownloadRole.addMetadata('cdk_nag', {
        rules_to_suppress: [
          {
            id: 'AwsSolutions-IAM4',
            reason:
              'The AWSLambdaBasicExecutionRole does not provide permissions beyond uploading logs to CloudWatch.',
          },
        ],
      });
      const cfnDownloadRoleChildren = download_defs.role.node.children;
      for (const child of cfnDownloadRoleChildren) {
        const resource = child.node.defaultChild as CfnResource;
        if (resource != undefined && resource.cfnResourceType == 'AWS::IAM::Policy') {
          resource.addMetadata('cdk_nag', {
            rules_to_suppress: [
              {
                id: 'AwsSolutions-IAM5',
                reason:
                  'The function is allowed to perform operations on all prefixes in the specified bucket.',
              },
            ],
          });
        }
      }
    }

    new Rule(this, 'VirusDefsUpdateRule', {
      schedule: Schedule.rate(Duration.hours(12)),
      targets: [new LambdaFunction(download_defs)],
    });

    const init_defs_cr = new Function(this, 'InitDefs', {
      runtime: Runtime.PYTHON_3_8,
      code: Code.fromAsset(
        path.join(__dirname, '../assets/lambda/code/initialize_defs_cr'),
      ),
      handler: 'lambda.lambda_handler',
      timeout: Duration.minutes(5),
    });
    download_defs.grantInvoke(init_defs_cr);
    if (init_defs_cr.role) {
      const cfnScanRole = init_defs_cr.role.node.defaultChild as CfnRole;
      cfnScanRole.addMetadata('cdk_nag', {
        rules_to_suppress: [
          {
            id: 'AwsSolutions-IAM4',
            reason:
              'The AWSLambdaBasicExecutionRole does not provide permissions beyond uploading logs to CloudWatch.',
          },
        ],
      });
    }
    new CustomResource(this, 'InitDefsCr', {
      serviceToken: init_defs_cr.functionArn,
      properties: {
        FnName: download_defs.functionName,
      },
    });

    if (props.buckets) {
      props.buckets.forEach((bucket) => {
        this.addSourceBucket(bucket);
      });
    }
  }