@medplum/cdk 2.1.1 → 2.1.3

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