@medplum/cdk 2.0.30 → 2.0.31

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 I=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.prototype.hasOwnProperty;var ae=(i,o)=>{for(var e in o)I(i,e,{get:o[e],enumerable:!0})},re=(i,o,e,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of ee(o))!te.call(i,r)&&r!==e&&I(i,r,{get:()=>o[r],enumerable:!(n=X(o,r))||n.enumerable});return i};var se=i=>re(I({},"__esModule",{value:!0}),i);var oe={};ae(oe,{main:()=>V});module.exports=se(oe);var g=require("aws-cdk-lib"),U=require("fs"),H=require("path");var t=require("aws-cdk-lib"),G=require("aws-cdk-lib/aws-ecr"),N=require("aws-cdk-lib/aws-rds"),O=require("constructs");var R=[{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 h=class extends O.Construct{constructor(o,e){super(o,"BackEnd");let n=e.name,r;if(e.vpcId)r=t.aws_ec2.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let c=new t.aws_logs.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+n,removalPolicy:t.RemovalPolicy.DESTROY});r=new t.aws_ec2.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:t.aws_ec2.FlowLogDestination.toCloudWatchLogs(c),trafficType:t.aws_ec2.FlowLogTrafficType.ALL}}})}let d=new t.aws_iam.Role(this,"BotLambdaRole",{assumedBy:new t.aws_iam.ServicePrincipal("lambda.amazonaws.com")}),l,u=e.rdsSecretsArn;if(!u){let c={instanceType:e.rdsInstanceType?new t.aws_ec2.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},k;if(e.rdsInstances>1){k=[];for(let v=0;v<e.rdsInstances-1;v++)k.push(N.ClusterInstance.provisioned("Instance"+(v+2),{...c}))}l=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:r,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},writer:N.ClusterInstance.provisioned("Instance1",{...c}),readers:k,backup:{retention:t.Duration.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:t.aws_rds.InstanceUpdateBehaviour.ROLLING}),u=l.secret.secretArn}let A=new t.aws_elasticache.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:r.privateSubnets.map(c=>c.subnetId)}),S=new t.aws_ec2.SecurityGroup(this,"RedisSecurityGroup",{vpc:r,description:"Redis Security Group",allowAllOutbound:!1}),p=new t.aws_secretsmanager.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),m=new t.aws_elasticache.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:p.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:A.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[S.securityGroupId]});m.node.addDependency(p);let f=new t.aws_secretsmanager.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:m.attrPrimaryEndPointAddress,port:m.attrPrimaryEndPointPort,password:p.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}});f.node.addDependency(p),f.node.addDependency(m);let $=new t.aws_ecs.Cluster(this,"Cluster",{vpc:r}),Y=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:[d.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:*"]})]}),Q=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:Y}}),b=new t.aws_ecs.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:Q}),z=new t.aws_logs.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+n,removalPolicy:t.RemovalPolicy.DESTROY}),_=new t.aws_ecs.AwsLogDriver({logGroup:z,streamPrefix:"Medplum"});if(b.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${n}/`:`aws:${e.region}:/medplum/${n}/`],logging:_}).addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let c of e.additionalContainers)b.addContainer("AdditionalContainer-"+c.name,{containerName:c.name,image:this.getContainerImage(e,c.image),command:c.command,environment:c.environment,logging:_});let E=new t.aws_ec2.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:r}),L=new t.aws_ecs.FargateService(this,"FargateService",{cluster:$,taskDefinition:b,assignPublicIp:!1,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[E],healthCheckGracePeriod:t.Duration.minutes(5)});l&&L.node.addDependency(l),L.node.addDependency(m);let K=new t.aws_elasticloadbalancingv2.ApplicationTargetGroup(this,"TargetGroup",{vpc:r,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:[L]}),P=new t.aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:r,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0});e.loadBalancerLoggingBucket&&P.logAccessLogs(t.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),P.addListener("HttpsListener",{port:443,certificates:[{certificateArn:e.apiSslCertArn}],sslPolicy:t.aws_elasticloadbalancingv2.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,defaultAction:t.aws_elasticloadbalancingv2.ListenerAction.forward([K])});let T=new t.aws_wafv2.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),q=new t.aws_wafv2.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:P.loadBalancerArn,webAclArn:T.attrArn});l&&l.connections.allowDefaultPortFrom(E),S.addIngressRule(E,t.aws_ec2.Port.tcp(6379));let D;if(!e.skipDns){let c=t.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});D=new t.aws_route53.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:t.aws_route53.RecordTarget.fromAlias(new t.aws_route53_targets.LoadBalancerTarget(P)),zone:c})}let j=new t.aws_ssm.StringParameter(this,"DatabaseSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:u}),Z=new t.aws_ssm.StringParameter(this,"RedisSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/RedisSecrets`,description:"Redis secrets ARN",stringValue:f.secretArn}),J=new t.aws_ssm.StringParameter(this,"BotLambdaRoleParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:d.roleArn});console.log("ARecord",D?.domainName),console.log("DatabaseSecretsParameter",j.parameterArn),console.log("RedisSecretsParameter",Z.parameterArn),console.log("RedisCluster",m.attrPrimaryEndPointAddress),console.log("BotLambdaRole",J.stringValue),console.log("WAF",T.attrArn),console.log("WAF Association",q.node.id)}getContainerImage(o,e){let r=new RegExp(`^${o.accountNumber}\\.dkr\\.ecr\\.${o.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),d=r?.[1],l=r?.[2];if(d&&l){let u=G.Repository.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${o.region}:${o.accountNumber}:repository/${d}`);return t.aws_ecs.ContainerImage.fromEcrRepository(u,l)}return t.aws_ecs.ContainerImage.fromRegistry(e)}};var a=require("aws-cdk-lib"),F=require("constructs");var M=require("aws-cdk-lib");function C(i,o){let e=new M.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(o.cloudFrontOriginAccessIdentityS3CanonicalUserId),i.addToResourcePolicy(e)}var w=class extends F.Construct{constructor(o,e,n){super(o,"FrontEnd");let r;if(n===e.region?r=new a.aws_s3.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:a.aws_s3.BlockPublicAccess.BLOCK_ALL,removalPolicy:a.RemovalPolicy.DESTROY,encryption:a.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):r=a.aws_s3.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),n==="us-east-1"){let d=new a.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:a.aws_cloudfront.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:a.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),l=new a.aws_wafv2.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),u=new a.aws_cloudfront.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:a.aws_cloudfront.CacheCookieBehavior.all(),headerBehavior:a.aws_cloudfront.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:a.aws_cloudfront.CacheQueryStringBehavior.all()}),A=new a.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});C(r,A);let S=new a.aws_cloudfront.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new a.aws_cloudfront_origins.S3Origin(r,{originAccessIdentity:A}),responseHeadersPolicy:d,viewerProtocolPolicy:a.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new a.aws_cloudfront_origins.HttpOrigin(e.apiDomainName),allowedMethods:a.aws_cloudfront.AllowedMethods.ALLOW_ALL,cachePolicy:u,viewerProtocolPolicy:a.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:a.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:l.attrArn,logBucket:e.appLoggingBucket?a.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),p;if(!e.skipDns){let m=a.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});p=new a.aws_route53.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:a.aws_route53.RecordTarget.fromAlias(new a.aws_route53_targets.CloudFrontTarget(S)),zone:m})}console.log("ARecord",p?.domainName)}}};var s=require("aws-cdk-lib"),W=require("cdk-serverless-clamscan"),x=require("constructs");var y=class extends x.Construct{constructor(o,e,n){super(o,"Storage");let r;if(n===e.region?(r=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 W.ServerlessClamscan(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(r)):r=s.aws_s3.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),n==="us-east-1"){let d=new s.aws_cloudfront.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey}),l=new s.aws_cloudfront.KeyGroup(this,"StorageKeyGroup",{items:[d]}),u=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}}}),A=new s.aws_wafv2.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),S=new s.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});C(r,S);let p=new s.aws_cloudfront.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new s.aws_cloudfront_origins.S3Origin(r,{originAccessIdentity:S}),responseHeadersPolicy:u,viewerProtocolPolicy:s.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[l]},certificate:s.aws_certificatemanager.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:A.attrArn,logBucket:e.storageLoggingBucket?s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),m;if(!e.skipDns){let f=s.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});m=new s.aws_route53.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:s.aws_route53.RecordTarget.fromAlias(new s.aws_route53_targets.CloudFrontTarget(p)),zone:f})}console.log("ARecord",m?.domainName)}}};var B=class{constructor(o,e){if(this.primaryStack=new g.Stack(o,e.stackName,{env:{region:e.region,account:e.accountNumber}}),g.Tags.of(this.primaryStack).add("medplum:environment",e.name),this.backEnd=new h(this.primaryStack,e),this.frontEnd=new w(this.primaryStack,e,e.region),this.storage=new y(this.primaryStack,e,e.region),e.region!=="us-east-1"){let n=new g.Stack(o,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});g.Tags.of(n).add("medplum:environment",e.name),this.frontEnd=new w(n,e,"us-east-1"),this.storage=new y(n,e,"us-east-1")}}};function V(i){let o=new g.App({context:i}),e=o.node.tryGetContext("config");if(!e){console.log('Missing "config" context variable'),console.log("Usage: cdk deploy -c config=my-config.json");return}let n=JSON.parse((0,U.readFileSync)((0,H.resolve)(e),"utf-8")),r=new B(o,n);console.log("Stack",r.primaryStack.stackId),console.log("BackEnd",r.backEnd.node.id),console.log("FrontEnd",r.frontEnd.node.id),console.log("Storage",r.storage.node.id),o.synth()}require.main===module&&V();0&&(module.exports={main});
1
+ "use strict";var I=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.prototype.hasOwnProperty;var ae=(i,o)=>{for(var e in o)I(i,e,{get:o[e],enumerable:!0})},re=(i,o,e,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of ee(o))!te.call(i,r)&&r!==e&&I(i,r,{get:()=>o[r],enumerable:!(n=X(o,r))||n.enumerable});return i};var se=i=>re(I({},"__esModule",{value:!0}),i);var oe={};ae(oe,{main:()=>V});module.exports=se(oe);var g=require("aws-cdk-lib"),U=require("fs"),H=require("path");var t=require("aws-cdk-lib"),G=require("aws-cdk-lib/aws-ecr"),N=require("aws-cdk-lib/aws-rds"),O=require("constructs");var R=[{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 h=class extends O.Construct{constructor(o,e){super(o,"BackEnd");let n=e.name,r;if(e.vpcId)r=t.aws_ec2.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let c=new t.aws_logs.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+n,removalPolicy:t.RemovalPolicy.DESTROY});r=new t.aws_ec2.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:t.aws_ec2.FlowLogDestination.toCloudWatchLogs(c),trafficType:t.aws_ec2.FlowLogTrafficType.ALL}}})}let m=new t.aws_iam.Role(this,"BotLambdaRole",{assumedBy:new t.aws_iam.ServicePrincipal("lambda.amazonaws.com")}),l,u=e.rdsSecretsArn;if(!u){let c={instanceType:e.rdsInstanceType?new t.aws_ec2.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},k;if(e.rdsInstances>1){k=[];for(let v=0;v<e.rdsInstances-1;v++)k.push(N.ClusterInstance.provisioned("Instance"+(v+2),{...c}))}l=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:r,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},writer:N.ClusterInstance.provisioned("Instance1",{...c}),readers:k,backup:{retention:t.Duration.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:t.aws_rds.InstanceUpdateBehaviour.ROLLING}),u=l.secret.secretArn}let A=new t.aws_elasticache.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:r.privateSubnets.map(c=>c.subnetId)}),S=new t.aws_ec2.SecurityGroup(this,"RedisSecurityGroup",{vpc:r,description:"Redis Security Group",allowAllOutbound:!1}),p=new t.aws_secretsmanager.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),d=new t.aws_elasticache.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:p.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:A.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[S.securityGroupId]});d.node.addDependency(p);let f=new t.aws_secretsmanager.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:d.attrPrimaryEndPointAddress,port:d.attrPrimaryEndPointPort,password:p.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}});f.node.addDependency(p),f.node.addDependency(d);let $=new t.aws_ecs.Cluster(this,"Cluster",{vpc:r}),K=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:[m.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:*"]})]}),Y=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:K}}),C=new t.aws_ecs.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:Y}),Q=new t.aws_logs.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+n,removalPolicy:t.RemovalPolicy.DESTROY}),_=new t.aws_ecs.AwsLogDriver({logGroup:Q,streamPrefix:"Medplum"});if(C.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${n}/`:`aws:${e.region}:/medplum/${n}/`],logging:_}).addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let c of e.additionalContainers)C.addContainer("AdditionalContainer-"+c.name,{containerName:c.name,image:this.getContainerImage(e,c.image),command:c.command,environment:c.environment,logging:_});let E=new t.aws_ec2.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:r}),L=new t.aws_ecs.FargateService(this,"FargateService",{cluster:$,taskDefinition:C,assignPublicIp:!1,vpcSubnets:{subnetType:t.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[E],healthCheckGracePeriod:t.Duration.minutes(5)});l&&L.node.addDependency(l),L.node.addDependency(d);let z=new t.aws_elasticloadbalancingv2.ApplicationTargetGroup(this,"TargetGroup",{vpc:r,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:[L]}),P=new t.aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:r,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0});e.loadBalancerLoggingBucket&&P.logAccessLogs(t.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),P.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 T=new t.aws_wafv2.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),q=new t.aws_wafv2.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:P.loadBalancerArn,webAclArn:T.attrArn});l&&l.connections.allowDefaultPortFrom(E),S.addIngressRule(E,t.aws_ec2.Port.tcp(6379));let D;if(!e.skipDns){let c=t.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});D=new t.aws_route53.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:t.aws_route53.RecordTarget.fromAlias(new t.aws_route53_targets.LoadBalancerTarget(P)),zone:c})}let j=new t.aws_ssm.StringParameter(this,"DatabaseSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:u}),Z=new t.aws_ssm.StringParameter(this,"RedisSecretsParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/RedisSecrets`,description:"Redis secrets ARN",stringValue:f.secretArn}),J=new t.aws_ssm.StringParameter(this,"BotLambdaRoleParameter",{tier:t.aws_ssm.ParameterTier.STANDARD,parameterName:`/medplum/${n}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:m.roleArn});console.log("ARecord",D?.domainName),console.log("DatabaseSecretsParameter",j.parameterArn),console.log("RedisSecretsParameter",Z.parameterArn),console.log("RedisCluster",d.attrPrimaryEndPointAddress),console.log("BotLambdaRole",J.stringValue),console.log("WAF",T.attrArn),console.log("WAF Association",q.node.id)}getContainerImage(o,e){let r=new RegExp(`^${o.accountNumber}\\.dkr\\.ecr\\.${o.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),m=r?.[1],l=r?.[2];if(m&&l){let u=G.Repository.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${o.region}:${o.accountNumber}:repository/${m}`);return t.aws_ecs.ContainerImage.fromEcrRepository(u,l)}return t.aws_ecs.ContainerImage.fromRegistry(e)}};var a=require("aws-cdk-lib"),F=require("constructs");var M=require("aws-cdk-lib");function b(i,o){let e=new M.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(o.cloudFrontOriginAccessIdentityS3CanonicalUserId),i.addToResourcePolicy(e)}var y=class extends F.Construct{constructor(o,e,n){super(o,"FrontEnd");let r;if(n===e.region?r=new a.aws_s3.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:a.aws_s3.BlockPublicAccess.BLOCK_ALL,removalPolicy:a.RemovalPolicy.DESTROY,encryption:a.aws_s3.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):r=a.aws_s3.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),n==="us-east-1"){let m=new a.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:a.aws_cloudfront.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:a.Duration.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),l=new a.aws_wafv2.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),u=new a.aws_cloudfront.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:a.aws_cloudfront.CacheCookieBehavior.all(),headerBehavior:a.aws_cloudfront.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:a.aws_cloudfront.CacheQueryStringBehavior.all()}),A=new a.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});b(r,A);let S=new a.aws_cloudfront.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new a.aws_cloudfront_origins.S3Origin(r,{originAccessIdentity:A}),responseHeadersPolicy:m,viewerProtocolPolicy:a.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new a.aws_cloudfront_origins.HttpOrigin(e.apiDomainName),allowedMethods:a.aws_cloudfront.AllowedMethods.ALLOW_ALL,cachePolicy:u,viewerProtocolPolicy:a.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:a.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:l.attrArn,logBucket:e.appLoggingBucket?a.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),p;if(!e.skipDns){let d=a.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});p=new a.aws_route53.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:a.aws_route53.RecordTarget.fromAlias(new a.aws_route53_targets.CloudFrontTarget(S)),zone:d})}console.log("ARecord",p?.domainName)}}};var s=require("aws-cdk-lib"),W=require("cdk-serverless-clamscan"),x=require("constructs");var w=class extends x.Construct{constructor(o,e,n){super(o,"Storage");let r;if(n===e.region?(r=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 W.ServerlessClamscan(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(r)):r=s.aws_s3.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),n==="us-east-1"){let m;e.signingKeyId?m=s.aws_cloudfront.PublicKey.fromPublicKeyId(this,"StoragePublicKey",e.signingKeyId):m=new s.aws_cloudfront.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey});let l=new s.aws_cloudfront.KeyGroup(this,"StorageKeyGroup",{items:[m]}),u=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}}}),A=new s.aws_wafv2.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),S=new s.aws_cloudfront.OriginAccessIdentity(this,"OriginAccessIdentity",{});b(r,S);let p=new s.aws_cloudfront.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new s.aws_cloudfront_origins.S3Origin(r,{originAccessIdentity:S}),responseHeadersPolicy:u,viewerProtocolPolicy:s.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[l]},certificate:s.aws_certificatemanager.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:A.attrArn,logBucket:e.storageLoggingBucket?s.aws_s3.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),d;if(!e.skipDns){let f=s.aws_route53.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});d=new s.aws_route53.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:s.aws_route53.RecordTarget.fromAlias(new s.aws_route53_targets.CloudFrontTarget(p)),zone:f})}console.log("ARecord",d?.domainName)}}};var B=class{constructor(o,e){if(this.primaryStack=new g.Stack(o,e.stackName,{env:{region:e.region,account:e.accountNumber}}),g.Tags.of(this.primaryStack).add("medplum:environment",e.name),this.backEnd=new h(this.primaryStack,e),this.frontEnd=new y(this.primaryStack,e,e.region),this.storage=new w(this.primaryStack,e,e.region),e.region!=="us-east-1"){let n=new g.Stack(o,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});g.Tags.of(n).add("medplum:environment",e.name),this.frontEnd=new y(n,e,"us-east-1"),this.storage=new w(n,e,"us-east-1")}}};function V(i){let o=new g.App({context:i}),e=o.node.tryGetContext("config");if(!e){console.log('Missing "config" context variable'),console.log("Usage: cdk deploy -c config=my-config.json");return}let n=JSON.parse((0,U.readFileSync)((0,H.resolve)(e),"utf-8")),r=new B(o,n);console.log("Stack",r.primaryStack.stackId),console.log("BackEnd",r.backEnd.node.id),console.log("FrontEnd",r.frontEnd.node.id),console.log("Storage",r.storage.node.id),o.synth()}require.main===module&&V();0&&(module.exports={main});
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts", "../../src/backend.ts", "../../src/waf.ts", "../../src/frontend.ts", "../../src/oai.ts", "../../src/storage.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';\n\nclass MedplumStack {\n primaryStack: Stack;\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\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\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 }\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\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 const publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {\n encodedKey: config.storagePublicKey,\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"],
5
- "mappings": "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",
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';\n\nclass MedplumStack {\n primaryStack: Stack;\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\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\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 }\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\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"],
5
+ "mappings": "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",
6
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", "MedplumStack", "scope", "config", "BackEnd", "FrontEnd", "Storage", "usEast1Stack", "main", "context", "app", "configFileName", "stack"]
7
7
  }
