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