@medplum/cdk 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- "use strict";var $=Object.defineProperty;var ae=Object.getOwnPropertyDescriptor;var re=Object.getOwnPropertyNames;var oe=Object.prototype.hasOwnProperty;var ne=(i,s)=>{for(var e in s)$(i,e,{get:s[e],enumerable:!0})},se=(i,s,e,r)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of re(s))!oe.call(i,a)&&a!==e&&$(i,a,{get:()=>s[a],enumerable:!(r=ae(s,a))||r.enumerable});return i};var ie=i=>se($({},"__esModule",{value:!0}),i);var ce={};ne(ce,{main:()=>K});module.exports=ie(ce);var y=require("aws-cdk-lib"),V=require("fs"),z=require("path");var t=require("aws-cdk-lib"),O=require("aws-cdk-lib/aws-ecr"),I=require("aws-cdk-lib/aws-rds"),M=require("constructs");var v=[{name:"AWS-AWSManagedRulesCommonRuleSet",priority:10,statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesCommonRuleSet",excludedRules:[{name:"NoUserAgent_HEADER"},{name:"UserAgent_BadBots_HEADER"},{name:"SizeRestrictions_QUERYSTRING"},{name:"SizeRestrictions_Cookie_HEADER"},{name:"SizeRestrictions_BODY"},{name:"SizeRestrictions_URIPATH"},{name:"EC2MetaDataSSRF_BODY"},{name:"EC2MetaDataSSRF_COOKIE"},{name:"EC2MetaDataSSRF_URIPATH"},{name:"EC2MetaDataSSRF_QUERYARGUMENTS"},{name:"GenericLFI_QUERYARGUMENTS"},{name:"GenericLFI_URIPATH"},{name:"GenericLFI_BODY"},{name:"RestrictedExtensions_URIPATH"},{name:"RestrictedExtensions_QUERYARGUMENTS"},{name:"GenericRFI_QUERYARGUMENTS"},{name:"GenericRFI_BODY"},{name:"GenericRFI_URIPATH"},{name:"CrossSiteScripting_COOKIE"},{name:"CrossSiteScripting_QUERYARGUMENTS"},{name:"CrossSiteScripting_BODY"},{name:"CrossSiteScripting_URIPATH"}]}},overrideAction:{count:{}},visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWS-AWSManagedRulesCommonRuleSet"}},{name:"AWS-AWSManagedRulesAmazonIpReputationList",priority:20,statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesAmazonIpReputationList",excludedRules:[{name:"AWSManagedIPReputationList"},{name:"AWSManagedReconnaissanceList"}]}},overrideAction:{count:{}},visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRulesAmazonIpReputationList"}},{name:"AWSManagedRulesSQLiRuleSet",priority:30,visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRulesSQLiRuleSet"},overrideAction:{count:{}},statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesSQLiRuleSet",excludedRules:[{name:"SQLi_QUERYARGUMENTS"},{name:"SQLiExtendedPatterns_QUERYARGUMENTS"},{name:"SQLi_BODY"},{name:"SQLiExtendedPatterns_BODY"},{name:"SQLi_COOKIE"},{name:"SQLi_URIPATH"}]}}},{name:"AWSManagedRuleLinux",priority:40,visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRuleLinux"},overrideAction:{count:{}},statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesLinuxRuleSet",excludedRules:[{name:"LFI_URIPATH"},{name:"LFI_QUERYSTRING"},{name:"LFI_COOKIE"}]}}}];var C=class extends M.Construct{constructor(s,e){super(s,"BackEnd");let r=e.name,a;if(e.vpcId)a=t.aws_ec2.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let u=new t.aws_logs.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+r,removalPolicy:t.RemovalPolicy.DESTROY});a=new t.aws_ec2.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:t.aws_ec2.FlowLogDestination.toCloudWatchLogs(u),trafficType:t.aws_ec2.FlowLogTrafficType.ALL}}})}let l=new t.aws_iam.Role(this,"BotLambdaRole",{assumedBy:new t.aws_iam.ServicePrincipal("lambda.amazonaws.com")}),c,d=e.rdsSecretsArn;if(!d){let u={instanceType:e.rdsInstanceType?new t.aws_ec2.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},E;if(e.rdsInstances>1){E=[];for(let L=0;L<e.rdsInstances-1;L++)E.push(I.ClusterInstance.provisioned("Instance"+(L+2),{...u}))}c=new t.aws_rds.DatabaseCluster(this,"DatabaseCluster",{engine:t.aws_rds.DatabaseClusterEngine.auroraPostgres({version:t.aws_rds.AuroraPostgresEngineVersion.VER_12_9}),credentials:t.aws_rds.Credentials.fromGeneratedSecret("clusteradmin"),defaultDatabaseName:"medplum",storageEncrypted:!0,vpc:a,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},writer:I.ClusterInstance.provisioned("Instance1",{...u}),readers:E,backup:{retention:t.Duration.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:t.aws_rds.InstanceUpdateBehaviour.ROLLING}),d=c.secret.secretArn}let A=new t.aws_elasticache.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:a.privateSubnets.map(u=>u.subnetId)}),S=new t.aws_ec2.SecurityGroup(this,"RedisSecurityGroup",{vpc:a,description:"Redis Security Group",allowAllOutbound:!1}),g=new t.aws_secretsmanager.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),p=new t.aws_elasticache.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:g.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:A.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[S.securityGroupId]});p.node.addDependency(g);let R=new t.aws_secretsmanager.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:p.attrPrimaryEndPointAddress,port:p.attrPrimaryEndPointPort,password:g.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}});R.node.addDependency(g),R.node.addDependency(p);let Y=new t.aws_ecs.Cluster(this,"Cluster",{vpc:a}),Q=new t.aws_iam.PolicyDocument({statements:[new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["logs:CreateLogStream","logs:PutLogEvents"],resources:["arn:aws:logs:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["secretsmanager:GetResourcePolicy","secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecrets","secretsmanager:ListSecretVersionIds"],resources:["arn:aws:secretsmanager:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["ssm:GetParametersByPath","ssm:GetParameters","ssm:GetParameter","ssm:DescribeParameters"],resources:["arn:aws:ssm:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["ses:SendEmail","ses:SendRawEmail"],resources:["arn:aws:ses:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["s3:ListBucket","s3:GetObject","s3:PutObject","s3:DeleteObject"],resources:["arn:aws:s3:::*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["iam:ListRoles","iam:GetRole","iam:PassRole"],resources:[l.roleArn]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["lambda:CreateFunction","lambda:GetFunction","lambda:GetFunctionConfiguration","lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration","lambda:ListLayerVersions","lambda:GetLayerVersion","lambda:InvokeFunction"],resources:["arn:aws:lambda:*"]})]}),j=new t.aws_iam.Role(this,"TaskExecutionRole",{assumedBy:new t.aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),description:"Medplum Server Task Execution Role",inlinePolicies:{TaskExecutionPolicies:Q}}),k=new t.aws_ecs.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:j}),q=new t.aws_logs.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+r,removalPolicy:t.RemovalPolicy.DESTROY}),G=new t.aws_ecs.AwsLogDriver({logGroup:q,streamPrefix:"Medplum"});if(k.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${r}/`:`aws:${e.region}:/medplum/${r}/`],logging:G}).addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let u of e.additionalContainers)k.addContainer("AdditionalContainer-"+u.name,{containerName:u.name,image:this.getContainerImage(e,u.image),command:u.command,environment:u.environment,logging:G});let T=new t.aws_ec2.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:a}),b=new t.aws_ecs.FargateService(this,"FargateService",{cluster:Y,taskDefinition:k,assignPublicIp:!1,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[T],healthCheckGracePeriod:t.Duration.minutes(5)});c&&b.node.addDependency(c),b.node.addDependency(p);let Z=new t.aws_elasticloadbalancingv2.ApplicationTargetGroup(this,"TargetGroup",{vpc:a,port:e.apiPort,protocol:t.aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,healthCheck:{path:"/healthcheck",interval:t.Duration.seconds(30),timeout:t.Duration.seconds(3),healthyThresholdCount:2,unhealthyThresholdCount:5},targets:[b]}),w=new t.aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:a,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0});e.loadBalancerLoggingBucket&&w.logAccessLogs(t.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),w.addListener("HttpsListener",{port:443,certificates:[{certificateArn:e.apiSslCertArn}],sslPolicy:t.aws_elasticloadbalancingv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,defaultAction:t.aws_elasticloadbalancingv2.ListenerAction.forward([Z])});let B=new t.aws_wafv2.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:v,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),J=new t.aws_wafv2.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:w.loadBalancerArn,webAclArn:B.attrArn});c&&c.connections.allowDefaultPortFrom(T),S.addIngressRule(T,t.aws_ec2.Port.tcp(6379));let _;if(!e.skipDns){let u=t.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});_=new t.aws_route53.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:t.aws_route53.RecordTarget.fromAlias(new t.aws_route53_targets.LoadBalancerTarget(w)),zone:u})}let X=new t.aws_ssm.StringParameter(this,"DatabaseSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${r}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:d}),ee=new t.aws_ssm.StringParameter(this,"RedisSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${r}/RedisSecrets`,description:"Redis secrets ARN",stringValue:R.secretArn}),te=new t.aws_ssm.StringParameter(this,"BotLambdaRoleParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${r}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:l.roleArn});console.log("ARecord",_?.domainName),console.log("DatabaseSecretsParameter",X.parameterArn),console.log("RedisSecretsParameter",ee.parameterArn),console.log("RedisCluster",p.attrPrimaryEndPointAddress),console.log("BotLambdaRole",te.stringValue),console.log("WAF",B.attrArn),console.log("WAF Association",J.node.id)}getContainerImage(s,e){let a=new RegExp(`^${s.accountNumber}\\.dkr\\.ecr\\.${s.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),l=a?.[1],c=a?.[2];if(l&&c){let d=O.Repository.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${s.region}:${s.accountNumber}:repository/${l}`);return t.aws_ecs.ContainerImage.fromEcrRepository(d,c)}return t.aws_ecs.ContainerImage.fromRegistry(e)}};var o=require("aws-cdk-lib"),W=require("constructs");var F=require("aws-cdk-lib");function P(i,s){let e=new F.aws_iam.PolicyStatement;e.addActions("s3:GetObject*"),e.addActions("s3:GetBucket*"),e.addActions("s3:List*"),e.addResources(i.bucketArn),e.addResources(`${i.bucketArn}/*`),e.addCanonicalUserPrincipal(s.cloudFrontOriginAccessIdentityS3CanonicalUserId),i.addToResourcePolicy(e)}var N=class extends W.Construct{constructor(s,e,r){super(s,"FrontEnd");let a;if(r===e.region?a=new o.aws_s3.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:o.aws_s3.BlockPublicAccess.BLOCK_ALL,removalPolicy:o.RemovalPolicy.DESTROY,encryption:o.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):a=o.aws_s3.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),r==="us-east-1"){let l=new o.aws_cloudfront.ResponseHeadersPolicy(this,"ResponseHeadersPolicy",{securityHeadersBehavior:{contentSecurityPolicy:{contentSecurityPolicy:["default-src 'none'","base-uri 'self'","child-src 'self'",`connect-src 'self' ${e.apiDomainName} *.google.com`,"font-src 'self' fonts.gstatic.com","form-action 'self' *.gstatic.com *.google.com","frame-ancestors 'none'","frame-src 'self' *.medplum.com *.gstatic.com *.google.com",`img-src 'self' data: ${e.storageDomainName} *.gstatic.com *.google.com *.googleapis.com`,"manifest-src 'self'",`media-src 'self' ${e.storageDomainName}`,"script-src 'self' *.medplum.com *.gstatic.com *.google.com","style-src 'self' 'unsafe-inline' *.medplum.com *.gstatic.com *.google.com","worker-src 'self' blob: *.gstatic.com *.google.com","upgrade-insecure-requests"].join("; "),override:!0},contentTypeOptions:{override:!0},frameOptions:{frameOption:o.aws_cloudfront.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:o.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),c=new o.aws_wafv2.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:v,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),d=new o.aws_cloudfront.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:o.aws_cloudfront.CacheCookieBehavior.all(),headerBehavior:o.aws_cloudfront.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:o.aws_cloudfront.CacheQueryStringBehavior.all()}),A=new o.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});P(a,A);let S=new o.aws_cloudfront.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new o.aws_cloudfront_origins.S3Origin(a,{originAccessIdentity:A}),responseHeadersPolicy:l,viewerProtocolPolicy:o.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new o.aws_cloudfront_origins.HttpOrigin(e.apiDomainName),allowedMethods:o.aws_cloudfront.AllowedMethods.ALLOW_ALL,cachePolicy:d,viewerProtocolPolicy:o.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:o.aws_certificatemanager.Certificate.fromCertificateArn(this,"AppCertificate",e.appSslCertArn),domainNames:[e.appDomainName],errorResponses:[{httpStatus:403,responseHttpStatus:200,responsePagePath:"/index.html"},{httpStatus:404,responseHttpStatus:200,responsePagePath:"/index.html"}],webAclId:c.attrArn,logBucket:e.appLoggingBucket?o.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),g;if(!e.skipDns){let p=o.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});g=new o.aws_route53.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:o.aws_route53.RecordTarget.fromAlias(new o.aws_route53_targets.CloudFrontTarget(S)),zone:p})}console.log("ARecord",g?.domainName)}}};var n=require("aws-cdk-lib"),x=require("cdk-serverless-clamscan"),U=require("constructs");var h=class extends U.Construct{constructor(s,e,r){super(s,"Storage");let a;if(r===e.region?(a=new n.aws_s3.Bucket(this,"StorageBucket",{bucketName:e.storageBucketName,publicReadAccess:!1,blockPublicAccess:n.aws_s3.BlockPublicAccess.BLOCK_ALL,encryption:n.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}),e.clamscanEnabled&&new x.ServerlessClamscan(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:n.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(a)):a=n.aws_s3.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),r==="us-east-1"){let l;e.signingKeyId?l=n.aws_cloudfront.PublicKey.fromPublicKeyId(this,"StoragePublicKey",e.signingKeyId):l=new n.aws_cloudfront.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey});let c=new n.aws_cloudfront.KeyGroup(this,"StorageKeyGroup",{items:[l]}),d=new n.aws_cloudfront.ResponseHeadersPolicy(this,"ResponseHeadersPolicy",{securityHeadersBehavior:{contentSecurityPolicy:{contentSecurityPolicy:"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors *.medplum.com;",override:!0},contentTypeOptions:{override:!0},frameOptions:{frameOption:n.aws_cloudfront.HeadersFrameOption.DENY,override:!0},referrerPolicy:{referrerPolicy:n.aws_cloudfront.HeadersReferrerPolicy.NO_REFERRER,override:!0},strictTransportSecurity:{accessControlMaxAge:n.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),A=new n.aws_wafv2.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:v,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),S=new n.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});P(a,S);let g=new n.aws_cloudfront.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new n.aws_cloudfront_origins.S3Origin(a,{originAccessIdentity:S}),responseHeadersPolicy:d,viewerProtocolPolicy:n.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[c]},certificate:n.aws_certificatemanager.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:A.attrArn,logBucket:e.storageLoggingBucket?n.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),p;if(!e.skipDns){let R=n.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});p=new n.aws_route53.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:n.aws_route53.RecordTarget.fromAlias(new n.aws_route53_targets.CloudFrontTarget(g)),zone:R})}console.log("ARecord",p?.domainName)}}};var m=require("aws-cdk-lib"),H=require("constructs"),f=class extends H.Construct{constructor(e,r){super(e,"CloudTrailAlarms");if(this.config=r,!r.cloudTrailAlarms)return;r.cloudTrailAlarms.logGroupCreate?(this.logGroup=new m.aws_logs.LogGroup(this,"CloudTrailLogGroup",{logGroupName:r.cloudTrailAlarms.logGroupName,retention:m.aws_logs.RetentionDays.ONE_YEAR}),this.cloudTrail=new m.aws_cloudtrail.Trail(this,"CloudTrail",{sendToCloudWatchLogs:!0,cloudWatchLogGroup:this.logGroup,includeGlobalServiceEvents:!0})):this.logGroup=m.aws_logs.LogGroup.fromLogGroupName(this,"CloudTrailLogGroup",r.cloudTrailAlarms.logGroupName),r.cloudTrailAlarms.snsTopicArn?this.alarmTopic=m.aws_sns.Topic.fromTopicArn(this,"AlarmTopic",r.cloudTrailAlarms.snsTopicArn):this.alarmTopic=new m.aws_sns.Topic(this,"AlarmTopic",{topicName:r.cloudTrailAlarms.snsTopicName});let a=[["UnauthorizedApiCalls","{ ($.errorCode = *UnauthorizedOperation) || ($.errorCode = AccessDenied*) }"],["SignInWithoutMfa","{ ($.eventName = ConsoleLogin) && ($.additionalEventData.MFAUsed != Yes) }"],["RootAccountUsage","{ $.userIdentity.type = Root && $.userIdentity.invokedBy NOT EXISTS && $.eventType != AwsServiceEvent }"],["IamPolicyChanges","{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}"],["CloudTrailConfigurationChanges","{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }"],["SignInFailures",'{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }'],["DisabledCmks","{($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion)) }"],["S3PolicyChanges","{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }"],["ConfigServiceChanges","{($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder))}"],["SecurityGroupChanges","{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}"],["NetworkAclChanges","{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }"],["NetworkGatewayChanges","{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }"],["RouteTableChanges","{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }"],["VpcChanges","{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }"],["OrganizationsChanges","{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = AcceptHandshake) || ($.eventName = AttachPolicy) || ($.eventName = CreateAccount) || ($.eventName = CreateOrganizationalUnit) || ($.eventName = CreatePolicy) || ($.eventName = DeclineHandshake) || ($.eventName = DeleteOrganization) || ($.eventName = DeleteOrganizationalUnit) || ($.eventName = DeletePolicy) || ($.eventName = DetachPolicy) || ($.eventName = DisablePolicyType) || ($.eventName = EnablePolicyType) || ($.eventName = InviteAccountToOrganization) || ($.eventName = LeaveOrganization) || ($.eventName = MoveAccount) || ($.eventName = RemoveAccountFromOrganization) || ($.eventName = UpdatePolicy) || ($.eventName = UpdateOrganizationalUnit)) }"]];for(let[l,c]of a)this.createMetricAlarm(l,c);console.log("LogGroup",this.logGroup?.node.id),console.log("CloudTrail",this.cloudTrail?.node.id),console.log("AlarmTopic",this.alarmTopic?.node.id)}createMetricAlarm(e,r){let a=`${this.config.stackName}${e}MetricFilter`,l=`${this.config.stackName}${e}Metric`,c=`${this.config.stackName}Metrics`,d=`${this.config.stackName}${e}Alarm`,A=new m.aws_logs.MetricFilter(this,a,{logGroup:this.logGroup,filterPattern:{logPatternString:r},metricNamespace:c,metricName:l});new m.aws_cloudwatch.Alarm(this,d,{metric:A.metric({}),threshold:1,evaluationPeriods:1,alarmName:d,actionsEnabled:!0,treatMissingData:m.aws_cloudwatch.TreatMissingData.NOT_BREACHING,comparisonOperator:m.aws_cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,datapointsToAlarm:1}).addAlarmAction(new m.aws_cloudwatch_actions.SnsAction(this.alarmTopic))}};var D=class{constructor(s,e){if(this.primaryStack=new y.Stack(s,e.stackName,{env:{region:e.region,account:e.accountNumber}}),y.Tags.of(this.primaryStack).add("medplum:environment",e.name),this.backEnd=new C(this.primaryStack,e),this.frontEnd=new N(this.primaryStack,e,e.region),this.storage=new h(this.primaryStack,e,e.region),this.cloudTrail=new f(this.primaryStack,e),e.region!=="us-east-1"){let r=new y.Stack(s,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});y.Tags.of(r).add("medplum:environment",e.name),this.frontEnd=new N(r,e,"us-east-1"),this.storage=new h(r,e,"us-east-1"),this.cloudTrail=new f(r,e)}}};function K(i){let s=new y.App({context:i}),e=s.node.tryGetContext("config");if(!e){console.log('Missing "config" context variable'),console.log("Usage: cdk deploy -c config=my-config.json");return}let r=JSON.parse((0,V.readFileSync)((0,z.resolve)(e),"utf-8")),a=new D(s,r);console.log("Stack",a.primaryStack.stackId),console.log("BackEnd",a.backEnd.node.id),console.log("FrontEnd",a.frontEnd.node.id),console.log("Storage",a.storage.node.id),console.log("CloudTrail",a.cloudTrail.node.id),s.synth()}require.main===module&&K();0&&(module.exports={main});
1
+ "use strict";var P=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var F=(c,n)=>{for(var a in n)P(c,a,{get:n[a],enumerable:!0})},x=(c,n,a,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of O(n))!M.call(c,o)&&o!==a&&P(c,o,{get:()=>n[o],enumerable:!(e=_(n,o))||e.enumerable});return c};var W=c=>x(P({},"__esModule",{value:!0}),c);var U={};F(U,{BackEnd:()=>A,CloudTrailAlarms:()=>p,FrontEnd:()=>g,MedplumGlobalStack:()=>R,MedplumPrimaryStack:()=>v,MedplumStack:()=>y,Storage:()=>h,awsManagedRules:()=>d,main:()=>B});module.exports=W(U);var E=require("aws-cdk-lib"),G=require("fs"),$=require("path");var S=require("aws-cdk-lib");var t=require("aws-cdk-lib"),N=require("aws-cdk-lib/aws-ecr"),w=require("aws-cdk-lib/aws-rds"),b=require("constructs");var d=[{name:"AWS-AWSManagedRulesCommonRuleSet",priority:10,statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesCommonRuleSet",excludedRules:[{name:"NoUserAgent_HEADER"},{name:"UserAgent_BadBots_HEADER"},{name:"SizeRestrictions_QUERYSTRING"},{name:"SizeRestrictions_Cookie_HEADER"},{name:"SizeRestrictions_BODY"},{name:"SizeRestrictions_URIPATH"},{name:"EC2MetaDataSSRF_BODY"},{name:"EC2MetaDataSSRF_COOKIE"},{name:"EC2MetaDataSSRF_URIPATH"},{name:"EC2MetaDataSSRF_QUERYARGUMENTS"},{name:"GenericLFI_QUERYARGUMENTS"},{name:"GenericLFI_URIPATH"},{name:"GenericLFI_BODY"},{name:"RestrictedExtensions_URIPATH"},{name:"RestrictedExtensions_QUERYARGUMENTS"},{name:"GenericRFI_QUERYARGUMENTS"},{name:"GenericRFI_BODY"},{name:"GenericRFI_URIPATH"},{name:"CrossSiteScripting_COOKIE"},{name:"CrossSiteScripting_QUERYARGUMENTS"},{name:"CrossSiteScripting_BODY"},{name:"CrossSiteScripting_URIPATH"}]}},overrideAction:{count:{}},visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWS-AWSManagedRulesCommonRuleSet"}},{name:"AWS-AWSManagedRulesAmazonIpReputationList",priority:20,statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesAmazonIpReputationList",excludedRules:[{name:"AWSManagedIPReputationList"},{name:"AWSManagedReconnaissanceList"}]}},overrideAction:{count:{}},visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRulesAmazonIpReputationList"}},{name:"AWSManagedRulesSQLiRuleSet",priority:30,visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRulesSQLiRuleSet"},overrideAction:{count:{}},statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesSQLiRuleSet",excludedRules:[{name:"SQLi_QUERYARGUMENTS"},{name:"SQLiExtendedPatterns_QUERYARGUMENTS"},{name:"SQLi_BODY"},{name:"SQLiExtendedPatterns_BODY"},{name:"SQLi_COOKIE"},{name:"SQLi_URIPATH"}]}}},{name:"AWSManagedRuleLinux",priority:40,visibilityConfig:{sampledRequestsEnabled:!0,cloudWatchMetricsEnabled:!0,metricName:"AWSManagedRuleLinux"},overrideAction:{count:{}},statement:{managedRuleGroupStatement:{vendorName:"AWS",name:"AWSManagedRulesLinuxRuleSet",excludedRules:[{name:"LFI_URIPATH"},{name:"LFI_QUERYSTRING"},{name:"LFI_COOKIE"}]}}}];var A=class extends b.Construct{constructor(a,e){super(a,"BackEnd");let o=e.name;if(e.vpcId)this.vpc=t.aws_ec2.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let i=new t.aws_logs.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+o,removalPolicy:t.RemovalPolicy.DESTROY});this.vpc=new t.aws_ec2.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:t.aws_ec2.FlowLogDestination.toCloudWatchLogs(i),trafficType:t.aws_ec2.FlowLogTrafficType.ALL}}})}if(this.botLambdaRole=new t.aws_iam.Role(this,"BotLambdaRole",{assumedBy:new t.aws_iam.ServicePrincipal("lambda.amazonaws.com")}),this.rdsSecretsArn=e.rdsSecretsArn,!this.rdsSecretsArn){let i={instanceType:e.rdsInstanceType?new t.aws_ec2.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},m;if(e.rdsInstances>1){m=[];for(let u=0;u<e.rdsInstances-1;u++)m.push(w.ClusterInstance.provisioned("Instance"+(u+2),{...i}))}this.rdsCluster=new t.aws_rds.DatabaseCluster(this,"DatabaseCluster",{engine:t.aws_rds.DatabaseClusterEngine.auroraPostgres({version:t.aws_rds.AuroraPostgresEngineVersion.VER_12_9}),credentials:t.aws_rds.Credentials.fromGeneratedSecret("clusteradmin"),defaultDatabaseName:"medplum",storageEncrypted:!0,vpc:this.vpc,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},writer:w.ClusterInstance.provisioned("Instance1",{...i}),readers:m,backup:{retention:t.Duration.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:t.aws_rds.InstanceUpdateBehaviour.ROLLING}),this.rdsSecretsArn=this.rdsCluster.secret.secretArn}if(this.redisSubnetGroup=new t.aws_elasticache.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:this.vpc.privateSubnets.map(i=>i.subnetId)}),this.redisSecurityGroup=new t.aws_ec2.SecurityGroup(this,"RedisSecurityGroup",{vpc:this.vpc,description:"Redis Security Group",allowAllOutbound:!1}),this.redisPassword=new t.aws_secretsmanager.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),this.redisCluster=new t.aws_elasticache.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:this.redisPassword.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:this.redisSubnetGroup.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[this.redisSecurityGroup.securityGroupId]}),this.redisCluster.node.addDependency(this.redisPassword),this.redisSecrets=new t.aws_secretsmanager.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:this.redisCluster.attrPrimaryEndPointAddress,port:this.redisCluster.attrPrimaryEndPointPort,password:this.redisPassword.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}}),this.redisSecrets.node.addDependency(this.redisPassword),this.redisSecrets.node.addDependency(this.redisCluster),this.ecsCluster=new t.aws_ecs.Cluster(this,"Cluster",{vpc:this.vpc}),this.taskRolePolicies=new t.aws_iam.PolicyDocument({statements:[new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["logs:CreateLogStream","logs:PutLogEvents"],resources:["arn:aws:logs:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["secretsmanager:GetResourcePolicy","secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecrets","secretsmanager:ListSecretVersionIds"],resources:["arn:aws:secretsmanager:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["ssm:GetParametersByPath","ssm:GetParameters","ssm:GetParameter","ssm:DescribeParameters"],resources:["arn:aws:ssm:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["ses:SendEmail","ses:SendRawEmail"],resources:["arn:aws:ses:*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["s3:ListBucket","s3:GetObject","s3:PutObject","s3:DeleteObject"],resources:["arn:aws:s3:::*"]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["iam:ListRoles","iam:GetRole","iam:PassRole"],resources:[this.botLambdaRole.roleArn]}),new t.aws_iam.PolicyStatement({effect:t.aws_iam.Effect.ALLOW,actions:["lambda:CreateFunction","lambda:GetFunction","lambda:GetFunctionConfiguration","lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration","lambda:ListLayerVersions","lambda:GetLayerVersion","lambda:InvokeFunction"],resources:["arn:aws:lambda:*"]})]}),this.taskRole=new t.aws_iam.Role(this,"TaskExecutionRole",{assumedBy:new t.aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),description:"Medplum Server Task Execution Role",inlinePolicies:{TaskExecutionPolicies:this.taskRolePolicies}}),this.taskDefinition=new t.aws_ecs.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:this.taskRole}),this.logGroup=new t.aws_logs.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+o,removalPolicy:t.RemovalPolicy.DESTROY}),this.logDriver=new t.aws_ecs.AwsLogDriver({logGroup:this.logGroup,streamPrefix:"Medplum"}),this.serviceContainer=this.taskDefinition.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${o}/`:`aws:${e.region}:/medplum/${o}/`],logging:this.logDriver}),this.serviceContainer.addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let i of e.additionalContainers)this.taskDefinition.addContainer("AdditionalContainer-"+i.name,{containerName:i.name,image:this.getContainerImage(e,i.image),command:i.command,environment:i.environment,logging:this.logDriver});if(this.fargateSecurityGroup=new t.aws_ec2.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:this.vpc}),this.fargateService=new t.aws_ecs.FargateService(this,"FargateService",{cluster:this.ecsCluster,taskDefinition:this.taskDefinition,assignPublicIp:!1,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[this.fargateSecurityGroup],healthCheckGracePeriod:t.Duration.minutes(5)}),this.rdsCluster&&this.fargateService.node.addDependency(this.rdsCluster),this.fargateService.node.addDependency(this.redisCluster),this.targetGroup=new t.aws_elasticloadbalancingv2.ApplicationTargetGroup(this,"TargetGroup",{vpc:this.vpc,port:e.apiPort,protocol:t.aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,healthCheck:{path:"/healthcheck",interval:t.Duration.seconds(30),timeout:t.Duration.seconds(3),healthyThresholdCount:2,unhealthyThresholdCount:5},targets:[this.fargateService]}),this.loadBalancer=new t.aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:this.vpc,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0}),e.loadBalancerLoggingBucket&&this.loadBalancer.logAccessLogs(t.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),this.loadBalancer.addListener("HttpsListener",{port:443,certificates:[{certificateArn:e.apiSslCertArn}],sslPolicy:t.aws_elasticloadbalancingv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,defaultAction:t.aws_elasticloadbalancingv2.ListenerAction.forward([this.targetGroup])}),this.waf=new t.aws_wafv2.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:d,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),this.wafAssociation=new t.aws_wafv2.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:this.loadBalancer.loadBalancerArn,webAclArn:this.waf.attrArn}),this.rdsCluster&&this.rdsCluster.connections.allowDefaultPortFrom(this.fargateSecurityGroup),this.redisSecurityGroup.addIngressRule(this.fargateSecurityGroup,t.aws_ec2.Port.tcp(6379)),!e.skipDns){let i=t.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});this.dnsRecord=new t.aws_route53.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:t.aws_route53.RecordTarget.fromAlias(new t.aws_route53_targets.LoadBalancerTarget(this.loadBalancer)),zone:i})}this.regionParameter=new t.aws_ssm.StringParameter(this,"RegionParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${o}/awsRegion`,description:"AWS region",stringValue:e.region}),this.databaseSecretsParameter=new t.aws_ssm.StringParameter(this,"DatabaseSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${o}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:this.rdsSecretsArn}),this.redisSecretsParameter=new t.aws_ssm.StringParameter(this,"RedisSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${o}/RedisSecrets`,description:"Redis secrets ARN",stringValue:this.redisSecrets.secretArn}),this.botLambdaRoleParameter=new t.aws_ssm.StringParameter(this,"BotLambdaRoleParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${o}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:this.botLambdaRole.roleArn})}getContainerImage(a,e){let i=new RegExp(`^${a.accountNumber}\\.dkr\\.ecr\\.${a.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),m=i?.[1],u=i?.[2];if(m&&u){let C=N.Repository.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${a.region}:${a.accountNumber}:repository/${m}`);return t.aws_ecs.ContainerImage.fromEcrRepository(C,u)}return t.aws_ecs.ContainerImage.fromRegistry(e)}};var l=require("aws-cdk-lib"),k=require("constructs"),p=class extends k.Construct{constructor(a,e){super(a,"CloudTrailAlarms");if(this.config=e,!e.cloudTrailAlarms)return;e.cloudTrailAlarms.logGroupCreate?(this.logGroup=new l.aws_logs.LogGroup(this,"CloudTrailLogGroup",{logGroupName:e.cloudTrailAlarms.logGroupName,retention:l.aws_logs.RetentionDays.ONE_YEAR}),this.cloudTrail=new l.aws_cloudtrail.Trail(this,"CloudTrail",{sendToCloudWatchLogs:!0,cloudWatchLogGroup:this.logGroup,includeGlobalServiceEvents:!0})):this.logGroup=l.aws_logs.LogGroup.fromLogGroupName(this,"CloudTrailLogGroup",e.cloudTrailAlarms.logGroupName),e.cloudTrailAlarms.snsTopicArn?this.alarmTopic=l.aws_sns.Topic.fromTopicArn(this,"AlarmTopic",e.cloudTrailAlarms.snsTopicArn):this.alarmTopic=new l.aws_sns.Topic(this,"AlarmTopic",{topicName:e.cloudTrailAlarms.snsTopicName});let o=[["UnauthorizedApiCalls","{ ($.errorCode = *UnauthorizedOperation) || ($.errorCode = AccessDenied*) }"],["SignInWithoutMfa","{ ($.eventName = ConsoleLogin) && ($.additionalEventData.MFAUsed != Yes) }"],["RootAccountUsage","{ $.userIdentity.type = Root && $.userIdentity.invokedBy NOT EXISTS && $.eventType != AwsServiceEvent }"],["IamPolicyChanges","{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}"],["CloudTrailConfigurationChanges","{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }"],["SignInFailures",'{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }'],["DisabledCmks","{($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion)) }"],["S3PolicyChanges","{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }"],["ConfigServiceChanges","{($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder))}"],["SecurityGroupChanges","{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}"],["NetworkAclChanges","{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }"],["NetworkGatewayChanges","{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }"],["RouteTableChanges","{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }"],["VpcChanges","{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }"],["OrganizationsChanges","{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = AcceptHandshake) || ($.eventName = AttachPolicy) || ($.eventName = CreateAccount) || ($.eventName = CreateOrganizationalUnit) || ($.eventName = CreatePolicy) || ($.eventName = DeclineHandshake) || ($.eventName = DeleteOrganization) || ($.eventName = DeleteOrganizationalUnit) || ($.eventName = DeletePolicy) || ($.eventName = DetachPolicy) || ($.eventName = DisablePolicyType) || ($.eventName = EnablePolicyType) || ($.eventName = InviteAccountToOrganization) || ($.eventName = LeaveOrganization) || ($.eventName = MoveAccount) || ($.eventName = RemoveAccountFromOrganization) || ($.eventName = UpdatePolicy) || ($.eventName = UpdateOrganizationalUnit)) }"]];for(let[i,m]of o)this.createMetricAlarm(i,m)}createMetricAlarm(a,e){let o=`${this.config.stackName}${a}MetricFilter`,i=`${this.config.stackName}${a}Metric`,m=`${this.config.stackName}Metrics`,u=`${this.config.stackName}${a}Alarm`,C=new l.aws_logs.MetricFilter(this,o,{logGroup:this.logGroup,filterPattern:{logPatternString:e},metricNamespace:m,metricName:i});new l.aws_cloudwatch.Alarm(this,u,{metric:C.metric({}),threshold:1,evaluationPeriods:1,alarmName:u,actionsEnabled:!0,treatMissingData:l.aws_cloudwatch.TreatMissingData.NOT_BREACHING,comparisonOperator:l.aws_cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,datapointsToAlarm:1}).addAlarmAction(new l.aws_cloudwatch_actions.SnsAction(this.alarmTopic))}};var r=require("aws-cdk-lib"),L=require("constructs");var I=require("aws-cdk-lib");function f(c,n){let a=new I.aws_iam.PolicyStatement;return a.addActions("s3:GetObject*"),a.addActions("s3:GetBucket*"),a.addActions("s3:List*"),a.addResources(c.bucketArn),a.addResources(`${c.bucketArn}/*`),a.addCanonicalUserPrincipal(n.cloudFrontOriginAccessIdentityS3CanonicalUserId),c.addToResourcePolicy(a),a}var g=class extends L.Construct{constructor(a,e,o){super(a,"FrontEnd");if(o===e.region?this.appBucket=new r.aws_s3.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:r.aws_s3.BlockPublicAccess.BLOCK_ALL,removalPolicy:r.RemovalPolicy.DESTROY,encryption:r.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):this.appBucket=r.aws_s3.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),o==="us-east-1"&&(this.responseHeadersPolicy=new r.aws_cloudfront.ResponseHeadersPolicy(this,"ResponseHeadersPolicy",{securityHeadersBehavior:{contentSecurityPolicy:{contentSecurityPolicy:["default-src 'none'","base-uri 'self'","child-src 'self'",`connect-src 'self' ${e.apiDomainName} *.google.com`,"font-src 'self' fonts.gstatic.com","form-action 'self' *.gstatic.com *.google.com","frame-ancestors 'none'","frame-src 'self' *.medplum.com *.gstatic.com *.google.com",`img-src 'self' data: ${e.storageDomainName} *.gstatic.com *.google.com *.googleapis.com`,"manifest-src 'self'",`media-src 'self' ${e.storageDomainName}`,"script-src 'self' *.medplum.com *.gstatic.com *.google.com","style-src 'self' 'unsafe-inline' *.medplum.com *.gstatic.com *.google.com","worker-src 'self' blob: *.gstatic.com *.google.com","upgrade-insecure-requests"].join("; "),override:!0},contentTypeOptions:{override:!0},frameOptions:{frameOption:r.aws_cloudfront.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:r.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),this.waf=new r.aws_wafv2.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:d,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),this.apiOriginCachePolicy=new r.aws_cloudfront.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:r.aws_cloudfront.CacheCookieBehavior.all(),headerBehavior:r.aws_cloudfront.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:r.aws_cloudfront.CacheQueryStringBehavior.all()}),this.originAccessIdentity=new r.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{}),this.originAccessPolicyStatement=f(this.appBucket,this.originAccessIdentity),this.distribution=new r.aws_cloudfront.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new r.aws_cloudfront_origins.S3Origin(this.appBucket,{originAccessIdentity:this.originAccessIdentity}),responseHeadersPolicy:this.responseHeadersPolicy,viewerProtocolPolicy:r.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new r.aws_cloudfront_origins.HttpOrigin(e.apiDomainName),allowedMethods:r.aws_cloudfront.AllowedMethods.ALLOW_ALL,cachePolicy:this.apiOriginCachePolicy,viewerProtocolPolicy:r.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:r.aws_certificatemanager.Certificate.fromCertificateArn(this,"AppCertificate",e.appSslCertArn),domainNames:[e.appDomainName],errorResponses:[{httpStatus:403,responseHttpStatus:200,responsePagePath:"/index.html"},{httpStatus:404,responseHttpStatus:200,responsePagePath:"/index.html"}],webAclId:this.waf.attrArn,logBucket:e.appLoggingBucket?r.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),!e.skipDns)){let i=r.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});this.dnsRecord=new r.aws_route53.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:r.aws_route53.RecordTarget.fromAlias(new r.aws_route53_targets.CloudFrontTarget(this.distribution)),zone:i})}}};var s=require("aws-cdk-lib"),T=require("cdk-serverless-clamscan"),D=require("constructs");var h=class extends D.Construct{constructor(a,e,o){super(a,"Storage");if(o===e.region?(this.storageBucket=new s.aws_s3.Bucket(this,"StorageBucket",{bucketName:e.storageBucketName,publicReadAccess:!1,blockPublicAccess:s.aws_s3.BlockPublicAccess.BLOCK_ALL,encryption:s.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}),e.clamscanEnabled&&new T.ServerlessClamscan(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(this.storageBucket)):this.storageBucket=s.aws_s3.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),o==="us-east-1"){let i;if(e.signingKeyId?i=s.aws_cloudfront.PublicKey.fromPublicKeyId(this,"StoragePublicKey",e.signingKeyId):i=new s.aws_cloudfront.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey}),this.keyGroup=new s.aws_cloudfront.KeyGroup(this,"StorageKeyGroup",{items:[i]}),this.responseHeadersPolicy=new s.aws_cloudfront.ResponseHeadersPolicy(this,"ResponseHeadersPolicy",{securityHeadersBehavior:{contentSecurityPolicy:{contentSecurityPolicy:"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors *.medplum.com;",override:!0},contentTypeOptions:{override:!0},frameOptions:{frameOption:s.aws_cloudfront.HeadersFrameOption.DENY,override:!0},referrerPolicy:{referrerPolicy:s.aws_cloudfront.HeadersReferrerPolicy.NO_REFERRER,override:!0},strictTransportSecurity:{accessControlMaxAge:s.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),this.waf=new s.aws_wafv2.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:d,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),this.originAccessIdentity=new s.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{}),this.originAccessPolicyStatement=f(this.storageBucket,this.originAccessIdentity),this.distribution=new s.aws_cloudfront.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new s.aws_cloudfront_origins.S3Origin(this.storageBucket,{originAccessIdentity:this.originAccessIdentity}),responseHeadersPolicy:this.responseHeadersPolicy,viewerProtocolPolicy:s.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[this.keyGroup]},certificate:s.aws_certificatemanager.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:this.waf.attrArn,logBucket:e.storageLoggingBucket?s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),!e.skipDns){let m=s.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});this.dnsRecord=new s.aws_route53.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:s.aws_route53.RecordTarget.fromAlias(new s.aws_route53_targets.CloudFrontTarget(this.distribution)),zone:m})}}}};var y=class{constructor(n,a){this.primaryStack=new v(n,a),a.region!=="us-east-1"&&(this.globalStack=new R(n,a),this.globalStack.addDependency(this.primaryStack))}},v=class extends S.Stack{constructor(a,e){super(a,e.stackName,{env:{region:e.region,account:e.accountNumber}});S.Tags.of(this).add("medplum:environment",e.name),this.backEnd=new A(this,e),this.frontEnd=new g(this,e,e.region),this.storage=new h(this,e,e.region),this.cloudTrail=new p(this,e)}},R=class extends S.Stack{constructor(a,e){super(a,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});S.Tags.of(this).add("medplum:environment",e.name),this.frontEnd=new g(this,e,"us-east-1"),this.storage=new h(this,e,"us-east-1"),this.cloudTrail=new p(this,e)}};function B(c){let n=new E.App({context:c}),a=n.node.tryGetContext("config");if(!a){console.log('Missing "config" context variable'),console.log("Usage: cdk deploy -c config=my-config.json");return}let e=JSON.parse((0,G.readFileSync)((0,$.resolve)(a),"utf-8")),o=new y(n,e);console.log("Stack",o.primaryStack.stackId),n.synth()}require.main===module&&B();0&&(module.exports={BackEnd,CloudTrailAlarms,FrontEnd,MedplumGlobalStack,MedplumPrimaryStack,MedplumStack,Storage,awsManagedRules,main});
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/index.ts", "../../src/backend.ts", "../../src/waf.ts", "../../src/frontend.ts", "../../src/oai.ts", "../../src/storage.ts", "../../src/cloudtrail.ts"],
4
- "sourcesContent": ["import { MedplumInfraConfig } from '@medplum/core';\nimport { App, Stack, Tags } from 'aws-cdk-lib';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\nimport { BackEnd } from './backend';\nimport { FrontEnd } from './frontend';\nimport { Storage } from './storage';\nimport { CloudTrailAlarms } from './cloudtrail';\n\nclass MedplumStack {\n primaryStack: Stack;\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\n cloudTrail: CloudTrailAlarms;\n\n constructor(scope: App, config: MedplumInfraConfig) {\n this.primaryStack = new Stack(scope, config.stackName, {\n env: {\n region: config.region,\n account: config.accountNumber,\n },\n });\n Tags.of(this.primaryStack).add('medplum:environment', config.name);\n\n this.backEnd = new BackEnd(this.primaryStack, config);\n this.frontEnd = new FrontEnd(this.primaryStack, config, config.region);\n this.storage = new Storage(this.primaryStack, config, config.region);\n this.cloudTrail = new CloudTrailAlarms(this.primaryStack, config);\n\n if (config.region !== 'us-east-1') {\n // Some resources must be created in us-east-1\n // For example, CloudFront distributions and ACM certificates\n // If the primary region is not us-east-1, create these resources in us-east-1\n const usEast1Stack = new Stack(scope, config.stackName + '-us-east-1', {\n env: {\n region: 'us-east-1',\n account: config.accountNumber,\n },\n });\n Tags.of(usEast1Stack).add('medplum:environment', config.name);\n\n this.frontEnd = new FrontEnd(usEast1Stack, config, 'us-east-1');\n this.storage = new Storage(usEast1Stack, config, 'us-east-1');\n this.cloudTrail = new CloudTrailAlarms(usEast1Stack, config);\n }\n }\n}\n\nexport function main(context?: Record<string, string>): void {\n const app = new App({ context });\n\n const configFileName = app.node.tryGetContext('config');\n if (!configFileName) {\n console.log('Missing \"config\" context variable');\n console.log('Usage: cdk deploy -c config=my-config.json');\n return;\n }\n\n const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as MedplumInfraConfig;\n\n const stack = new MedplumStack(app, config);\n\n console.log('Stack', stack.primaryStack.stackId);\n console.log('BackEnd', stack.backEnd.node.id);\n console.log('FrontEnd', stack.frontEnd.node.id);\n console.log('Storage', stack.storage.node.id);\n console.log('CloudTrail', stack.cloudTrail.node.id);\n\n app.synth();\n}\n\nif (require.main === module) {\n main();\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n Duration,\n aws_ec2 as ec2,\n aws_ecs as ecs,\n aws_elasticache as elasticache,\n aws_elasticloadbalancingv2 as elbv2,\n aws_iam as iam,\n aws_logs as logs,\n aws_rds as rds,\n RemovalPolicy,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_secretsmanager as secretsmanager,\n aws_ssm as ssm,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { Repository } from 'aws-cdk-lib/aws-ecr';\nimport { ClusterInstance } from 'aws-cdk-lib/aws-rds';\nimport { Construct } from 'constructs';\nimport { awsManagedRules } from './waf';\n\n/**\n * Based on: https://github.com/aws-samples/http-api-aws-fargate-cdk/blob/master/cdk/singleAccount/lib/fargate-vpclink-stack.ts\n *\n * RDS config: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-rds-readme.html\n */\nexport class BackEnd extends Construct {\n constructor(scope: Construct, config: MedplumInfraConfig) {\n super(scope, 'BackEnd');\n\n const name = config.name;\n\n // VPC\n let vpc: ec2.IVpc;\n\n if (config.vpcId) {\n // Lookup VPC by ARN\n vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });\n } else {\n // VPC Flow Logs\n const vpcFlowLogs = new logs.LogGroup(this, 'VpcFlowLogs', {\n logGroupName: '/medplum/flowlogs/' + name,\n removalPolicy: RemovalPolicy.DESTROY,\n });\n\n // Create VPC\n vpc = new ec2.Vpc(this, 'VPC', {\n maxAzs: config.maxAzs,\n flowLogs: {\n cloudwatch: {\n destination: ec2.FlowLogDestination.toCloudWatchLogs(vpcFlowLogs),\n trafficType: ec2.FlowLogTrafficType.ALL,\n },\n },\n });\n }\n\n // Bot Lambda Role\n const botLambdaRole = new iam.Role(this, 'BotLambdaRole', {\n assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),\n });\n\n // RDS\n let rdsCluster = undefined;\n let rdsSecretsArn = config.rdsSecretsArn;\n if (!rdsSecretsArn) {\n // See: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds-readme.html#migrating-from-instanceprops\n const instanceProps: rds.ProvisionedClusterInstanceProps = {\n instanceType: config.rdsInstanceType ? new ec2.InstanceType(config.rdsInstanceType) : undefined,\n enablePerformanceInsights: true,\n isFromLegacyInstanceProps: true,\n };\n\n let readers = undefined;\n if (config.rdsInstances > 1) {\n readers = [];\n for (let i = 0; i < config.rdsInstances - 1; i++) {\n readers.push(\n ClusterInstance.provisioned('Instance' + (i + 2), {\n ...instanceProps,\n })\n );\n }\n }\n\n rdsCluster = new rds.DatabaseCluster(this, 'DatabaseCluster', {\n engine: rds.DatabaseClusterEngine.auroraPostgres({\n version: rds.AuroraPostgresEngineVersion.VER_12_9,\n }),\n credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'),\n defaultDatabaseName: 'medplum',\n storageEncrypted: true,\n vpc: vpc,\n vpcSubnets: {\n subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,\n },\n writer: ClusterInstance.provisioned('Instance1', {\n ...instanceProps,\n }),\n readers,\n backup: {\n retention: Duration.days(7),\n },\n cloudwatchLogsExports: ['postgresql'],\n instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,\n });\n\n rdsSecretsArn = (rdsCluster.secret as secretsmanager.ISecret).secretArn;\n }\n\n // Redis\n // Important: For HIPAA compliance, you must specify TransitEncryptionEnabled as true, an AuthToken, and a CacheSubnetGroup.\n const redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {\n description: 'Redis Subnet Group',\n subnetIds: vpc.privateSubnets.map((subnet) => subnet.subnetId),\n });\n\n const redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {\n vpc,\n description: 'Redis Security Group',\n allowAllOutbound: false,\n });\n\n const redisPassword = new secretsmanager.Secret(this, 'RedisPassword', {\n generateSecretString: {\n secretStringTemplate: '{}',\n generateStringKey: 'password',\n excludeCharacters: '@%*()_+=`~{}|[]\\\\:\";\\'?,./',\n },\n });\n\n const redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {\n engine: 'Redis',\n engineVersion: '6.x',\n cacheNodeType: config.cacheNodeType ?? 'cache.t2.medium',\n replicationGroupDescription: 'RedisReplicationGroup',\n authToken: redisPassword.secretValueFromJson('password').toString(),\n transitEncryptionEnabled: true,\n atRestEncryptionEnabled: true,\n multiAzEnabled: true,\n cacheSubnetGroupName: redisSubnetGroup.ref,\n numNodeGroups: 1,\n replicasPerNodeGroup: 1,\n securityGroupIds: [redisSecurityGroup.securityGroupId],\n });\n redisCluster.node.addDependency(redisPassword);\n\n const redisSecrets = new secretsmanager.Secret(this, 'RedisSecrets', {\n generateSecretString: {\n secretStringTemplate: JSON.stringify({\n host: redisCluster.attrPrimaryEndPointAddress,\n port: redisCluster.attrPrimaryEndPointPort,\n password: redisPassword.secretValueFromJson('password').toString(),\n tls: {},\n }),\n generateStringKey: 'unused',\n },\n });\n redisSecrets.node.addDependency(redisPassword);\n redisSecrets.node.addDependency(redisCluster);\n\n // ECS Cluster\n const cluster = new ecs.Cluster(this, 'Cluster', {\n vpc: vpc,\n });\n\n // Task Policies\n const taskRolePolicies = new iam.PolicyDocument({\n statements: [\n // CloudWatch Logs: Create streams and put events\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],\n resources: ['arn:aws:logs:*'],\n }),\n\n // Secrets Manager: Read only access to secrets\n // https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: [\n 'secretsmanager:GetResourcePolicy',\n 'secretsmanager:GetSecretValue',\n 'secretsmanager:DescribeSecret',\n 'secretsmanager:ListSecrets',\n 'secretsmanager:ListSecretVersionIds',\n ],\n resources: ['arn:aws:secretsmanager:*'],\n }),\n\n // Parameter Store: Read only access\n // https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'],\n resources: ['arn:aws:ssm:*'],\n }),\n\n // SES: Send emails\n // https://docs.aws.amazon.com/ses/latest/dg/sending-authorization-policy-examples.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['ses:SendEmail', 'ses:SendRawEmail'],\n resources: ['arn:aws:ses:*'],\n }),\n\n // S3: Read and write access to buckets\n // https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['s3:ListBucket', 's3:GetObject', 's3:PutObject', 's3:DeleteObject'],\n resources: ['arn:aws:s3:::*'],\n }),\n\n // IAM: Pass role to innvoke lambda functions\n // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['iam:ListRoles', 'iam:GetRole', 'iam:PassRole'],\n resources: [botLambdaRole.roleArn],\n }),\n\n // Lambda: Create, read, update, delete, and invoke functions\n // https://docs.aws.amazon.com/lambda/latest/dg/access-control-identity-based.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: [\n 'lambda:CreateFunction',\n 'lambda:GetFunction',\n 'lambda:GetFunctionConfiguration',\n 'lambda:UpdateFunctionCode',\n 'lambda:UpdateFunctionConfiguration',\n 'lambda:ListLayerVersions',\n 'lambda:GetLayerVersion',\n 'lambda:InvokeFunction',\n ],\n resources: ['arn:aws:lambda:*'],\n }),\n ],\n });\n\n // Task Role\n const taskRole = new iam.Role(this, 'TaskExecutionRole', {\n assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),\n description: 'Medplum Server Task Execution Role',\n inlinePolicies: {\n TaskExecutionPolicies: taskRolePolicies,\n },\n });\n\n // Task Definitions\n const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {\n memoryLimitMiB: config.serverMemory,\n cpu: config.serverCpu,\n taskRole: taskRole,\n });\n\n // Log Groups\n const logGroup = new logs.LogGroup(this, 'LogGroup', {\n logGroupName: '/ecs/medplum/' + name,\n removalPolicy: RemovalPolicy.DESTROY,\n });\n\n const logDriver = new ecs.AwsLogDriver({\n logGroup: logGroup,\n streamPrefix: 'Medplum',\n });\n\n // Task Containers\n const serviceContainer = taskDefinition.addContainer('MedplumTaskDefinition', {\n image: this.getContainerImage(config, config.serverImage),\n command: [config.region === 'us-east-1' ? `aws:/medplum/${name}/` : `aws:${config.region}:/medplum/${name}/`],\n logging: logDriver,\n });\n\n serviceContainer.addPortMappings({\n containerPort: config.apiPort,\n hostPort: config.apiPort,\n });\n\n if (config.additionalContainers) {\n for (const container of config.additionalContainers) {\n taskDefinition.addContainer('AdditionalContainer-' + container.name, {\n containerName: container.name,\n image: this.getContainerImage(config, container.image),\n command: container.command,\n environment: container.environment,\n logging: logDriver,\n });\n }\n }\n\n // Security Groups\n const fargateSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {\n allowAllOutbound: true,\n securityGroupName: 'MedplumSecurityGroup',\n vpc: vpc,\n });\n\n // Fargate Services\n const fargateService = new ecs.FargateService(this, 'FargateService', {\n cluster: cluster,\n taskDefinition: taskDefinition,\n assignPublicIp: false,\n vpcSubnets: {\n subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,\n },\n desiredCount: config.desiredServerCount,\n securityGroups: [fargateSecurityGroup],\n healthCheckGracePeriod: Duration.minutes(5),\n });\n\n // Add dependencies - make sure Fargate service is created after RDS and Redis\n if (rdsCluster) {\n fargateService.node.addDependency(rdsCluster);\n }\n fargateService.node.addDependency(redisCluster);\n\n // Load Balancer Target Group\n const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {\n vpc: vpc,\n port: config.apiPort,\n protocol: elbv2.ApplicationProtocol.HTTP,\n healthCheck: {\n path: '/healthcheck',\n interval: Duration.seconds(30),\n timeout: Duration.seconds(3),\n healthyThresholdCount: 2,\n unhealthyThresholdCount: 5,\n },\n targets: [fargateService],\n });\n\n // Load Balancer\n const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {\n vpc: vpc,\n internetFacing: config.apiInternetFacing !== false, // default true\n http2Enabled: true,\n });\n\n if (config.loadBalancerLoggingBucket) {\n // Load Balancer logging\n loadBalancer.logAccessLogs(\n s3.Bucket.fromBucketName(this, 'LoggingBucket', config.loadBalancerLoggingBucket),\n config.loadBalancerLoggingPrefix\n );\n }\n\n // HTTPS Listener\n // Forward to the target group\n loadBalancer.addListener('HttpsListener', {\n port: 443,\n certificates: [\n {\n certificateArn: config.apiSslCertArn,\n },\n ],\n sslPolicy: elbv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,\n defaultAction: elbv2.ListenerAction.forward([targetGroup]),\n });\n\n // WAF\n const waf = new wafv2.CfnWebACL(this, 'BackEndWAF', {\n defaultAction: { allow: {} },\n scope: 'REGIONAL',\n name: `${config.stackName}-BackEndWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-BackEndWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // Create an association between the load balancer and the WAF\n const wafAssociation = new wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {\n resourceArn: loadBalancer.loadBalancerArn,\n webAclArn: waf.attrArn,\n });\n\n // Grant RDS access to the fargate group\n if (rdsCluster) {\n rdsCluster.connections.allowDefaultPortFrom(fargateSecurityGroup);\n }\n\n // Grant Redis access to the fargate group\n redisSecurityGroup.addIngressRule(fargateSecurityGroup, ec2.Port.tcp(6379));\n\n // DNS\n let record = undefined;\n if (!config.skipDns) {\n // Route 53\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the load balancer\n record = new route53.ARecord(this, 'LoadBalancerAliasRecord', {\n recordName: config.apiDomainName,\n target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(loadBalancer)),\n zone: zone,\n });\n }\n\n // SSM Parameters\n const databaseSecrets = new ssm.StringParameter(this, 'DatabaseSecretsParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/DatabaseSecrets`,\n description: 'Database secrets ARN',\n stringValue: rdsSecretsArn,\n });\n\n const redisSecretsParameter = new ssm.StringParameter(this, 'RedisSecretsParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/RedisSecrets`,\n description: 'Redis secrets ARN',\n stringValue: redisSecrets.secretArn,\n });\n\n const botLambdaRoleParameter = new ssm.StringParameter(this, 'BotLambdaRoleParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/botLambdaRoleArn`,\n description: 'Bot lambda execution role ARN',\n stringValue: botLambdaRole.roleArn,\n });\n\n // Debug\n console.log('ARecord', record?.domainName);\n console.log('DatabaseSecretsParameter', databaseSecrets.parameterArn);\n console.log('RedisSecretsParameter', redisSecretsParameter.parameterArn);\n console.log('RedisCluster', redisCluster.attrPrimaryEndPointAddress);\n console.log('BotLambdaRole', botLambdaRoleParameter.stringValue);\n console.log('WAF', waf.attrArn);\n console.log('WAF Association', wafAssociation.node.id);\n }\n\n /**\n * Returns a container image for the given image name.\n * If the image name is an ECR image, then the image will be pulled from ECR.\n * Otherwise, the image name is assumed to be a Docker Hub image.\n * @param config The config settings (account number and region).\n * @param imageName The image name.\n * @returns The container image.\n */\n private getContainerImage(config: MedplumInfraConfig, imageName: string): ecs.ContainerImage {\n // Pull out the image name and tag from the image URI if it's an ECR image\n const ecrImageUriRegex = new RegExp(\n `^${config.accountNumber}\\\\.dkr\\\\.ecr\\\\.${config.region}\\\\.amazonaws\\\\.com/(.*)[:@](.*)$`\n );\n const nameTagMatches = ecrImageUriRegex.exec(imageName);\n const serverImageName = nameTagMatches?.[1];\n const serverImageTag = nameTagMatches?.[2];\n if (serverImageName && serverImageTag) {\n // Creating an ecr repository image will automatically grant fine-grained permissions to ecs to access the image\n const ecrRepo = Repository.fromRepositoryArn(\n this,\n 'ServerImageRepo',\n `arn:aws:ecr:${config.region}:${config.accountNumber}:repository/${serverImageName}`\n );\n return ecs.ContainerImage.fromEcrRepository(ecrRepo, serverImageTag);\n }\n\n // Otherwise, use the standard container image\n return ecs.ContainerImage.fromRegistry(imageName);\n }\n}\n", "// Based on https://gist.github.com/statik/f1ac9d6227d98d30c7a7cec0c83f4e64\n\nimport { aws_wafv2 as wafv2 } from 'aws-cdk-lib';\n\nexport const awsManagedRules: wafv2.CfnWebACL.RuleProperty[] = [\n // Common Rule Set aligns with major portions of OWASP Core Rule Set\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html\n {\n name: 'AWS-AWSManagedRulesCommonRuleSet',\n priority: 10,\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesCommonRuleSet',\n // Excluding generic RFI body rule for sns notifications\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html\n excludedRules: [\n { name: 'NoUserAgent_HEADER' },\n { name: 'UserAgent_BadBots_HEADER' },\n { name: 'SizeRestrictions_QUERYSTRING' },\n { name: 'SizeRestrictions_Cookie_HEADER' },\n { name: 'SizeRestrictions_BODY' },\n { name: 'SizeRestrictions_URIPATH' },\n { name: 'EC2MetaDataSSRF_BODY' },\n { name: 'EC2MetaDataSSRF_COOKIE' },\n { name: 'EC2MetaDataSSRF_URIPATH' },\n { name: 'EC2MetaDataSSRF_QUERYARGUMENTS' },\n { name: 'GenericLFI_QUERYARGUMENTS' },\n { name: 'GenericLFI_URIPATH' },\n { name: 'GenericLFI_BODY' },\n { name: 'RestrictedExtensions_URIPATH' },\n { name: 'RestrictedExtensions_QUERYARGUMENTS' },\n { name: 'GenericRFI_QUERYARGUMENTS' },\n { name: 'GenericRFI_BODY' },\n { name: 'GenericRFI_URIPATH' },\n { name: 'CrossSiteScripting_COOKIE' },\n { name: 'CrossSiteScripting_QUERYARGUMENTS' },\n { name: 'CrossSiteScripting_BODY' },\n { name: 'CrossSiteScripting_URIPATH' },\n ],\n },\n },\n overrideAction: {\n count: {},\n },\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWS-AWSManagedRulesCommonRuleSet',\n },\n },\n // AWS IP Reputation list includes known malicious actors/bots and is regularly updated\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html\n {\n name: 'AWS-AWSManagedRulesAmazonIpReputationList',\n priority: 20,\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesAmazonIpReputationList',\n excludedRules: [{ name: 'AWSManagedIPReputationList' }, { name: 'AWSManagedReconnaissanceList' }],\n },\n },\n overrideAction: {\n count: {},\n },\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRulesAmazonIpReputationList',\n },\n },\n // Blocks common SQL Injection\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-sql-db\n {\n name: 'AWSManagedRulesSQLiRuleSet',\n priority: 30,\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRulesSQLiRuleSet',\n },\n overrideAction: {\n count: {},\n },\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesSQLiRuleSet',\n excludedRules: [\n { name: 'SQLi_QUERYARGUMENTS' },\n { name: 'SQLiExtendedPatterns_QUERYARGUMENTS' },\n { name: 'SQLi_BODY' },\n { name: 'SQLiExtendedPatterns_BODY' },\n { name: 'SQLi_COOKIE' },\n { name: 'SQLi_URIPATH' },\n ],\n },\n },\n },\n // Blocks attacks targeting LFI(Local File Injection) for linux systems\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-linux-os\n {\n name: 'AWSManagedRuleLinux',\n priority: 40,\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRuleLinux',\n },\n overrideAction: {\n count: {},\n },\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesLinuxRuleSet',\n excludedRules: [{ name: 'LFI_URIPATH' }, { name: 'LFI_QUERYSTRING' }, { name: 'LFI_COOKIE' }],\n },\n },\n },\n];\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_certificatemanager as acm,\n aws_cloudfront as cloudfront,\n Duration,\n aws_cloudfront_origins as origins,\n RemovalPolicy,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { Construct } from 'constructs';\nimport { grantBucketAccessToOriginAccessIdentity } from './oai';\nimport { awsManagedRules } from './waf';\n\n/**\n * Static app infrastructure, which deploys app content to an S3 bucket.\n *\n * The app redirects from HTTP to HTTPS, using a CloudFront distribution,\n * Route53 alias record, and ACM certificate.\n */\nexport class FrontEnd extends Construct {\n constructor(parent: Construct, config: MedplumInfraConfig, region: string) {\n super(parent, 'FrontEnd');\n\n let appBucket: s3.IBucket;\n\n if (region === config.region) {\n // S3 bucket\n appBucket = new s3.Bucket(this, 'AppBucket', {\n bucketName: config.appDomainName,\n publicReadAccess: false,\n blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,\n removalPolicy: RemovalPolicy.DESTROY,\n encryption: s3.BucketEncryption.S3_MANAGED,\n enforceSSL: true,\n versioned: true,\n });\n } else {\n // Otherwise, reference the bucket by name and region\n appBucket = s3.Bucket.fromBucketAttributes(this, 'AppBucket', {\n bucketName: config.appDomainName,\n region: config.region,\n });\n }\n\n if (region === 'us-east-1') {\n // HTTP response headers policy\n const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {\n securityHeadersBehavior: {\n contentSecurityPolicy: {\n contentSecurityPolicy: [\n `default-src 'none'`,\n `base-uri 'self'`,\n `child-src 'self'`,\n `connect-src 'self' ${config.apiDomainName} *.google.com`,\n `font-src 'self' fonts.gstatic.com`,\n `form-action 'self' *.gstatic.com *.google.com`,\n `frame-ancestors 'none'`,\n `frame-src 'self' *.medplum.com *.gstatic.com *.google.com`,\n `img-src 'self' data: ${config.storageDomainName} *.gstatic.com *.google.com *.googleapis.com`,\n `manifest-src 'self'`,\n `media-src 'self' ${config.storageDomainName}`,\n `script-src 'self' *.medplum.com *.gstatic.com *.google.com`,\n `style-src 'self' 'unsafe-inline' *.medplum.com *.gstatic.com *.google.com`,\n `worker-src 'self' blob: *.gstatic.com *.google.com`,\n `upgrade-insecure-requests`,\n ].join('; '),\n override: true,\n },\n contentTypeOptions: { override: true },\n frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },\n strictTransportSecurity: {\n accessControlMaxAge: Duration.seconds(63072000),\n includeSubdomains: true,\n override: true,\n },\n xssProtection: {\n protection: true,\n modeBlock: true,\n override: true,\n },\n },\n });\n\n // WAF\n const waf = new wafv2.CfnWebACL(this, 'FrontEndWAF', {\n defaultAction: { allow: {} },\n scope: 'CLOUDFRONT',\n name: `${config.stackName}-FrontEndWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-FrontEndWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // API Origin Cache Policy\n const apiOriginCachePolicy = new cloudfront.CachePolicy(this, 'ApiOriginCachePolicy', {\n cachePolicyName: `${config.stackName}-ApiOriginCachePolicy`,\n cookieBehavior: cloudfront.CacheCookieBehavior.all(),\n headerBehavior: cloudfront.CacheHeaderBehavior.allowList(\n 'Authorization',\n 'Content-Encoding',\n 'Content-Type',\n 'If-None-Match',\n 'Origin',\n 'Referer',\n 'User-Agent',\n 'X-Medplum'\n ),\n queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),\n });\n\n // Origin access identity\n const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});\n grantBucketAccessToOriginAccessIdentity(appBucket, originAccessIdentity);\n\n // CloudFront distribution\n const distribution = new cloudfront.Distribution(this, 'AppDistribution', {\n defaultRootObject: 'index.html',\n defaultBehavior: {\n origin: new origins.S3Origin(appBucket, { originAccessIdentity }),\n responseHeadersPolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n },\n additionalBehaviors: config.appApiProxy\n ? {\n '/api/*': {\n origin: new origins.HttpOrigin(config.apiDomainName),\n allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,\n cachePolicy: apiOriginCachePolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n },\n }\n : undefined,\n certificate: acm.Certificate.fromCertificateArn(this, 'AppCertificate', config.appSslCertArn),\n domainNames: [config.appDomainName],\n errorResponses: [\n {\n httpStatus: 403,\n responseHttpStatus: 200,\n responsePagePath: '/index.html',\n },\n {\n httpStatus: 404,\n responseHttpStatus: 200,\n responsePagePath: '/index.html',\n },\n ],\n webAclId: waf.attrArn,\n logBucket: config.appLoggingBucket\n ? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.appLoggingBucket)\n : undefined,\n logFilePrefix: config.appLoggingPrefix,\n });\n\n // DNS\n let record = undefined;\n if (!config.skipDns) {\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the CloudFront distribution\n record = new route53.ARecord(this, 'AppAliasRecord', {\n recordName: config.appDomainName,\n target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),\n zone,\n });\n }\n\n // Debug\n console.log('ARecord', record?.domainName);\n }\n }\n}\n", "import { aws_cloudfront as cloudfront, aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib';\n\n/**\n * Grants S3 bucket read access to the CloudFront Origin Access Identity (OAI).\n *\n * Under normal circumstances, where CDK creates both the S3 bucket and the OAI,\n * you can achieve this same behavior by simply calling:\n *\n * bucket.grantRead(identity);\n *\n * However, if importing an S3 bucket via `s3.Bucket.fromBucketAttributes()`, that does not work.\n *\n * See: https://stackoverflow.com/a/60917015\n * @param bucket The S3 bucket.\n * @param identity The CloudFront Origin Access Identity.\n */\nexport function grantBucketAccessToOriginAccessIdentity(\n bucket: s3.IBucket,\n identity: cloudfront.OriginAccessIdentity\n): void {\n const policyStatement = new iam.PolicyStatement();\n policyStatement.addActions('s3:GetObject*');\n policyStatement.addActions('s3:GetBucket*');\n policyStatement.addActions('s3:List*');\n policyStatement.addResources(bucket.bucketArn);\n policyStatement.addResources(`${bucket.bucketArn}/*`);\n policyStatement.addCanonicalUserPrincipal(identity.cloudFrontOriginAccessIdentityS3CanonicalUserId);\n bucket.addToResourcePolicy(policyStatement);\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_certificatemanager as acm,\n aws_cloudfront as cloudfront,\n Duration,\n aws_cloudfront_origins as origins,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { ServerlessClamscan } from 'cdk-serverless-clamscan';\nimport { Construct } from 'constructs';\nimport { grantBucketAccessToOriginAccessIdentity } from './oai';\nimport { awsManagedRules } from './waf';\n\n/**\n * Binary storage bucket and CloudFront distribution.\n */\nexport class Storage extends Construct {\n constructor(parent: Construct, config: MedplumInfraConfig, region: string) {\n super(parent, 'Storage');\n\n let storageBucket: s3.IBucket;\n\n if (region === config.region) {\n // S3 bucket\n storageBucket = new s3.Bucket(this, 'StorageBucket', {\n bucketName: config.storageBucketName,\n publicReadAccess: false,\n blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,\n encryption: s3.BucketEncryption.S3_MANAGED,\n enforceSSL: true,\n versioned: true,\n });\n\n if (config.clamscanEnabled) {\n // ClamAV serverless scan\n const sc = new ServerlessClamscan(this, 'ServerlessClamscan', {\n defsBucketAccessLogsConfig: {\n logsBucket: s3.Bucket.fromBucketName(this, 'LoggingBucket', config.clamscanLoggingBucket),\n logsPrefix: config.clamscanLoggingPrefix,\n },\n });\n sc.addSourceBucket(storageBucket);\n }\n } else {\n // Otherwise, reference the bucket by name\n storageBucket = s3.Bucket.fromBucketAttributes(this, 'StorageBucket', {\n bucketName: config.storageBucketName,\n region: config.region,\n });\n }\n\n if (region === 'us-east-1') {\n // Public key in PEM format\n let publicKey: cloudfront.IPublicKey;\n if (config.signingKeyId) {\n publicKey = cloudfront.PublicKey.fromPublicKeyId(this, 'StoragePublicKey', config.signingKeyId);\n } else {\n publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {\n encodedKey: config.storagePublicKey,\n });\n }\n\n // Authorized key group for presigned URLs\n const keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {\n items: [publicKey],\n });\n\n // HTTP response headers policy\n const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {\n securityHeadersBehavior: {\n contentSecurityPolicy: {\n contentSecurityPolicy:\n \"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors *.medplum.com;\",\n override: true,\n },\n contentTypeOptions: { override: true },\n frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },\n referrerPolicy: { referrerPolicy: cloudfront.HeadersReferrerPolicy.NO_REFERRER, override: true },\n strictTransportSecurity: {\n accessControlMaxAge: Duration.seconds(63072000),\n includeSubdomains: true,\n override: true,\n },\n xssProtection: {\n protection: true,\n modeBlock: true,\n override: true,\n },\n },\n });\n\n // WAF\n const waf = new wafv2.CfnWebACL(this, 'StorageWAF', {\n defaultAction: { allow: {} },\n scope: 'CLOUDFRONT',\n name: `${config.stackName}-StorageWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-StorageWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // Origin access identity\n const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});\n grantBucketAccessToOriginAccessIdentity(storageBucket, originAccessIdentity);\n\n // CloudFront distribution\n const distribution = new cloudfront.Distribution(this, 'StorageDistribution', {\n defaultBehavior: {\n origin: new origins.S3Origin(storageBucket, { originAccessIdentity }),\n responseHeadersPolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n trustedKeyGroups: [keyGroup],\n },\n certificate: acm.Certificate.fromCertificateArn(this, 'StorageCertificate', config.storageSslCertArn),\n domainNames: [config.storageDomainName],\n webAclId: waf.attrArn,\n logBucket: config.storageLoggingBucket\n ? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.storageLoggingBucket)\n : undefined,\n logFilePrefix: config.storageLoggingPrefix,\n });\n\n // DNS\n let record = undefined;\n if (!config.skipDns) {\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the CloudFront distribution\n record = new route53.ARecord(this, 'StorageAliasRecord', {\n recordName: config.storageDomainName,\n target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),\n zone,\n });\n }\n\n // Debug\n console.log('ARecord', record?.domainName);\n }\n }\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_cloudtrail as cloudtrail,\n aws_cloudwatch as cloudwatch,\n aws_cloudwatch_actions as cloudwatch_actions,\n aws_logs as logs,\n aws_sns as sns,\n} from 'aws-cdk-lib';\nimport { Construct } from 'constructs';\n\nexport class CloudTrailAlarms extends Construct {\n config: MedplumInfraConfig;\n logGroup?: logs.ILogGroup;\n cloudTrail?: cloudtrail.Trail;\n alarmTopic?: sns.ITopic;\n\n constructor(scope: Construct, config: MedplumInfraConfig) {\n super(scope, 'CloudTrailAlarms');\n this.config = config;\n\n // CloudTrail is optional\n if (!config.cloudTrailAlarms) {\n return;\n }\n\n // Get the CloudTrail log group\n // This can be created or imported by name\n if (config.cloudTrailAlarms.logGroupCreate) {\n this.logGroup = new logs.LogGroup(this, 'CloudTrailLogGroup', {\n logGroupName: config.cloudTrailAlarms.logGroupName,\n retention: logs.RetentionDays.ONE_YEAR,\n });\n this.cloudTrail = new cloudtrail.Trail(this, 'CloudTrail', {\n sendToCloudWatchLogs: true,\n cloudWatchLogGroup: this.logGroup,\n includeGlobalServiceEvents: true,\n });\n } else {\n this.logGroup = logs.LogGroup.fromLogGroupName(this, 'CloudTrailLogGroup', config.cloudTrailAlarms.logGroupName);\n }\n\n // Get the SNS Topic\n // This can be created or imported by name\n if (config.cloudTrailAlarms.snsTopicArn) {\n this.alarmTopic = sns.Topic.fromTopicArn(this, 'AlarmTopic', config.cloudTrailAlarms.snsTopicArn);\n } else {\n this.alarmTopic = new sns.Topic(this, 'AlarmTopic', { topicName: config.cloudTrailAlarms.snsTopicName });\n }\n const alarmDefinitions = [\n ['UnauthorizedApiCalls', '{ ($.errorCode = *UnauthorizedOperation) || ($.errorCode = AccessDenied*) }'],\n ['SignInWithoutMfa', '{ ($.eventName = ConsoleLogin) && ($.additionalEventData.MFAUsed != Yes) }'],\n [\n 'RootAccountUsage',\n '{ $.userIdentity.type = Root && $.userIdentity.invokedBy NOT EXISTS && $.eventType != AwsServiceEvent }',\n ],\n [\n 'IamPolicyChanges',\n '{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}',\n ],\n [\n 'CloudTrailConfigurationChanges',\n '{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }',\n ],\n ['SignInFailures', '{ ($.eventName = ConsoleLogin) && ($.errorMessage = \"Failed authentication\") }'],\n [\n 'DisabledCmks',\n '{($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion)) }',\n ],\n [\n 'S3PolicyChanges',\n '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }',\n ],\n [\n 'ConfigServiceChanges',\n '{($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder))}',\n ],\n [\n 'SecurityGroupChanges',\n '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}',\n ],\n [\n 'NetworkAclChanges',\n '{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }',\n ],\n [\n 'NetworkGatewayChanges',\n '{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }',\n ],\n [\n 'RouteTableChanges',\n '{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }',\n ],\n [\n 'VpcChanges',\n '{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }',\n ],\n [\n 'OrganizationsChanges',\n '{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = AcceptHandshake) || ($.eventName = AttachPolicy) || ($.eventName = CreateAccount) || ($.eventName = CreateOrganizationalUnit) || ($.eventName = CreatePolicy) || ($.eventName = DeclineHandshake) || ($.eventName = DeleteOrganization) || ($.eventName = DeleteOrganizationalUnit) || ($.eventName = DeletePolicy) || ($.eventName = DetachPolicy) || ($.eventName = DisablePolicyType) || ($.eventName = EnablePolicyType) || ($.eventName = InviteAccountToOrganization) || ($.eventName = LeaveOrganization) || ($.eventName = MoveAccount) || ($.eventName = RemoveAccountFromOrganization) || ($.eventName = UpdatePolicy) || ($.eventName = UpdateOrganizationalUnit)) }',\n ],\n ];\n\n for (const [name, filterPattern] of alarmDefinitions) {\n this.createMetricAlarm(name, filterPattern);\n }\n\n // Debug\n console.log('LogGroup', this.logGroup?.node.id);\n console.log('CloudTrail', this.cloudTrail?.node.id);\n console.log('AlarmTopic', this.alarmTopic?.node.id);\n }\n\n createMetricAlarm(name: string, filterPattern: string): void {\n const filterName = `${this.config.stackName}${name}MetricFilter`;\n const metricName = `${this.config.stackName}${name}Metric`;\n const metricNamespace = `${this.config.stackName}Metrics`;\n const alarmName = `${this.config.stackName}${name}Alarm`;\n\n const metricFilter = new logs.MetricFilter(this, filterName, {\n logGroup: this.logGroup as logs.ILogGroup,\n filterPattern: { logPatternString: filterPattern },\n metricNamespace,\n metricName,\n });\n\n const alarm = new cloudwatch.Alarm(this, alarmName, {\n metric: metricFilter.metric({}),\n threshold: 1,\n evaluationPeriods: 1,\n alarmName,\n actionsEnabled: true,\n treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,\n comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,\n datapointsToAlarm: 1,\n });\n\n alarm.addAlarmAction(new cloudwatch_actions.SnsAction(this.alarmTopic as sns.ITopic));\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["src_exports", "__export", "main", "__toCommonJS", "import_aws_cdk_lib", "import_fs", "import_path", "import_aws_cdk_lib", "import_aws_ecr", "import_aws_rds", "import_constructs", "awsManagedRules", "BackEnd", "scope", "config", "name", "vpc", "ec2", "vpcFlowLogs", "logs", "botLambdaRole", "iam", "rdsCluster", "rdsSecretsArn", "instanceProps", "readers", "i", "rds", "redisSubnetGroup", "elasticache", "subnet", "redisSecurityGroup", "redisPassword", "secretsmanager", "redisCluster", "redisSecrets", "cluster", "ecs", "taskRolePolicies", "taskRole", "taskDefinition", "logGroup", "logDriver", "container", "fargateSecurityGroup", "fargateService", "targetGroup", "elbv2", "loadBalancer", "s3", "waf", "wafv2", "awsManagedRules", "wafAssociation", "record", "zone", "route53", "targets", "databaseSecrets", "ssm", "redisSecretsParameter", "botLambdaRoleParameter", "imageName", "nameTagMatches", "serverImageName", "serverImageTag", "ecrRepo", "import_aws_cdk_lib", "import_constructs", "import_aws_cdk_lib", "grantBucketAccessToOriginAccessIdentity", "bucket", "identity", "policyStatement", "iam", "FrontEnd", "parent", "config", "region", "appBucket", "s3", "responseHeadersPolicy", "cloudfront", "waf", "wafv2", "awsManagedRules", "apiOriginCachePolicy", "originAccessIdentity", "grantBucketAccessToOriginAccessIdentity", "distribution", "origins", "acm", "record", "zone", "route53", "targets", "import_aws_cdk_lib", "import_cdk_serverless_clamscan", "import_constructs", "Storage", "parent", "config", "region", "storageBucket", "s3", "publicKey", "cloudfront", "keyGroup", "responseHeadersPolicy", "waf", "wafv2", "awsManagedRules", "originAccessIdentity", "grantBucketAccessToOriginAccessIdentity", "distribution", "origins", "acm", "record", "zone", "route53", "targets", "import_aws_cdk_lib", "import_constructs", "CloudTrailAlarms", "scope", "config", "logs", "cloudtrail", "sns", "alarmDefinitions", "name", "filterPattern", "filterName", "metricName", "metricNamespace", "alarmName", "metricFilter", "cloudwatch", "cloudwatch_actions", "MedplumStack", "scope", "config", "BackEnd", "FrontEnd", "Storage", "CloudTrailAlarms", "usEast1Stack", "main", "context", "app", "configFileName", "stack"]
3
+ "sources": ["../../src/index.ts", "../../src/stack.ts", "../../src/backend.ts", "../../src/waf.ts", "../../src/cloudtrail.ts", "../../src/frontend.ts", "../../src/oai.ts", "../../src/storage.ts"],
4
+ "sourcesContent": ["import { MedplumInfraConfig } from '@medplum/core';\nimport { App } from 'aws-cdk-lib';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\nimport { MedplumStack } from './stack';\n\nexport * from './backend';\nexport * from './cloudtrail';\nexport * from './frontend';\nexport * from './stack';\nexport * from './storage';\nexport * from './waf';\n\nexport function main(context?: Record<string, string>): void {\n const app = new App({ context });\n\n const configFileName = app.node.tryGetContext('config');\n if (!configFileName) {\n console.log('Missing \"config\" context variable');\n console.log('Usage: cdk deploy -c config=my-config.json');\n return;\n }\n\n const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as MedplumInfraConfig;\n\n const stack = new MedplumStack(app, config);\n console.log('Stack', stack.primaryStack.stackId);\n\n app.synth();\n}\n\nif (require.main === module) {\n main();\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport { App, Stack, Tags } from 'aws-cdk-lib';\nimport { BackEnd } from './backend';\nimport { CloudTrailAlarms } from './cloudtrail';\nimport { FrontEnd } from './frontend';\nimport { Storage } from './storage';\n\nexport class MedplumStack {\n primaryStack: MedplumPrimaryStack;\n globalStack?: MedplumGlobalStack;\n\n constructor(scope: App, config: MedplumInfraConfig) {\n this.primaryStack = new MedplumPrimaryStack(scope, config);\n\n if (config.region !== 'us-east-1') {\n // Some resources must be created in us-east-1\n // For example, CloudFront distributions and ACM certificates\n // If the primary region is not us-east-1, create these resources in us-east-1\n this.globalStack = new MedplumGlobalStack(scope, config);\n this.globalStack.addDependency(this.primaryStack);\n }\n }\n}\n\nexport class MedplumPrimaryStack extends Stack {\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\n cloudTrail: CloudTrailAlarms;\n\n constructor(scope: App, config: MedplumInfraConfig) {\n super(scope, config.stackName, {\n env: {\n region: config.region,\n account: config.accountNumber,\n },\n });\n Tags.of(this).add('medplum:environment', config.name);\n\n this.backEnd = new BackEnd(this, config);\n this.frontEnd = new FrontEnd(this, config, config.region);\n this.storage = new Storage(this, config, config.region);\n this.cloudTrail = new CloudTrailAlarms(this, config);\n }\n}\n\nexport class MedplumGlobalStack extends Stack {\n frontEnd: FrontEnd;\n storage: Storage;\n cloudTrail: CloudTrailAlarms;\n\n constructor(scope: App, config: MedplumInfraConfig) {\n super(scope, config.stackName + '-us-east-1', {\n env: {\n region: 'us-east-1',\n account: config.accountNumber,\n },\n });\n Tags.of(this).add('medplum:environment', config.name);\n\n this.frontEnd = new FrontEnd(this, config, 'us-east-1');\n this.storage = new Storage(this, config, 'us-east-1');\n this.cloudTrail = new CloudTrailAlarms(this, config);\n }\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n Duration,\n aws_ec2 as ec2,\n aws_ecs as ecs,\n aws_elasticache as elasticache,\n aws_elasticloadbalancingv2 as elbv2,\n aws_iam as iam,\n aws_logs as logs,\n aws_rds as rds,\n RemovalPolicy,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_secretsmanager as secretsmanager,\n aws_ssm as ssm,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { Repository } from 'aws-cdk-lib/aws-ecr';\nimport { ClusterInstance } from 'aws-cdk-lib/aws-rds';\nimport { Construct } from 'constructs';\nimport { awsManagedRules } from './waf';\n\n/**\n * Based on: https://github.com/aws-samples/http-api-aws-fargate-cdk/blob/master/cdk/singleAccount/lib/fargate-vpclink-stack.ts\n *\n * RDS config: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-rds-readme.html\n */\nexport class BackEnd extends Construct {\n vpc: ec2.IVpc;\n botLambdaRole: iam.IRole;\n rdsSecretsArn?: string;\n rdsCluster?: rds.DatabaseCluster;\n redisSubnetGroup: elasticache.CfnSubnetGroup;\n redisSecurityGroup: ec2.SecurityGroup;\n redisPassword: secretsmanager.ISecret;\n redisCluster: elasticache.CfnReplicationGroup;\n redisSecrets: secretsmanager.ISecret;\n ecsCluster: ecs.Cluster;\n taskRolePolicies: iam.PolicyDocument;\n taskRole: iam.Role;\n taskDefinition: ecs.FargateTaskDefinition;\n logGroup: logs.ILogGroup;\n logDriver: ecs.AwsLogDriver;\n serviceContainer: ecs.ContainerDefinition;\n fargateSecurityGroup: ec2.SecurityGroup;\n fargateService: ecs.FargateService;\n targetGroup: elbv2.ApplicationTargetGroup;\n loadBalancer: elbv2.ApplicationLoadBalancer;\n waf: wafv2.CfnWebACL;\n wafAssociation: wafv2.CfnWebACLAssociation;\n dnsRecord?: route53.ARecord;\n regionParameter: ssm.StringParameter;\n databaseSecretsParameter: ssm.StringParameter;\n redisSecretsParameter: ssm.StringParameter;\n botLambdaRoleParameter: ssm.StringParameter;\n\n constructor(scope: Construct, config: MedplumInfraConfig) {\n super(scope, 'BackEnd');\n\n const name = config.name;\n\n // VPC\n if (config.vpcId) {\n // Lookup VPC by ARN\n this.vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: config.vpcId });\n } else {\n // VPC Flow Logs\n const vpcFlowLogs = new logs.LogGroup(this, 'VpcFlowLogs', {\n logGroupName: '/medplum/flowlogs/' + name,\n removalPolicy: RemovalPolicy.DESTROY,\n });\n\n // Create VPC\n this.vpc = new ec2.Vpc(this, 'VPC', {\n maxAzs: config.maxAzs,\n flowLogs: {\n cloudwatch: {\n destination: ec2.FlowLogDestination.toCloudWatchLogs(vpcFlowLogs),\n trafficType: ec2.FlowLogTrafficType.ALL,\n },\n },\n });\n }\n\n // Bot Lambda Role\n this.botLambdaRole = new iam.Role(this, 'BotLambdaRole', {\n assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),\n });\n\n // RDS\n this.rdsSecretsArn = config.rdsSecretsArn;\n if (!this.rdsSecretsArn) {\n // See: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds-readme.html#migrating-from-instanceprops\n const instanceProps: rds.ProvisionedClusterInstanceProps = {\n instanceType: config.rdsInstanceType ? new ec2.InstanceType(config.rdsInstanceType) : undefined,\n enablePerformanceInsights: true,\n isFromLegacyInstanceProps: true,\n };\n\n let readers = undefined;\n if (config.rdsInstances > 1) {\n readers = [];\n for (let i = 0; i < config.rdsInstances - 1; i++) {\n readers.push(\n ClusterInstance.provisioned('Instance' + (i + 2), {\n ...instanceProps,\n })\n );\n }\n }\n\n this.rdsCluster = new rds.DatabaseCluster(this, 'DatabaseCluster', {\n engine: rds.DatabaseClusterEngine.auroraPostgres({\n version: rds.AuroraPostgresEngineVersion.VER_12_9,\n }),\n credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'),\n defaultDatabaseName: 'medplum',\n storageEncrypted: true,\n vpc: this.vpc,\n vpcSubnets: {\n subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,\n },\n writer: ClusterInstance.provisioned('Instance1', {\n ...instanceProps,\n }),\n readers,\n backup: {\n retention: Duration.days(7),\n },\n cloudwatchLogsExports: ['postgresql'],\n instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,\n });\n\n this.rdsSecretsArn = (this.rdsCluster.secret as secretsmanager.ISecret).secretArn;\n }\n\n // Redis\n // Important: For HIPAA compliance, you must specify TransitEncryptionEnabled as true, an AuthToken, and a CacheSubnetGroup.\n this.redisSubnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnetGroup', {\n description: 'Redis Subnet Group',\n subnetIds: this.vpc.privateSubnets.map((subnet) => subnet.subnetId),\n });\n\n this.redisSecurityGroup = new ec2.SecurityGroup(this, 'RedisSecurityGroup', {\n vpc: this.vpc,\n description: 'Redis Security Group',\n allowAllOutbound: false,\n });\n\n this.redisPassword = new secretsmanager.Secret(this, 'RedisPassword', {\n generateSecretString: {\n secretStringTemplate: '{}',\n generateStringKey: 'password',\n excludeCharacters: '@%*()_+=`~{}|[]\\\\:\";\\'?,./',\n },\n });\n\n this.redisCluster = new elasticache.CfnReplicationGroup(this, 'RedisCluster', {\n engine: 'Redis',\n engineVersion: '6.x',\n cacheNodeType: config.cacheNodeType ?? 'cache.t2.medium',\n replicationGroupDescription: 'RedisReplicationGroup',\n authToken: this.redisPassword.secretValueFromJson('password').toString(),\n transitEncryptionEnabled: true,\n atRestEncryptionEnabled: true,\n multiAzEnabled: true,\n cacheSubnetGroupName: this.redisSubnetGroup.ref,\n numNodeGroups: 1,\n replicasPerNodeGroup: 1,\n securityGroupIds: [this.redisSecurityGroup.securityGroupId],\n });\n this.redisCluster.node.addDependency(this.redisPassword);\n\n this.redisSecrets = new secretsmanager.Secret(this, 'RedisSecrets', {\n generateSecretString: {\n secretStringTemplate: JSON.stringify({\n host: this.redisCluster.attrPrimaryEndPointAddress,\n port: this.redisCluster.attrPrimaryEndPointPort,\n password: this.redisPassword.secretValueFromJson('password').toString(),\n tls: {},\n }),\n generateStringKey: 'unused',\n },\n });\n this.redisSecrets.node.addDependency(this.redisPassword);\n this.redisSecrets.node.addDependency(this.redisCluster);\n\n // ECS Cluster\n this.ecsCluster = new ecs.Cluster(this, 'Cluster', {\n vpc: this.vpc,\n });\n\n // Task Policies\n this.taskRolePolicies = new iam.PolicyDocument({\n statements: [\n // CloudWatch Logs: Create streams and put events\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],\n resources: ['arn:aws:logs:*'],\n }),\n\n // Secrets Manager: Read only access to secrets\n // https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: [\n 'secretsmanager:GetResourcePolicy',\n 'secretsmanager:GetSecretValue',\n 'secretsmanager:DescribeSecret',\n 'secretsmanager:ListSecrets',\n 'secretsmanager:ListSecretVersionIds',\n ],\n resources: ['arn:aws:secretsmanager:*'],\n }),\n\n // Parameter Store: Read only access\n // https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'],\n resources: ['arn:aws:ssm:*'],\n }),\n\n // SES: Send emails\n // https://docs.aws.amazon.com/ses/latest/dg/sending-authorization-policy-examples.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['ses:SendEmail', 'ses:SendRawEmail'],\n resources: ['arn:aws:ses:*'],\n }),\n\n // S3: Read and write access to buckets\n // https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['s3:ListBucket', 's3:GetObject', 's3:PutObject', 's3:DeleteObject'],\n resources: ['arn:aws:s3:::*'],\n }),\n\n // IAM: Pass role to innvoke lambda functions\n // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: ['iam:ListRoles', 'iam:GetRole', 'iam:PassRole'],\n resources: [this.botLambdaRole.roleArn],\n }),\n\n // Lambda: Create, read, update, delete, and invoke functions\n // https://docs.aws.amazon.com/lambda/latest/dg/access-control-identity-based.html\n new iam.PolicyStatement({\n effect: iam.Effect.ALLOW,\n actions: [\n 'lambda:CreateFunction',\n 'lambda:GetFunction',\n 'lambda:GetFunctionConfiguration',\n 'lambda:UpdateFunctionCode',\n 'lambda:UpdateFunctionConfiguration',\n 'lambda:ListLayerVersions',\n 'lambda:GetLayerVersion',\n 'lambda:InvokeFunction',\n ],\n resources: ['arn:aws:lambda:*'],\n }),\n ],\n });\n\n // Task Role\n this.taskRole = new iam.Role(this, 'TaskExecutionRole', {\n assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),\n description: 'Medplum Server Task Execution Role',\n inlinePolicies: {\n TaskExecutionPolicies: this.taskRolePolicies,\n },\n });\n\n // Task Definitions\n this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {\n memoryLimitMiB: config.serverMemory,\n cpu: config.serverCpu,\n taskRole: this.taskRole,\n });\n\n // Log Groups\n this.logGroup = new logs.LogGroup(this, 'LogGroup', {\n logGroupName: '/ecs/medplum/' + name,\n removalPolicy: RemovalPolicy.DESTROY,\n });\n\n this.logDriver = new ecs.AwsLogDriver({\n logGroup: this.logGroup,\n streamPrefix: 'Medplum',\n });\n\n // Task Containers\n this.serviceContainer = this.taskDefinition.addContainer('MedplumTaskDefinition', {\n image: this.getContainerImage(config, config.serverImage),\n command: [config.region === 'us-east-1' ? `aws:/medplum/${name}/` : `aws:${config.region}:/medplum/${name}/`],\n logging: this.logDriver,\n });\n\n this.serviceContainer.addPortMappings({\n containerPort: config.apiPort,\n hostPort: config.apiPort,\n });\n\n if (config.additionalContainers) {\n for (const container of config.additionalContainers) {\n this.taskDefinition.addContainer('AdditionalContainer-' + container.name, {\n containerName: container.name,\n image: this.getContainerImage(config, container.image),\n command: container.command,\n environment: container.environment,\n logging: this.logDriver,\n });\n }\n }\n\n // Security Groups\n this.fargateSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {\n allowAllOutbound: true,\n securityGroupName: 'MedplumSecurityGroup',\n vpc: this.vpc,\n });\n\n // Fargate Services\n this.fargateService = new ecs.FargateService(this, 'FargateService', {\n cluster: this.ecsCluster,\n taskDefinition: this.taskDefinition,\n assignPublicIp: false,\n vpcSubnets: {\n subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,\n },\n desiredCount: config.desiredServerCount,\n securityGroups: [this.fargateSecurityGroup],\n healthCheckGracePeriod: Duration.minutes(5),\n });\n\n // Add dependencies - make sure Fargate service is created after RDS and Redis\n if (this.rdsCluster) {\n this.fargateService.node.addDependency(this.rdsCluster);\n }\n this.fargateService.node.addDependency(this.redisCluster);\n\n // Load Balancer Target Group\n this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {\n vpc: this.vpc,\n port: config.apiPort,\n protocol: elbv2.ApplicationProtocol.HTTP,\n healthCheck: {\n path: '/healthcheck',\n interval: Duration.seconds(30),\n timeout: Duration.seconds(3),\n healthyThresholdCount: 2,\n unhealthyThresholdCount: 5,\n },\n targets: [this.fargateService],\n });\n\n // Load Balancer\n this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {\n vpc: this.vpc,\n internetFacing: config.apiInternetFacing !== false, // default true\n http2Enabled: true,\n });\n\n if (config.loadBalancerLoggingBucket) {\n // Load Balancer logging\n this.loadBalancer.logAccessLogs(\n s3.Bucket.fromBucketName(this, 'LoggingBucket', config.loadBalancerLoggingBucket),\n config.loadBalancerLoggingPrefix\n );\n }\n\n // HTTPS Listener\n // Forward to the target group\n this.loadBalancer.addListener('HttpsListener', {\n port: 443,\n certificates: [\n {\n certificateArn: config.apiSslCertArn,\n },\n ],\n sslPolicy: elbv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,\n defaultAction: elbv2.ListenerAction.forward([this.targetGroup]),\n });\n\n // WAF\n this.waf = new wafv2.CfnWebACL(this, 'BackEndWAF', {\n defaultAction: { allow: {} },\n scope: 'REGIONAL',\n name: `${config.stackName}-BackEndWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-BackEndWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // Create an association between the load balancer and the WAF\n this.wafAssociation = new wafv2.CfnWebACLAssociation(this, 'LoadBalancerAssociation', {\n resourceArn: this.loadBalancer.loadBalancerArn,\n webAclArn: this.waf.attrArn,\n });\n\n // Grant RDS access to the fargate group\n if (this.rdsCluster) {\n this.rdsCluster.connections.allowDefaultPortFrom(this.fargateSecurityGroup);\n }\n\n // Grant Redis access to the fargate group\n this.redisSecurityGroup.addIngressRule(this.fargateSecurityGroup, ec2.Port.tcp(6379));\n\n // DNS\n if (!config.skipDns) {\n // Route 53\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the load balancer\n this.dnsRecord = new route53.ARecord(this, 'LoadBalancerAliasRecord', {\n recordName: config.apiDomainName,\n target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(this.loadBalancer)),\n zone: zone,\n });\n }\n\n // SSM Parameters\n this.regionParameter = new ssm.StringParameter(this, 'RegionParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/awsRegion`,\n description: 'AWS region',\n stringValue: config.region,\n });\n\n this.databaseSecretsParameter = new ssm.StringParameter(this, 'DatabaseSecretsParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/DatabaseSecrets`,\n description: 'Database secrets ARN',\n stringValue: this.rdsSecretsArn,\n });\n\n this.redisSecretsParameter = new ssm.StringParameter(this, 'RedisSecretsParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/RedisSecrets`,\n description: 'Redis secrets ARN',\n stringValue: this.redisSecrets.secretArn,\n });\n\n this.botLambdaRoleParameter = new ssm.StringParameter(this, 'BotLambdaRoleParameter', {\n tier: ssm.ParameterTier.STANDARD,\n parameterName: `/medplum/${name}/botLambdaRoleArn`,\n description: 'Bot lambda execution role ARN',\n stringValue: this.botLambdaRole.roleArn,\n });\n }\n\n /**\n * Returns a container image for the given image name.\n * If the image name is an ECR image, then the image will be pulled from ECR.\n * Otherwise, the image name is assumed to be a Docker Hub image.\n * @param config The config settings (account number and region).\n * @param imageName The image name.\n * @returns The container image.\n */\n private getContainerImage(config: MedplumInfraConfig, imageName: string): ecs.ContainerImage {\n // Pull out the image name and tag from the image URI if it's an ECR image\n const ecrImageUriRegex = new RegExp(\n `^${config.accountNumber}\\\\.dkr\\\\.ecr\\\\.${config.region}\\\\.amazonaws\\\\.com/(.*)[:@](.*)$`\n );\n const nameTagMatches = ecrImageUriRegex.exec(imageName);\n const serverImageName = nameTagMatches?.[1];\n const serverImageTag = nameTagMatches?.[2];\n if (serverImageName && serverImageTag) {\n // Creating an ecr repository image will automatically grant fine-grained permissions to ecs to access the image\n const ecrRepo = Repository.fromRepositoryArn(\n this,\n 'ServerImageRepo',\n `arn:aws:ecr:${config.region}:${config.accountNumber}:repository/${serverImageName}`\n );\n return ecs.ContainerImage.fromEcrRepository(ecrRepo, serverImageTag);\n }\n\n // Otherwise, use the standard container image\n return ecs.ContainerImage.fromRegistry(imageName);\n }\n}\n", "// Based on https://gist.github.com/statik/f1ac9d6227d98d30c7a7cec0c83f4e64\n\nimport { aws_wafv2 as wafv2 } from 'aws-cdk-lib';\n\nexport const awsManagedRules: wafv2.CfnWebACL.RuleProperty[] = [\n // Common Rule Set aligns with major portions of OWASP Core Rule Set\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html\n {\n name: 'AWS-AWSManagedRulesCommonRuleSet',\n priority: 10,\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesCommonRuleSet',\n // Excluding generic RFI body rule for sns notifications\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html\n excludedRules: [\n { name: 'NoUserAgent_HEADER' },\n { name: 'UserAgent_BadBots_HEADER' },\n { name: 'SizeRestrictions_QUERYSTRING' },\n { name: 'SizeRestrictions_Cookie_HEADER' },\n { name: 'SizeRestrictions_BODY' },\n { name: 'SizeRestrictions_URIPATH' },\n { name: 'EC2MetaDataSSRF_BODY' },\n { name: 'EC2MetaDataSSRF_COOKIE' },\n { name: 'EC2MetaDataSSRF_URIPATH' },\n { name: 'EC2MetaDataSSRF_QUERYARGUMENTS' },\n { name: 'GenericLFI_QUERYARGUMENTS' },\n { name: 'GenericLFI_URIPATH' },\n { name: 'GenericLFI_BODY' },\n { name: 'RestrictedExtensions_URIPATH' },\n { name: 'RestrictedExtensions_QUERYARGUMENTS' },\n { name: 'GenericRFI_QUERYARGUMENTS' },\n { name: 'GenericRFI_BODY' },\n { name: 'GenericRFI_URIPATH' },\n { name: 'CrossSiteScripting_COOKIE' },\n { name: 'CrossSiteScripting_QUERYARGUMENTS' },\n { name: 'CrossSiteScripting_BODY' },\n { name: 'CrossSiteScripting_URIPATH' },\n ],\n },\n },\n overrideAction: {\n count: {},\n },\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWS-AWSManagedRulesCommonRuleSet',\n },\n },\n // AWS IP Reputation list includes known malicious actors/bots and is regularly updated\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html\n {\n name: 'AWS-AWSManagedRulesAmazonIpReputationList',\n priority: 20,\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesAmazonIpReputationList',\n excludedRules: [{ name: 'AWSManagedIPReputationList' }, { name: 'AWSManagedReconnaissanceList' }],\n },\n },\n overrideAction: {\n count: {},\n },\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRulesAmazonIpReputationList',\n },\n },\n // Blocks common SQL Injection\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-sql-db\n {\n name: 'AWSManagedRulesSQLiRuleSet',\n priority: 30,\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRulesSQLiRuleSet',\n },\n overrideAction: {\n count: {},\n },\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesSQLiRuleSet',\n excludedRules: [\n { name: 'SQLi_QUERYARGUMENTS' },\n { name: 'SQLiExtendedPatterns_QUERYARGUMENTS' },\n { name: 'SQLi_BODY' },\n { name: 'SQLiExtendedPatterns_BODY' },\n { name: 'SQLi_COOKIE' },\n { name: 'SQLi_URIPATH' },\n ],\n },\n },\n },\n // Blocks attacks targeting LFI(Local File Injection) for linux systems\n // https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-linux-os\n {\n name: 'AWSManagedRuleLinux',\n priority: 40,\n visibilityConfig: {\n sampledRequestsEnabled: true,\n cloudWatchMetricsEnabled: true,\n metricName: 'AWSManagedRuleLinux',\n },\n overrideAction: {\n count: {},\n },\n statement: {\n managedRuleGroupStatement: {\n vendorName: 'AWS',\n name: 'AWSManagedRulesLinuxRuleSet',\n excludedRules: [{ name: 'LFI_URIPATH' }, { name: 'LFI_QUERYSTRING' }, { name: 'LFI_COOKIE' }],\n },\n },\n },\n];\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_cloudtrail as cloudtrail,\n aws_cloudwatch as cloudwatch,\n aws_cloudwatch_actions as cloudwatch_actions,\n aws_logs as logs,\n aws_sns as sns,\n} from 'aws-cdk-lib';\nimport { Construct } from 'constructs';\n\nexport class CloudTrailAlarms extends Construct {\n config: MedplumInfraConfig;\n logGroup?: logs.ILogGroup;\n cloudTrail?: cloudtrail.Trail;\n alarmTopic?: sns.ITopic;\n\n constructor(scope: Construct, config: MedplumInfraConfig) {\n super(scope, 'CloudTrailAlarms');\n this.config = config;\n\n // CloudTrail is optional\n if (!config.cloudTrailAlarms) {\n return;\n }\n\n // Get the CloudTrail log group\n // This can be created or imported by name\n if (config.cloudTrailAlarms.logGroupCreate) {\n this.logGroup = new logs.LogGroup(this, 'CloudTrailLogGroup', {\n logGroupName: config.cloudTrailAlarms.logGroupName,\n retention: logs.RetentionDays.ONE_YEAR,\n });\n this.cloudTrail = new cloudtrail.Trail(this, 'CloudTrail', {\n sendToCloudWatchLogs: true,\n cloudWatchLogGroup: this.logGroup,\n includeGlobalServiceEvents: true,\n });\n } else {\n this.logGroup = logs.LogGroup.fromLogGroupName(this, 'CloudTrailLogGroup', config.cloudTrailAlarms.logGroupName);\n }\n\n // Get the SNS Topic\n // This can be created or imported by name\n if (config.cloudTrailAlarms.snsTopicArn) {\n this.alarmTopic = sns.Topic.fromTopicArn(this, 'AlarmTopic', config.cloudTrailAlarms.snsTopicArn);\n } else {\n this.alarmTopic = new sns.Topic(this, 'AlarmTopic', { topicName: config.cloudTrailAlarms.snsTopicName });\n }\n const alarmDefinitions = [\n ['UnauthorizedApiCalls', '{ ($.errorCode = *UnauthorizedOperation) || ($.errorCode = AccessDenied*) }'],\n ['SignInWithoutMfa', '{ ($.eventName = ConsoleLogin) && ($.additionalEventData.MFAUsed != Yes) }'],\n [\n 'RootAccountUsage',\n '{ $.userIdentity.type = Root && $.userIdentity.invokedBy NOT EXISTS && $.eventType != AwsServiceEvent }',\n ],\n [\n 'IamPolicyChanges',\n '{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}',\n ],\n [\n 'CloudTrailConfigurationChanges',\n '{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }',\n ],\n ['SignInFailures', '{ ($.eventName = ConsoleLogin) && ($.errorMessage = \"Failed authentication\") }'],\n [\n 'DisabledCmks',\n '{($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion)) }',\n ],\n [\n 'S3PolicyChanges',\n '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }',\n ],\n [\n 'ConfigServiceChanges',\n '{($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder))}',\n ],\n [\n 'SecurityGroupChanges',\n '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}',\n ],\n [\n 'NetworkAclChanges',\n '{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }',\n ],\n [\n 'NetworkGatewayChanges',\n '{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }',\n ],\n [\n 'RouteTableChanges',\n '{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }',\n ],\n [\n 'VpcChanges',\n '{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }',\n ],\n [\n 'OrganizationsChanges',\n '{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = AcceptHandshake) || ($.eventName = AttachPolicy) || ($.eventName = CreateAccount) || ($.eventName = CreateOrganizationalUnit) || ($.eventName = CreatePolicy) || ($.eventName = DeclineHandshake) || ($.eventName = DeleteOrganization) || ($.eventName = DeleteOrganizationalUnit) || ($.eventName = DeletePolicy) || ($.eventName = DetachPolicy) || ($.eventName = DisablePolicyType) || ($.eventName = EnablePolicyType) || ($.eventName = InviteAccountToOrganization) || ($.eventName = LeaveOrganization) || ($.eventName = MoveAccount) || ($.eventName = RemoveAccountFromOrganization) || ($.eventName = UpdatePolicy) || ($.eventName = UpdateOrganizationalUnit)) }',\n ],\n ];\n\n for (const [name, filterPattern] of alarmDefinitions) {\n this.createMetricAlarm(name, filterPattern);\n }\n }\n\n createMetricAlarm(name: string, filterPattern: string): void {\n const filterName = `${this.config.stackName}${name}MetricFilter`;\n const metricName = `${this.config.stackName}${name}Metric`;\n const metricNamespace = `${this.config.stackName}Metrics`;\n const alarmName = `${this.config.stackName}${name}Alarm`;\n\n const metricFilter = new logs.MetricFilter(this, filterName, {\n logGroup: this.logGroup as logs.ILogGroup,\n filterPattern: { logPatternString: filterPattern },\n metricNamespace,\n metricName,\n });\n\n const alarm = new cloudwatch.Alarm(this, alarmName, {\n metric: metricFilter.metric({}),\n threshold: 1,\n evaluationPeriods: 1,\n alarmName,\n actionsEnabled: true,\n treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,\n comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,\n datapointsToAlarm: 1,\n });\n\n alarm.addAlarmAction(new cloudwatch_actions.SnsAction(this.alarmTopic as sns.ITopic));\n }\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_certificatemanager as acm,\n aws_cloudfront as cloudfront,\n Duration,\n aws_iam as iam,\n aws_cloudfront_origins as origins,\n RemovalPolicy,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { Construct } from 'constructs';\nimport { grantBucketAccessToOriginAccessIdentity } from './oai';\nimport { awsManagedRules } from './waf';\n\n/**\n * Static app infrastructure, which deploys app content to an S3 bucket.\n *\n * The app redirects from HTTP to HTTPS, using a CloudFront distribution,\n * Route53 alias record, and ACM certificate.\n */\nexport class FrontEnd extends Construct {\n appBucket: s3.IBucket;\n responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;\n waf?: wafv2.CfnWebACL;\n apiOriginCachePolicy?: cloudfront.ICachePolicy;\n originAccessIdentity?: cloudfront.OriginAccessIdentity;\n originAccessPolicyStatement?: iam.PolicyStatement;\n distribution?: cloudfront.IDistribution;\n dnsRecord?: route53.IRecordSet;\n\n constructor(parent: Construct, config: MedplumInfraConfig, region: string) {\n super(parent, 'FrontEnd');\n\n if (region === config.region) {\n // S3 bucket\n this.appBucket = new s3.Bucket(this, 'AppBucket', {\n bucketName: config.appDomainName,\n publicReadAccess: false,\n blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,\n removalPolicy: RemovalPolicy.DESTROY,\n encryption: s3.BucketEncryption.S3_MANAGED,\n enforceSSL: true,\n versioned: true,\n });\n } else {\n // Otherwise, reference the bucket by name and region\n this.appBucket = s3.Bucket.fromBucketAttributes(this, 'AppBucket', {\n bucketName: config.appDomainName,\n region: config.region,\n });\n }\n\n if (region === 'us-east-1') {\n // HTTP response headers policy\n this.responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {\n securityHeadersBehavior: {\n contentSecurityPolicy: {\n contentSecurityPolicy: [\n `default-src 'none'`,\n `base-uri 'self'`,\n `child-src 'self'`,\n `connect-src 'self' ${config.apiDomainName} *.google.com`,\n `font-src 'self' fonts.gstatic.com`,\n `form-action 'self' *.gstatic.com *.google.com`,\n `frame-ancestors 'none'`,\n `frame-src 'self' *.medplum.com *.gstatic.com *.google.com`,\n `img-src 'self' data: ${config.storageDomainName} *.gstatic.com *.google.com *.googleapis.com`,\n `manifest-src 'self'`,\n `media-src 'self' ${config.storageDomainName}`,\n `script-src 'self' *.medplum.com *.gstatic.com *.google.com`,\n `style-src 'self' 'unsafe-inline' *.medplum.com *.gstatic.com *.google.com`,\n `worker-src 'self' blob: *.gstatic.com *.google.com`,\n `upgrade-insecure-requests`,\n ].join('; '),\n override: true,\n },\n contentTypeOptions: { override: true },\n frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },\n strictTransportSecurity: {\n accessControlMaxAge: Duration.seconds(63072000),\n includeSubdomains: true,\n override: true,\n },\n xssProtection: {\n protection: true,\n modeBlock: true,\n override: true,\n },\n },\n });\n\n // WAF\n this.waf = new wafv2.CfnWebACL(this, 'FrontEndWAF', {\n defaultAction: { allow: {} },\n scope: 'CLOUDFRONT',\n name: `${config.stackName}-FrontEndWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-FrontEndWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // API Origin Cache Policy\n this.apiOriginCachePolicy = new cloudfront.CachePolicy(this, 'ApiOriginCachePolicy', {\n cachePolicyName: `${config.stackName}-ApiOriginCachePolicy`,\n cookieBehavior: cloudfront.CacheCookieBehavior.all(),\n headerBehavior: cloudfront.CacheHeaderBehavior.allowList(\n 'Authorization',\n 'Content-Encoding',\n 'Content-Type',\n 'If-None-Match',\n 'Origin',\n 'Referer',\n 'User-Agent',\n 'X-Medplum'\n ),\n queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),\n });\n\n // Origin access identity\n this.originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});\n this.originAccessPolicyStatement = grantBucketAccessToOriginAccessIdentity(\n this.appBucket,\n this.originAccessIdentity\n );\n\n // CloudFront distribution\n this.distribution = new cloudfront.Distribution(this, 'AppDistribution', {\n defaultRootObject: 'index.html',\n defaultBehavior: {\n origin: new origins.S3Origin(this.appBucket, {\n originAccessIdentity: this.originAccessIdentity,\n }),\n responseHeadersPolicy: this.responseHeadersPolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n },\n additionalBehaviors: config.appApiProxy\n ? {\n '/api/*': {\n origin: new origins.HttpOrigin(config.apiDomainName),\n allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,\n cachePolicy: this.apiOriginCachePolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n },\n }\n : undefined,\n certificate: acm.Certificate.fromCertificateArn(this, 'AppCertificate', config.appSslCertArn),\n domainNames: [config.appDomainName],\n errorResponses: [\n {\n httpStatus: 403,\n responseHttpStatus: 200,\n responsePagePath: '/index.html',\n },\n {\n httpStatus: 404,\n responseHttpStatus: 200,\n responsePagePath: '/index.html',\n },\n ],\n webAclId: this.waf.attrArn,\n logBucket: config.appLoggingBucket\n ? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.appLoggingBucket)\n : undefined,\n logFilePrefix: config.appLoggingPrefix,\n });\n\n // DNS\n if (!config.skipDns) {\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the CloudFront distribution\n this.dnsRecord = new route53.ARecord(this, 'AppAliasRecord', {\n recordName: config.appDomainName,\n target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),\n zone,\n });\n }\n }\n }\n}\n", "import { aws_cloudfront as cloudfront, aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib';\n\n/**\n * Grants S3 bucket read access to the CloudFront Origin Access Identity (OAI).\n *\n * Under normal circumstances, where CDK creates both the S3 bucket and the OAI,\n * you can achieve this same behavior by simply calling:\n *\n * bucket.grantRead(identity);\n *\n * However, if importing an S3 bucket via `s3.Bucket.fromBucketAttributes()`, that does not work.\n *\n * See: https://stackoverflow.com/a/60917015\n *\n * @param bucket The S3 bucket.\n * @param identity The CloudFront Origin Access Identity.\n * @returns The policy statement.\n */\nexport function grantBucketAccessToOriginAccessIdentity(\n bucket: s3.IBucket,\n identity: cloudfront.OriginAccessIdentity\n): iam.PolicyStatement {\n const policyStatement = new iam.PolicyStatement();\n policyStatement.addActions('s3:GetObject*');\n policyStatement.addActions('s3:GetBucket*');\n policyStatement.addActions('s3:List*');\n policyStatement.addResources(bucket.bucketArn);\n policyStatement.addResources(`${bucket.bucketArn}/*`);\n policyStatement.addCanonicalUserPrincipal(identity.cloudFrontOriginAccessIdentityS3CanonicalUserId);\n bucket.addToResourcePolicy(policyStatement);\n return policyStatement;\n}\n", "import { MedplumInfraConfig } from '@medplum/core';\nimport {\n aws_certificatemanager as acm,\n aws_cloudfront as cloudfront,\n Duration,\n aws_iam as iam,\n aws_cloudfront_origins as origins,\n aws_route53 as route53,\n aws_s3 as s3,\n aws_route53_targets as targets,\n aws_wafv2 as wafv2,\n} from 'aws-cdk-lib';\nimport { ServerlessClamscan } from 'cdk-serverless-clamscan';\nimport { Construct } from 'constructs';\nimport { grantBucketAccessToOriginAccessIdentity } from './oai';\nimport { awsManagedRules } from './waf';\n\n/**\n * Binary storage bucket and CloudFront distribution.\n */\nexport class Storage extends Construct {\n storageBucket: s3.IBucket;\n keyGroup?: cloudfront.IKeyGroup;\n responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;\n waf?: wafv2.CfnWebACL;\n originAccessIdentity?: cloudfront.OriginAccessIdentity;\n originAccessPolicyStatement?: iam.PolicyStatement;\n distribution?: cloudfront.IDistribution;\n dnsRecord?: route53.IRecordSet;\n\n constructor(parent: Construct, config: MedplumInfraConfig, region: string) {\n super(parent, 'Storage');\n\n if (region === config.region) {\n // S3 bucket\n this.storageBucket = new s3.Bucket(this, 'StorageBucket', {\n bucketName: config.storageBucketName,\n publicReadAccess: false,\n blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,\n encryption: s3.BucketEncryption.S3_MANAGED,\n enforceSSL: true,\n versioned: true,\n });\n\n if (config.clamscanEnabled) {\n // ClamAV serverless scan\n const sc = new ServerlessClamscan(this, 'ServerlessClamscan', {\n defsBucketAccessLogsConfig: {\n logsBucket: s3.Bucket.fromBucketName(this, 'LoggingBucket', config.clamscanLoggingBucket),\n logsPrefix: config.clamscanLoggingPrefix,\n },\n });\n sc.addSourceBucket(this.storageBucket);\n }\n } else {\n // Otherwise, reference the bucket by name and region\n this.storageBucket = s3.Bucket.fromBucketAttributes(this, 'StorageBucket', {\n bucketName: config.storageBucketName,\n region: config.region,\n });\n }\n\n if (region === 'us-east-1') {\n // Public key in PEM format\n let publicKey: cloudfront.IPublicKey;\n if (config.signingKeyId) {\n publicKey = cloudfront.PublicKey.fromPublicKeyId(this, 'StoragePublicKey', config.signingKeyId);\n } else {\n publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {\n encodedKey: config.storagePublicKey,\n });\n }\n\n // Authorized key group for presigned URLs\n this.keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {\n items: [publicKey],\n });\n\n // HTTP response headers policy\n this.responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {\n securityHeadersBehavior: {\n contentSecurityPolicy: {\n contentSecurityPolicy:\n \"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors *.medplum.com;\",\n override: true,\n },\n contentTypeOptions: { override: true },\n frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },\n referrerPolicy: { referrerPolicy: cloudfront.HeadersReferrerPolicy.NO_REFERRER, override: true },\n strictTransportSecurity: {\n accessControlMaxAge: Duration.seconds(63072000),\n includeSubdomains: true,\n override: true,\n },\n xssProtection: {\n protection: true,\n modeBlock: true,\n override: true,\n },\n },\n });\n\n // WAF\n this.waf = new wafv2.CfnWebACL(this, 'StorageWAF', {\n defaultAction: { allow: {} },\n scope: 'CLOUDFRONT',\n name: `${config.stackName}-StorageWAF`,\n rules: awsManagedRules,\n visibilityConfig: {\n cloudWatchMetricsEnabled: true,\n metricName: `${config.stackName}-StorageWAF-Metric`,\n sampledRequestsEnabled: false,\n },\n });\n\n // Origin access identity\n this.originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {});\n this.originAccessPolicyStatement = grantBucketAccessToOriginAccessIdentity(\n this.storageBucket,\n this.originAccessIdentity\n );\n\n // CloudFront distribution\n this.distribution = new cloudfront.Distribution(this, 'StorageDistribution', {\n defaultBehavior: {\n origin: new origins.S3Origin(this.storageBucket, {\n originAccessIdentity: this.originAccessIdentity,\n }),\n responseHeadersPolicy: this.responseHeadersPolicy,\n viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n trustedKeyGroups: [this.keyGroup],\n },\n certificate: acm.Certificate.fromCertificateArn(this, 'StorageCertificate', config.storageSslCertArn),\n domainNames: [config.storageDomainName],\n webAclId: this.waf.attrArn,\n logBucket: config.storageLoggingBucket\n ? s3.Bucket.fromBucketName(this, 'LoggingBucket', config.storageLoggingBucket)\n : undefined,\n logFilePrefix: config.storageLoggingPrefix,\n });\n\n // DNS\n if (!config.skipDns) {\n const zone = route53.HostedZone.fromLookup(this, 'Zone', {\n domainName: config.domainName.split('.').slice(-2).join('.'),\n });\n\n // Route53 alias record for the CloudFront distribution\n this.dnsRecord = new route53.ARecord(this, 'StorageAliasRecord', {\n recordName: config.storageDomainName,\n target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),\n zone,\n });\n }\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["src_exports", "__export", "BackEnd", "CloudTrailAlarms", "FrontEnd", "MedplumGlobalStack", "MedplumPrimaryStack", "MedplumStack", "Storage", "awsManagedRules", "main", "__toCommonJS", "import_aws_cdk_lib", "import_fs", "import_path", "import_aws_cdk_lib", "import_aws_cdk_lib", "import_aws_ecr", "import_aws_rds", "import_constructs", "awsManagedRules", "BackEnd", "scope", "config", "name", "ec2", "vpcFlowLogs", "logs", "iam", "instanceProps", "readers", "i", "rds", "elasticache", "subnet", "secretsmanager", "ecs", "container", "elbv2", "s3", "wafv2", "awsManagedRules", "zone", "route53", "targets", "ssm", "imageName", "nameTagMatches", "serverImageName", "serverImageTag", "ecrRepo", "import_aws_cdk_lib", "import_constructs", "CloudTrailAlarms", "scope", "config", "logs", "cloudtrail", "sns", "alarmDefinitions", "name", "filterPattern", "filterName", "metricName", "metricNamespace", "alarmName", "metricFilter", "cloudwatch", "cloudwatch_actions", "import_aws_cdk_lib", "import_constructs", "import_aws_cdk_lib", "grantBucketAccessToOriginAccessIdentity", "bucket", "identity", "policyStatement", "iam", "FrontEnd", "parent", "config", "region", "s3", "cloudfront", "wafv2", "awsManagedRules", "grantBucketAccessToOriginAccessIdentity", "origins", "acm", "zone", "route53", "targets", "import_aws_cdk_lib", "import_cdk_serverless_clamscan", "import_constructs", "Storage", "parent", "config", "region", "s3", "publicKey", "cloudfront", "wafv2", "awsManagedRules", "grantBucketAccessToOriginAccessIdentity", "origins", "acm", "zone", "route53", "targets", "MedplumStack", "scope", "config", "MedplumPrimaryStack", "MedplumGlobalStack", "BackEnd", "FrontEnd", "Storage", "CloudTrailAlarms", "main", "context", "app", "configFileName", "config", "stack", "MedplumStack"]
7
7
  }