@@ -1,2 +1,2 @@
1
- var V=(o=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(o,{get:(a,e)=>(typeof require<"u"?require:a)[e]}):o)(function(o){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+o+'" is not supported')});import{App as Le,Stack as Z,Tags as J}from"aws-cdk-lib";import{readFileSync as ke}from"fs";import{resolve as ve}from"path";import{Duration as v,aws_ec2 as d,aws_ecs as f,aws_elasticache as $,aws_elasticloadbalancingv2 as h,aws_iam as s,aws_logs as Y,aws_rds as C,RemovalPolicy as Q,aws_route53 as O,aws_s3 as ce,aws_secretsmanager as z,aws_ssm as w,aws_route53_targets as le,aws_wafv2 as K}from"aws-cdk-lib";import{Repository as me}from"aws-cdk-lib/aws-ecr";import{ClusterInstance as q}from"aws-cdk-lib/aws-rds";import{Construct as de}from"constructs";var R=[{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 I=class extends de{constructor(a,e){super(a,"BackEnd");let r=e.name,t;if(e.vpcId)t=d.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let n=new Y.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+r,removalPolicy:Q.DESTROY});t=new d.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:d.FlowLogDestination.toCloudWatchLogs(n),trafficType:d.FlowLogTrafficType.ALL}}})}let m=new s.Role(this,"BotLambdaRole",{assumedBy:new s.ServicePrincipal("lambda.amazonaws.com")}),i,u=e.rdsSecretsArn;if(!u){let n={instanceType:e.rdsInstanceType?new d.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},D;if(e.rdsInstances>1){D=[];for(let G=0;G<e.rdsInstances-1;G++)D.push(q.provisioned("Instance"+(G+2),{...n}))}i=new C.DatabaseCluster(this,"DatabaseCluster",{engine:C.DatabaseClusterEngine.auroraPostgres({version:C.AuroraPostgresEngineVersion.VER_12_9}),credentials:C.Credentials.fromGeneratedSecret("clusteradmin"),defaultDatabaseName:"medplum",storageEncrypted:!0,vpc:t,vpcSubnets:{subnetType:d.SubnetType.PRIVATE_WITH_EGRESS},writer:q.provisioned("Instance1",{...n}),readers:D,backup:{retention:v.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:C.InstanceUpdateBehaviour.ROLLING}),u=i.secret.secretArn}let A=new $.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:t.privateSubnets.map(n=>n.subnetId)}),S=new d.SecurityGroup(this,"RedisSecurityGroup",{vpc:t,description:"Redis Security Group",allowAllOutbound:!1}),p=new z.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),c=new $.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:p.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:A.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[S.securityGroupId]});c.node.addDependency(p);let P=new z.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:c.attrPrimaryEndPointAddress,port:c.attrPrimaryEndPointPort,password:p.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}});P.node.addDependency(p),P.node.addDependency(c);let X=new f.Cluster(this,"Cluster",{vpc:t}),ee=new s.PolicyDocument({statements:[new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["logs:CreateLogStream","logs:PutLogEvents"],resources:["arn:aws:logs:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["secretsmanager:GetResourcePolicy","secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecrets","secretsmanager:ListSecretVersionIds"],resources:["arn:aws:secretsmanager:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["ssm:GetParametersByPath","ssm:GetParameters","ssm:GetParameter","ssm:DescribeParameters"],resources:["arn:aws:ssm:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["ses:SendEmail","ses:SendRawEmail"],resources:["arn:aws:ses:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["s3:ListBucket","s3:GetObject","s3:PutObject","s3:DeleteObject"],resources:["arn:aws:s3:::*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["iam:ListRoles","iam:GetRole","iam:PassRole"],resources:[m.roleArn]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["lambda:CreateFunction","lambda:GetFunction","lambda:GetFunctionConfiguration","lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration","lambda:ListLayerVersions","lambda:GetLayerVersion","lambda:InvokeFunction"],resources:["arn:aws:lambda:*"]})]}),te=new s.Role(this,"TaskExecutionRole",{assumedBy:new s.ServicePrincipal("ecs-tasks.amazonaws.com"),description:"Medplum Server Task Execution Role",inlinePolicies:{TaskExecutionPolicies:ee}}),B=new f.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:te}),ae=new Y.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+r,removalPolicy:Q.DESTROY}),x=new f.AwsLogDriver({logGroup:ae,streamPrefix:"Medplum"});if(B.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${r}/`:`aws:${e.region}:/medplum/${r}/`],logging:x}).addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let n of e.additionalContainers)B.addContainer("AdditionalContainer-"+n.name,{containerName:n.name,image:this.getContainerImage(e,n.image),command:n.command,environment:n.environment,logging:x});let _=new d.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:t}),T=new f.FargateService(this,"FargateService",{cluster:X,taskDefinition:B,assignPublicIp:!1,vpcSubnets:{subnetType:d.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[_],healthCheckGracePeriod:v.minutes(5)});i&&T.node.addDependency(i),T.node.addDependency(c);let re=new h.ApplicationTargetGroup(this,"TargetGroup",{vpc:t,port:e.apiPort,protocol:h.ApplicationProtocol.HTTP,healthCheck:{path:"/healthcheck",interval:v.seconds(30),timeout:v.seconds(3),healthyThresholdCount:2,unhealthyThresholdCount:5},targets:[T]}),k=new h.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:t,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0});e.loadBalancerLoggingBucket&&k.logAccessLogs(ce.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),k.addListener("HttpsListener",{port:443,certificates:[{certificateArn:e.apiSslCertArn}],sslPolicy:h.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,defaultAction:h.ListenerAction.forward([re])});let U=new K.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),se=new K.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:k.loadBalancerArn,webAclArn:U.attrArn});i&&i.connections.allowDefaultPortFrom(_),S.addIngressRule(_,d.Port.tcp(6379));let H;if(!e.skipDns){let n=O.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});H=new O.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:O.RecordTarget.fromAlias(new le.LoadBalancerTarget(k)),zone:n})}let oe=new w.StringParameter(this,"DatabaseSecretsParameter",{tier:w.ParameterTier.STANDARD,parameterName:`/medplum/${r}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:u}),ne=new w.StringParameter(this,"RedisSecretsParameter",{tier:w.ParameterTier.STANDARD,parameterName:`/medplum/${r}/RedisSecrets`,description:"Redis secrets ARN",stringValue:P.secretArn}),ie=new w.StringParameter(this,"BotLambdaRoleParameter",{tier:w.ParameterTier.STANDARD,parameterName:`/medplum/${r}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:m.roleArn});console.log("ARecord",H?.domainName),console.log("DatabaseSecretsParameter",oe.parameterArn),console.log("RedisSecretsParameter",ne.parameterArn),console.log("RedisCluster",c.attrPrimaryEndPointAddress),console.log("BotLambdaRole",ie.stringValue),console.log("WAF",U.attrArn),console.log("WAF Association",se.node.id)}getContainerImage(a,e){let t=new RegExp(`^${a.accountNumber}\\.dkr\\.ecr\\.${a.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),m=t?.[1],i=t?.[2];if(m&&i){let u=me.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${a.region}:${a.accountNumber}:repository/${m}`);return f.ContainerImage.fromEcrRepository(u,i)}return f.ContainerImage.fromRegistry(e)}};import{aws_certificatemanager as pe,aws_cloudfront as l,Duration as ge,aws_cloudfront_origins as j,RemovalPolicy as Se,aws_route53 as M,aws_s3 as b,aws_route53_targets as Ae,aws_wafv2 as Re}from"aws-cdk-lib";import{Construct as fe}from"constructs";import{aws_iam as ue}from"aws-cdk-lib";function N(o,a){let e=new ue.PolicyStatement;e.addActions("s3:GetObject*"),e.addActions("s3:GetBucket*"),e.addActions("s3:List*"),e.addResources(o.bucketArn),e.addResources(`${o.bucketArn}/*`),e.addCanonicalUserPrincipal(a.cloudFrontOriginAccessIdentityS3CanonicalUserId),o.addToResourcePolicy(e)}var E=class extends fe{constructor(a,e,r){super(a,"FrontEnd");let t;if(r===e.region?t=new b.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:b.BlockPublicAccess.BLOCK_ALL,removalPolicy:Se.DESTROY,encryption:b.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):t=b.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),r==="us-east-1"){let m=new l.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:l.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:ge.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),i=new Re.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),u=new l.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:l.CacheCookieBehavior.all(),headerBehavior:l.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:l.CacheQueryStringBehavior.all()}),A=new l.OriginAccessIdentity(this,"OriginAccessIdentity",{});N(t,A);let S=new l.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new j.S3Origin(t,{originAccessIdentity:A}),responseHeadersPolicy:m,viewerProtocolPolicy:l.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new j.HttpOrigin(e.apiDomainName),allowedMethods:l.AllowedMethods.ALLOW_ALL,cachePolicy:u,viewerProtocolPolicy:l.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:pe.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:i.attrArn,logBucket:e.appLoggingBucket?b.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),p;if(!e.skipDns){let c=M.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});p=new M.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:M.RecordTarget.fromAlias(new Ae.CloudFrontTarget(S)),zone:c})}console.log("ARecord",p?.domainName)}}};import{aws_certificatemanager as we,aws_cloudfront as g,Duration as ye,aws_cloudfront_origins as Pe,aws_route53 as F,aws_s3 as y,aws_route53_targets as he,aws_wafv2 as Ce}from"aws-cdk-lib";import{ServerlessClamscan as be}from"cdk-serverless-clamscan";import{Construct as Ee}from"constructs";var L=class extends Ee{constructor(a,e,r){super(a,"Storage");let t;if(r===e.region?(t=new y.Bucket(this,"StorageBucket",{bucketName:e.storageBucketName,publicReadAccess:!1,blockPublicAccess:y.BlockPublicAccess.BLOCK_ALL,encryption:y.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}),e.clamscanEnabled&&new be(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:y.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(t)):t=y.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),r==="us-east-1"){let m=new g.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey}),i=new g.KeyGroup(this,"StorageKeyGroup",{items:[m]}),u=new g.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:g.HeadersFrameOption.DENY,override:!0},referrerPolicy:{referrerPolicy:g.HeadersReferrerPolicy.NO_REFERRER,override:!0},strictTransportSecurity:{accessControlMaxAge:ye.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),A=new Ce.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),S=new g.OriginAccessIdentity(this,"OriginAccessIdentity",{});N(t,S);let p=new g.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new Pe.S3Origin(t,{originAccessIdentity:S}),responseHeadersPolicy:u,viewerProtocolPolicy:g.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[i]},certificate:we.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:A.attrArn,logBucket:e.storageLoggingBucket?y.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),c;if(!e.skipDns){let P=F.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});c=new F.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:F.RecordTarget.fromAlias(new he.CloudFrontTarget(p)),zone:P})}console.log("ARecord",c?.domainName)}}};var W=class{constructor(a,e){if(this.primaryStack=new Z(a,e.stackName,{env:{region:e.region,account:e.accountNumber}}),J.of(this.primaryStack).add("medplum:environment",e.name),this.backEnd=new I(this.primaryStack,e),this.frontEnd=new E(this.primaryStack,e,e.region),this.storage=new L(this.primaryStack,e,e.region),e.region!=="us-east-1"){let r=new Z(a,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});J.of(r).add("medplum:environment",e.name),this.frontEnd=new E(r,e,"us-east-1"),this.storage=new L(r,e,"us-east-1")}}};function Ie(o){let a=new Le({context:o}),e=a.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(ke(ve(e),"utf-8")),t=new W(a,r);console.log("Stack",t.primaryStack.stackId),console.log("BackEnd",t.backEnd.node.id),console.log("FrontEnd",t.frontEnd.node.id),console.log("Storage",t.storage.node.id),a.synth()}V.main===module&&Ie();export{Ie as main};
1
+ var V=(o=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(o,{get:(a,e)=>(typeof require<"u"?require:a)[e]}):o)(function(o){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+o+'" is not supported')});import{App as Le,Stack as Z,Tags as J}from"aws-cdk-lib";import{readFileSync as ke}from"fs";import{resolve as ve}from"path";import{Duration as v,aws_ec2 as d,aws_ecs as f,aws_elasticache as $,aws_elasticloadbalancingv2 as h,aws_iam as s,aws_logs as K,aws_rds as b,RemovalPolicy as Y,aws_route53 as O,aws_s3 as ce,aws_secretsmanager as Q,aws_ssm as y,aws_route53_targets as le,aws_wafv2 as z}from"aws-cdk-lib";import{Repository as me}from"aws-cdk-lib/aws-ecr";import{ClusterInstance as q}from"aws-cdk-lib/aws-rds";import{Construct as de}from"constructs";var R=[{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 I=class extends de{constructor(a,e){super(a,"BackEnd");let r=e.name,t;if(e.vpcId)t=d.Vpc.fromLookup(this,"VPC",{vpcId:e.vpcId});else{let n=new K.LogGroup(this,"VpcFlowLogs",{logGroupName:"/medplum/flowlogs/"+r,removalPolicy:Y.DESTROY});t=new d.Vpc(this,"VPC",{maxAzs:e.maxAzs,flowLogs:{cloudwatch:{destination:d.FlowLogDestination.toCloudWatchLogs(n),trafficType:d.FlowLogTrafficType.ALL}}})}let c=new s.Role(this,"BotLambdaRole",{assumedBy:new s.ServicePrincipal("lambda.amazonaws.com")}),i,u=e.rdsSecretsArn;if(!u){let n={instanceType:e.rdsInstanceType?new d.InstanceType(e.rdsInstanceType):void 0,enablePerformanceInsights:!0,isFromLegacyInstanceProps:!0},D;if(e.rdsInstances>1){D=[];for(let G=0;G<e.rdsInstances-1;G++)D.push(q.provisioned("Instance"+(G+2),{...n}))}i=new b.DatabaseCluster(this,"DatabaseCluster",{engine:b.DatabaseClusterEngine.auroraPostgres({version:b.AuroraPostgresEngineVersion.VER_12_9}),credentials:b.Credentials.fromGeneratedSecret("clusteradmin"),defaultDatabaseName:"medplum",storageEncrypted:!0,vpc:t,vpcSubnets:{subnetType:d.SubnetType.PRIVATE_WITH_EGRESS},writer:q.provisioned("Instance1",{...n}),readers:D,backup:{retention:v.days(7)},cloudwatchLogsExports:["postgresql"],instanceUpdateBehaviour:b.InstanceUpdateBehaviour.ROLLING}),u=i.secret.secretArn}let A=new $.CfnSubnetGroup(this,"RedisSubnetGroup",{description:"Redis Subnet Group",subnetIds:t.privateSubnets.map(n=>n.subnetId)}),S=new d.SecurityGroup(this,"RedisSecurityGroup",{vpc:t,description:"Redis Security Group",allowAllOutbound:!1}),p=new Q.Secret(this,"RedisPassword",{generateSecretString:{secretStringTemplate:"{}",generateStringKey:"password",excludeCharacters:"@%*()_+=`~{}|[]\\:\";'?,./"}}),l=new $.CfnReplicationGroup(this,"RedisCluster",{engine:"Redis",engineVersion:"6.x",cacheNodeType:e.cacheNodeType??"cache.t2.medium",replicationGroupDescription:"RedisReplicationGroup",authToken:p.secretValueFromJson("password").toString(),transitEncryptionEnabled:!0,atRestEncryptionEnabled:!0,multiAzEnabled:!0,cacheSubnetGroupName:A.ref,numNodeGroups:1,replicasPerNodeGroup:1,securityGroupIds:[S.securityGroupId]});l.node.addDependency(p);let P=new Q.Secret(this,"RedisSecrets",{generateSecretString:{secretStringTemplate:JSON.stringify({host:l.attrPrimaryEndPointAddress,port:l.attrPrimaryEndPointPort,password:p.secretValueFromJson("password").toString(),tls:{}}),generateStringKey:"unused"}});P.node.addDependency(p),P.node.addDependency(l);let X=new f.Cluster(this,"Cluster",{vpc:t}),ee=new s.PolicyDocument({statements:[new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["logs:CreateLogStream","logs:PutLogEvents"],resources:["arn:aws:logs:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["secretsmanager:GetResourcePolicy","secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecrets","secretsmanager:ListSecretVersionIds"],resources:["arn:aws:secretsmanager:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["ssm:GetParametersByPath","ssm:GetParameters","ssm:GetParameter","ssm:DescribeParameters"],resources:["arn:aws:ssm:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["ses:SendEmail","ses:SendRawEmail"],resources:["arn:aws:ses:*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["s3:ListBucket","s3:GetObject","s3:PutObject","s3:DeleteObject"],resources:["arn:aws:s3:::*"]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["iam:ListRoles","iam:GetRole","iam:PassRole"],resources:[c.roleArn]}),new s.PolicyStatement({effect:s.Effect.ALLOW,actions:["lambda:CreateFunction","lambda:GetFunction","lambda:GetFunctionConfiguration","lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration","lambda:ListLayerVersions","lambda:GetLayerVersion","lambda:InvokeFunction"],resources:["arn:aws:lambda:*"]})]}),te=new s.Role(this,"TaskExecutionRole",{assumedBy:new s.ServicePrincipal("ecs-tasks.amazonaws.com"),description:"Medplum Server Task Execution Role",inlinePolicies:{TaskExecutionPolicies:ee}}),B=new f.FargateTaskDefinition(this,"TaskDefinition",{memoryLimitMiB:e.serverMemory,cpu:e.serverCpu,taskRole:te}),ae=new K.LogGroup(this,"LogGroup",{logGroupName:"/ecs/medplum/"+r,removalPolicy:Y.DESTROY}),x=new f.AwsLogDriver({logGroup:ae,streamPrefix:"Medplum"});if(B.addContainer("MedplumTaskDefinition",{image:this.getContainerImage(e,e.serverImage),command:[e.region==="us-east-1"?`aws:/medplum/${r}/`:`aws:${e.region}:/medplum/${r}/`],logging:x}).addPortMappings({containerPort:e.apiPort,hostPort:e.apiPort}),e.additionalContainers)for(let n of e.additionalContainers)B.addContainer("AdditionalContainer-"+n.name,{containerName:n.name,image:this.getContainerImage(e,n.image),command:n.command,environment:n.environment,logging:x});let _=new d.SecurityGroup(this,"ServiceSecurityGroup",{allowAllOutbound:!0,securityGroupName:"MedplumSecurityGroup",vpc:t}),T=new f.FargateService(this,"FargateService",{cluster:X,taskDefinition:B,assignPublicIp:!1,vpcSubnets:{subnetType:d.SubnetType.PRIVATE_WITH_EGRESS},desiredCount:e.desiredServerCount,securityGroups:[_],healthCheckGracePeriod:v.minutes(5)});i&&T.node.addDependency(i),T.node.addDependency(l);let re=new h.ApplicationTargetGroup(this,"TargetGroup",{vpc:t,port:e.apiPort,protocol:h.ApplicationProtocol.HTTP,healthCheck:{path:"/healthcheck",interval:v.seconds(30),timeout:v.seconds(3),healthyThresholdCount:2,unhealthyThresholdCount:5},targets:[T]}),k=new h.ApplicationLoadBalancer(this,"LoadBalancer",{vpc:t,internetFacing:e.apiInternetFacing!==!1,http2Enabled:!0});e.loadBalancerLoggingBucket&&k.logAccessLogs(ce.Bucket.fromBucketName(this,"LoggingBucket",e.loadBalancerLoggingBucket),e.loadBalancerLoggingPrefix),k.addListener("HttpsListener",{port:443,certificates:[{certificateArn:e.apiSslCertArn}],sslPolicy:h.SslPolicy.FORWARD_SECRECY_TLS12_RES_GCM,defaultAction:h.ListenerAction.forward([re])});let U=new z.CfnWebACL(this,"BackEndWAF",{defaultAction:{allow:{}},scope:"REGIONAL",name:`${e.stackName}-BackEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-BackEndWAF-Metric`,sampledRequestsEnabled:!1}}),se=new z.CfnWebACLAssociation(this,"LoadBalancerAssociation",{resourceArn:k.loadBalancerArn,webAclArn:U.attrArn});i&&i.connections.allowDefaultPortFrom(_),S.addIngressRule(_,d.Port.tcp(6379));let H;if(!e.skipDns){let n=O.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});H=new O.ARecord(this,"LoadBalancerAliasRecord",{recordName:e.apiDomainName,target:O.RecordTarget.fromAlias(new le.LoadBalancerTarget(k)),zone:n})}let oe=new y.StringParameter(this,"DatabaseSecretsParameter",{tier:y.ParameterTier.STANDARD,parameterName:`/medplum/${r}/DatabaseSecrets`,description:"Database secrets ARN",stringValue:u}),ne=new y.StringParameter(this,"RedisSecretsParameter",{tier:y.ParameterTier.STANDARD,parameterName:`/medplum/${r}/RedisSecrets`,description:"Redis secrets ARN",stringValue:P.secretArn}),ie=new y.StringParameter(this,"BotLambdaRoleParameter",{tier:y.ParameterTier.STANDARD,parameterName:`/medplum/${r}/botLambdaRoleArn`,description:"Bot lambda execution role ARN",stringValue:c.roleArn});console.log("ARecord",H?.domainName),console.log("DatabaseSecretsParameter",oe.parameterArn),console.log("RedisSecretsParameter",ne.parameterArn),console.log("RedisCluster",l.attrPrimaryEndPointAddress),console.log("BotLambdaRole",ie.stringValue),console.log("WAF",U.attrArn),console.log("WAF Association",se.node.id)}getContainerImage(a,e){let t=new RegExp(`^${a.accountNumber}\\.dkr\\.ecr\\.${a.region}\\.amazonaws\\.com/(.*)[:@](.*)$`).exec(e),c=t?.[1],i=t?.[2];if(c&&i){let u=me.fromRepositoryArn(this,"ServerImageRepo",`arn:aws:ecr:${a.region}:${a.accountNumber}:repository/${c}`);return f.ContainerImage.fromEcrRepository(u,i)}return f.ContainerImage.fromRegistry(e)}};import{aws_certificatemanager as pe,aws_cloudfront as m,Duration as ge,aws_cloudfront_origins as j,RemovalPolicy as Se,aws_route53 as M,aws_s3 as C,aws_route53_targets as Ae,aws_wafv2 as Re}from"aws-cdk-lib";import{Construct as fe}from"constructs";import{aws_iam as ue}from"aws-cdk-lib";function N(o,a){let e=new ue.PolicyStatement;e.addActions("s3:GetObject*"),e.addActions("s3:GetBucket*"),e.addActions("s3:List*"),e.addResources(o.bucketArn),e.addResources(`${o.bucketArn}/*`),e.addCanonicalUserPrincipal(a.cloudFrontOriginAccessIdentityS3CanonicalUserId),o.addToResourcePolicy(e)}var E=class extends fe{constructor(a,e,r){super(a,"FrontEnd");let t;if(r===e.region?t=new C.Bucket(this,"AppBucket",{bucketName:e.appDomainName,publicReadAccess:!1,blockPublicAccess:C.BlockPublicAccess.BLOCK_ALL,removalPolicy:Se.DESTROY,encryption:C.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}):t=C.Bucket.fromBucketAttributes(this,"AppBucket",{bucketName:e.appDomainName,region:e.region}),r==="us-east-1"){let c=new m.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:m.HeadersFrameOption.DENY,override:!0},strictTransportSecurity:{accessControlMaxAge:ge.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),i=new Re.CfnWebACL(this,"FrontEndWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-FrontEndWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-FrontEndWAF-Metric`,sampledRequestsEnabled:!1}}),u=new m.CachePolicy(this,"ApiOriginCachePolicy",{cachePolicyName:`${e.stackName}-ApiOriginCachePolicy`,cookieBehavior:m.CacheCookieBehavior.all(),headerBehavior:m.CacheHeaderBehavior.allowList("Authorization","Content-Encoding","Content-Type","If-None-Match","Origin","Referer","User-Agent","X-Medplum"),queryStringBehavior:m.CacheQueryStringBehavior.all()}),A=new m.OriginAccessIdentity(this,"OriginAccessIdentity",{});N(t,A);let S=new m.Distribution(this,"AppDistribution",{defaultRootObject:"index.html",defaultBehavior:{origin:new j.S3Origin(t,{originAccessIdentity:A}),responseHeadersPolicy:c,viewerProtocolPolicy:m.ViewerProtocolPolicy.REDIRECT_TO_HTTPS},additionalBehaviors:e.appApiProxy?{"/api/*":{origin:new j.HttpOrigin(e.apiDomainName),allowedMethods:m.AllowedMethods.ALLOW_ALL,cachePolicy:u,viewerProtocolPolicy:m.ViewerProtocolPolicy.REDIRECT_TO_HTTPS}}:void 0,certificate:pe.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:i.attrArn,logBucket:e.appLoggingBucket?C.Bucket.fromBucketName(this,"LoggingBucket",e.appLoggingBucket):void 0,logFilePrefix:e.appLoggingPrefix}),p;if(!e.skipDns){let l=M.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});p=new M.ARecord(this,"AppAliasRecord",{recordName:e.appDomainName,target:M.RecordTarget.fromAlias(new Ae.CloudFrontTarget(S)),zone:l})}console.log("ARecord",p?.domainName)}}};import{aws_certificatemanager as ye,aws_cloudfront as g,Duration as we,aws_cloudfront_origins as Pe,aws_route53 as F,aws_s3 as w,aws_route53_targets as he,aws_wafv2 as be}from"aws-cdk-lib";import{ServerlessClamscan as Ce}from"cdk-serverless-clamscan";import{Construct as Ee}from"constructs";var L=class extends Ee{constructor(a,e,r){super(a,"Storage");let t;if(r===e.region?(t=new w.Bucket(this,"StorageBucket",{bucketName:e.storageBucketName,publicReadAccess:!1,blockPublicAccess:w.BlockPublicAccess.BLOCK_ALL,encryption:w.BucketEncryption.S3_MANAGED,enforceSSL:!0,versioned:!0}),e.clamscanEnabled&&new Ce(this,"ServerlessClamscan",{defsBucketAccessLogsConfig:{logsBucket:w.Bucket.fromBucketName(this,"LoggingBucket",e.clamscanLoggingBucket),logsPrefix:e.clamscanLoggingPrefix}}).addSourceBucket(t)):t=w.Bucket.fromBucketAttributes(this,"StorageBucket",{bucketName:e.storageBucketName,region:e.region}),r==="us-east-1"){let c;e.signingKeyId?c=g.PublicKey.fromPublicKeyId(this,"StoragePublicKey",e.signingKeyId):c=new g.PublicKey(this,"StoragePublicKey",{encodedKey:e.storagePublicKey});let i=new g.KeyGroup(this,"StorageKeyGroup",{items:[c]}),u=new g.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:g.HeadersFrameOption.DENY,override:!0},referrerPolicy:{referrerPolicy:g.HeadersReferrerPolicy.NO_REFERRER,override:!0},strictTransportSecurity:{accessControlMaxAge:we.seconds(63072e3),includeSubdomains:!0,override:!0},xssProtection:{protection:!0,modeBlock:!0,override:!0}}}),A=new be.CfnWebACL(this,"StorageWAF",{defaultAction:{allow:{}},scope:"CLOUDFRONT",name:`${e.stackName}-StorageWAF`,rules:R,visibilityConfig:{cloudWatchMetricsEnabled:!0,metricName:`${e.stackName}-StorageWAF-Metric`,sampledRequestsEnabled:!1}}),S=new g.OriginAccessIdentity(this,"OriginAccessIdentity",{});N(t,S);let p=new g.Distribution(this,"StorageDistribution",{defaultBehavior:{origin:new Pe.S3Origin(t,{originAccessIdentity:S}),responseHeadersPolicy:u,viewerProtocolPolicy:g.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,trustedKeyGroups:[i]},certificate:ye.Certificate.fromCertificateArn(this,"StorageCertificate",e.storageSslCertArn),domainNames:[e.storageDomainName],webAclId:A.attrArn,logBucket:e.storageLoggingBucket?w.Bucket.fromBucketName(this,"LoggingBucket",e.storageLoggingBucket):void 0,logFilePrefix:e.storageLoggingPrefix}),l;if(!e.skipDns){let P=F.HostedZone.fromLookup(this,"Zone",{domainName:e.domainName.split(".").slice(-2).join(".")});l=new F.ARecord(this,"StorageAliasRecord",{recordName:e.storageDomainName,target:F.RecordTarget.fromAlias(new he.CloudFrontTarget(p)),zone:P})}console.log("ARecord",l?.domainName)}}};var W=class{constructor(a,e){if(this.primaryStack=new Z(a,e.stackName,{env:{region:e.region,account:e.accountNumber}}),J.of(this.primaryStack).add("medplum:environment",e.name),this.backEnd=new I(this.primaryStack,e),this.frontEnd=new E(this.primaryStack,e,e.region),this.storage=new L(this.primaryStack,e,e.region),e.region!=="us-east-1"){let r=new Z(a,e.stackName+"-us-east-1",{env:{region:"us-east-1",account:e.accountNumber}});J.of(r).add("medplum:environment",e.name),this.frontEnd=new E(r,e,"us-east-1"),this.storage=new L(r,e,"us-east-1")}}};function Ie(o){let a=new Le({context:o}),e=a.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(ke(ve(e),"utf-8")),t=new W(a,r);console.log("Stack",t.primaryStack.stackId),console.log("BackEnd",t.backEnd.node.id),console.log("FrontEnd",t.frontEnd.node.id),console.log("Storage",t.storage.node.id),a.synth()}V.main===module&&Ie();export{Ie as main};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts", "../../src/backend.ts", "../../src/waf.ts", "../../src/frontend.ts", "../../src/oai.ts", "../../src/storage.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';\n\nclass MedplumStack {\n primaryStack: Stack;\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\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\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 }\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\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 const publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {\n encodedKey: config.storagePublicKey,\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"],
5
- "mappings": "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",
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';\n\nclass MedplumStack {\n primaryStack: Stack;\n backEnd: BackEnd;\n frontEnd: FrontEnd;\n storage: Storage;\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\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 }\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\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"],
5
+ "mappings": "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",
6
6
  "names": ["App", "Stack", "Tags", "readFileSync", "resolve", "Duration", "ec2", "ecs", "elasticache", "elbv2", "iam", "logs", "rds", "RemovalPolicy", "route53", "s3", "secretsmanager", "ssm", "targets", "wafv2", "Repository", "ClusterInstance", "Construct", "awsManagedRules", "BackEnd", "Construct", "scope", "config", "name", "vpc", "ec2", "vpcFlowLogs", "logs", "RemovalPolicy", "botLambdaRole", "iam", "rdsCluster", "rdsSecretsArn", "instanceProps", "readers", "i", "ClusterInstance", "rds", "Duration", "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", "Repository", "acm", "cloudfront", "Duration", "origins", "RemovalPolicy", "route53", "s3", "targets", "wafv2", "Construct", "iam", "grantBucketAccessToOriginAccessIdentity", "bucket", "identity", "policyStatement", "FrontEnd", "Construct", "parent", "config", "region", "appBucket", "s3", "RemovalPolicy", "responseHeadersPolicy", "cloudfront", "Duration", "waf", "wafv2", "awsManagedRules", "apiOriginCachePolicy", "originAccessIdentity", "grantBucketAccessToOriginAccessIdentity", "distribution", "origins", "acm", "record", "zone", "route53", "targets", "acm", "cloudfront", "Duration", "origins", "route53", "s3", "targets", "wafv2", "ServerlessClamscan", "Construct", "Storage", "Construct", "parent", "config", "region", "storageBucket", "s3", "ServerlessClamscan", "publicKey", "cloudfront", "keyGroup", "responseHeadersPolicy", "Duration", "waf", "wafv2", "awsManagedRules", "originAccessIdentity", "grantBucketAccessToOriginAccessIdentity", "distribution", "origins", "acm", "record", "zone", "route53", "targets", "MedplumStack", "scope", "config", "Stack", "Tags", "BackEnd", "FrontEnd", "Storage", "usEast1Stack", "main", "context", "app", "App", "configFileName", "readFileSync", "resolve", "stack", "__require"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/cdk",
3
- "version": "2.0.30",
3
+ "version": "2.0.31",
4
4
  "description": "Medplum CDK Infra as Code",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
@@ -20,12 +20,12 @@
20
20
  "test": "jest --runInBand"
21
21
  },
22
22
  "dependencies": {
23
- "@aws-sdk/types": "3.378.0",
23
+ "@aws-sdk/types": "3.391.0",
24
24
  "@medplum/core": "*",
25
- "aws-cdk-lib": "2.88.0",
26
- "cdk": "2.88.0",
27
- "cdk-nag": "2.27.81",
28
- "cdk-serverless-clamscan": "2.5.34",
25
+ "aws-cdk-lib": "2.92.0",
26
+ "cdk": "2.92.0",
27
+ "cdk-nag": "2.27.107",
28
+ "cdk-serverless-clamscan": "2.5.60",
29
29
  "constructs": "10.2.69"
30
30
  },
31
31
  "bin": {
package/src/index.test.ts CHANGED
@@ -465,4 +465,37 @@ describe('Infra', () => {
465
465
  expect(() => main({ config: filename })).not.toThrow();
466
466
  unlinkSync(filename);
467
467
  });
468
+
469
+ test('Existing signing key', () => {
470
+ const filename = resolve('./medplum.signingKey.config.json');
471
+ writeFileSync(
472
+ filename,
473
+ JSON.stringify({
474
+ name: 'signingKey',
475
+ stackName: 'MedplumSigningKeyStack',
476
+ accountNumber: '647991932601',
477
+ region: 'us-east-1',
478
+ domainName: 'medplum.com',
479
+ apiPort: 8103,
480
+ apiDomainName: 'api.medplum.com',
481
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
482
+ appDomainName: 'app.medplum.com',
483
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
484
+ storageBucketName: 'medplum-storage',
485
+ storageDomainName: 'storage.medplum.com',
486
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
487
+ signingKeyId: 'K1234',
488
+ maxAzs: 2,
489
+ rdsInstances: 2,
490
+ desiredServerCount: 1,
491
+ serverImage: 'medplum/medplum-server:staging',
492
+ serverMemory: 512,
493
+ serverCpu: 256,
494
+ }),
495
+ { encoding: 'utf-8' }
496
+ );
497
+
498
+ expect(() => main({ config: filename })).not.toThrow();
499
+ unlinkSync(filename);
500
+ });
468
501
  });
package/src/storage.ts CHANGED
@@ -54,9 +54,14 @@ export class Storage extends Construct {
54
54
 
55
55
  if (region === 'us-east-1') {
56
56
  // Public key in PEM format
57
- const publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {
58
- encodedKey: config.storagePublicKey,
59
- });
57
+ let publicKey: cloudfront.IPublicKey;
58
+ if (config.signingKeyId) {
59
+ publicKey = cloudfront.PublicKey.fromPublicKeyId(this, 'StoragePublicKey', config.signingKeyId);
60
+ } else {
61
+ publicKey = new cloudfront.PublicKey(this, 'StoragePublicKey', {
62
+ encodedKey: config.storagePublicKey,
63
+ });
64
+ }
60
65
 
61
66
  // Authorized key group for presigned URLs
62
67
  const keyGroup = new cloudfront.KeyGroup(this, 'StorageKeyGroup', {