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);
});
}
}