@jaypie/constructs 1.2.50 → 1.2.52
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/JaypieGitHubDeployRole.d.ts +2 -0
- package/dist/cjs/JaypieStaticWebBucket.d.ts +4 -2
- package/dist/cjs/JaypieWebDeploymentBucket.d.ts +77 -2
- package/dist/cjs/__tests__/JaypieGitHubDeployRole.spec.d.ts +1 -0
- package/dist/cjs/__tests__/JaypieWebDeploymentBucket.spec.d.ts +1 -0
- package/dist/cjs/index.cjs +314 -17
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/JaypieGitHubDeployRole.d.ts +2 -0
- package/dist/esm/JaypieStaticWebBucket.d.ts +4 -2
- package/dist/esm/JaypieWebDeploymentBucket.d.ts +77 -2
- package/dist/esm/__tests__/JaypieGitHubDeployRole.spec.d.ts +1 -0
- package/dist/esm/__tests__/JaypieWebDeploymentBucket.spec.d.ts +1 -0
- package/dist/esm/index.js +314 -17
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Role } from "aws-cdk-lib/aws-iam";
|
|
2
2
|
import { Construct } from "constructs";
|
|
3
3
|
export interface JaypieGitHubDeployRoleProps {
|
|
4
|
+
ecr?: boolean;
|
|
4
5
|
oidcProviderArn?: string;
|
|
5
6
|
output?: boolean | string;
|
|
6
7
|
repoRestriction?: string;
|
|
8
|
+
sponsor?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare class JaypieGitHubDeployRole extends Construct {
|
|
9
11
|
private readonly _role;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
+
import { HostConfig } from "./helpers";
|
|
2
3
|
import { JaypieWebDeploymentBucket, JaypieWebDeploymentBucketProps } from "./JaypieWebDeploymentBucket";
|
|
3
4
|
export interface JaypieStaticWebBucketProps extends Omit<JaypieWebDeploymentBucketProps, "host" | "name" | "roleTag"> {
|
|
4
5
|
/**
|
|
5
|
-
* The domain name for the website
|
|
6
|
+
* The domain name for the website. Accepts a string or a HostConfig
|
|
7
|
+
* object resolved via envHostname().
|
|
6
8
|
* @default envHostname({ subdomain: "static" })
|
|
7
9
|
*/
|
|
8
|
-
host?: string;
|
|
10
|
+
host?: string | HostConfig;
|
|
9
11
|
/**
|
|
10
12
|
* Optional bucket name
|
|
11
13
|
* @default constructEnvName("static")
|
|
@@ -4,9 +4,21 @@ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
|
|
|
4
4
|
import { AddToResourcePolicyResult, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import * as route53 from "aws-cdk-lib/aws-route53";
|
|
6
6
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
7
|
+
import { LambdaDestination } from "aws-cdk-lib/aws-s3-notifications";
|
|
7
8
|
import * as kms from "aws-cdk-lib/aws-kms";
|
|
9
|
+
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
|
|
8
10
|
import { Construct } from "constructs";
|
|
11
|
+
import { HostConfig } from "./helpers";
|
|
12
|
+
import { JaypieWafConfig, SecurityHeadersOverrides } from "./JaypieDistribution";
|
|
9
13
|
import { JaypieHostedZone } from "./JaypieHostedZone";
|
|
14
|
+
/**
|
|
15
|
+
* WAF configuration for JaypieWebDeploymentBucket. Same shape as
|
|
16
|
+
* JaypieDistribution's JaypieWafConfig, but `name` is optional — when omitted,
|
|
17
|
+
* the construct id is used to namespace the WebACL and WAF log bucket.
|
|
18
|
+
*/
|
|
19
|
+
export type JaypieWebDeploymentBucketWafConfig = Omit<JaypieWafConfig, "name"> & {
|
|
20
|
+
name?: string;
|
|
21
|
+
};
|
|
10
22
|
export interface JaypieWebDeploymentBucketProps extends s3.BucketProps {
|
|
11
23
|
/**
|
|
12
24
|
* SSL certificate for the CloudFront distribution
|
|
@@ -14,19 +26,75 @@ export interface JaypieWebDeploymentBucketProps extends s3.BucketProps {
|
|
|
14
26
|
*/
|
|
15
27
|
certificate?: boolean | acm.ICertificate;
|
|
16
28
|
/**
|
|
17
|
-
*
|
|
29
|
+
* Log destination configuration for CloudFront access logs.
|
|
30
|
+
* - LambdaDestination: Use a specific Lambda destination for S3 notifications
|
|
31
|
+
* - true: Use Datadog forwarder for S3 notifications (default)
|
|
32
|
+
* - false: Disable S3 notifications (logging still occurs if logBucket is set)
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
destination?: LambdaDestination | boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The domain name for the website.
|
|
38
|
+
*
|
|
39
|
+
* Supports both string and config object:
|
|
40
|
+
* - String: used directly as the domain name (e.g., "app.example.com")
|
|
41
|
+
* - Object: passed to envHostname() to construct the domain name
|
|
42
|
+
* - { subdomain, domain, env, component }
|
|
43
|
+
*
|
|
18
44
|
* @default mergeDomain(CDK_ENV_WEB_SUBDOMAIN, CDK_ENV_WEB_HOSTED_ZONE || CDK_ENV_HOSTED_ZONE)
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Direct string
|
|
48
|
+
* host: "app.example.com"
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Config object - resolves using envHostname()
|
|
52
|
+
* host: { subdomain: "app" }
|
|
19
53
|
*/
|
|
20
|
-
host?: string;
|
|
54
|
+
host?: string | HostConfig;
|
|
55
|
+
/**
|
|
56
|
+
* External log bucket for CloudFront access logs.
|
|
57
|
+
* - IBucket: Use existing bucket directly
|
|
58
|
+
* - string: Bucket name to import
|
|
59
|
+
* - { exportName: string }: CloudFormation export name to import
|
|
60
|
+
* - true: Use account logging bucket (CDK.IMPORT.LOG_BUCKET)
|
|
61
|
+
* @default undefined (creates new bucket if destination !== false)
|
|
62
|
+
*/
|
|
63
|
+
logBucket?: s3.IBucket | string | {
|
|
64
|
+
exportName: string;
|
|
65
|
+
} | true;
|
|
21
66
|
/**
|
|
22
67
|
* Optional bucket name
|
|
23
68
|
*/
|
|
24
69
|
name?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Full override for the response headers policy.
|
|
72
|
+
* When provided, bypasses all default security header logic.
|
|
73
|
+
*/
|
|
74
|
+
responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
|
|
25
75
|
/**
|
|
26
76
|
* Role tag for tagging resources
|
|
27
77
|
* @default CDK.ROLE.HOSTING
|
|
28
78
|
*/
|
|
29
79
|
roleTag?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Security headers configuration.
|
|
82
|
+
* - true/undefined: apply sensible defaults (HSTS, X-Frame-Options, CSP, etc.)
|
|
83
|
+
* - false: disable security headers entirely
|
|
84
|
+
* - SecurityHeadersOverrides object: merge overrides with defaults
|
|
85
|
+
* @default true
|
|
86
|
+
*/
|
|
87
|
+
securityHeaders?: boolean | SecurityHeadersOverrides;
|
|
88
|
+
/**
|
|
89
|
+
* WAF WebACL configuration for the CloudFront distribution.
|
|
90
|
+
* - true/undefined: create and attach a WebACL with sensible defaults; the
|
|
91
|
+
* construct id is used to namespace the WebACL and WAF log bucket
|
|
92
|
+
* - false: disable WAF
|
|
93
|
+
* - JaypieWebDeploymentBucketWafConfig: customize WAF behavior; if `name`
|
|
94
|
+
* is omitted the construct id is used
|
|
95
|
+
* @default true
|
|
96
|
+
*/
|
|
97
|
+
waf?: boolean | JaypieWebDeploymentBucketWafConfig;
|
|
30
98
|
/**
|
|
31
99
|
* The hosted zone for DNS records
|
|
32
100
|
* @default CDK_ENV_WEB_HOSTED_ZONE || CDK_ENV_HOSTED_ZONE
|
|
@@ -50,7 +118,14 @@ export declare class JaypieWebDeploymentBucket extends Construct implements s3.I
|
|
|
50
118
|
readonly distributionDomainName?: string;
|
|
51
119
|
readonly certificate?: acm.ICertificate;
|
|
52
120
|
readonly distribution?: cloudfront.Distribution;
|
|
121
|
+
readonly logBucket?: s3.IBucket;
|
|
122
|
+
readonly responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
|
|
123
|
+
readonly wafLogBucket?: s3.IBucket;
|
|
124
|
+
readonly webAcl?: wafv2.CfnWebACL;
|
|
53
125
|
constructor(scope: Construct, id: string, props?: JaypieWebDeploymentBucketProps);
|
|
126
|
+
private resolveWafConfig;
|
|
127
|
+
private isExportNameObject;
|
|
128
|
+
private resolveLogBucket;
|
|
54
129
|
addEventNotification(event: s3.EventType, dest: s3.IBucketNotificationDestination, ...filters: s3.NotificationKeyFilter[]): void;
|
|
55
130
|
addObjectCreatedNotification(dest: s3.IBucketNotificationDestination, ...filters: s3.NotificationKeyFilter[]): void;
|
|
56
131
|
addObjectRemovedNotification(dest: s3.IBucketNotificationDestination, ...filters: s3.NotificationKeyFilter[]): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -2431,8 +2431,8 @@ class JaypieDatadogSecret extends JaypieEnvSecret {
|
|
|
2431
2431
|
}
|
|
2432
2432
|
}
|
|
2433
2433
|
|
|
2434
|
-
const DEFAULT_RATE_LIMIT = 2000;
|
|
2435
|
-
const DEFAULT_MANAGED_RULES = [
|
|
2434
|
+
const DEFAULT_RATE_LIMIT$1 = 2000;
|
|
2435
|
+
const DEFAULT_MANAGED_RULES$1 = [
|
|
2436
2436
|
"AWSManagedRulesCommonRuleSet",
|
|
2437
2437
|
"AWSManagedRulesKnownBadInputsRuleSet",
|
|
2438
2438
|
];
|
|
@@ -2693,7 +2693,7 @@ class JaypieDistribution extends constructs.Construct {
|
|
|
2693
2693
|
}
|
|
2694
2694
|
else {
|
|
2695
2695
|
// Create new WebACL
|
|
2696
|
-
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES, rateLimitPerIp = DEFAULT_RATE_LIMIT, } = wafConfig;
|
|
2696
|
+
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES$1, rateLimitPerIp = DEFAULT_RATE_LIMIT$1, } = wafConfig;
|
|
2697
2697
|
let priority = 0;
|
|
2698
2698
|
const rules = [];
|
|
2699
2699
|
// Add managed rule groups
|
|
@@ -3299,23 +3299,33 @@ class JaypieExpressLambda extends JaypieLambda {
|
|
|
3299
3299
|
}
|
|
3300
3300
|
}
|
|
3301
3301
|
|
|
3302
|
+
const ECR_PUSH_ACTIONS = [
|
|
3303
|
+
"ecr:BatchCheckLayerAvailability",
|
|
3304
|
+
"ecr:BatchGetImage",
|
|
3305
|
+
"ecr:CompleteLayerUpload",
|
|
3306
|
+
"ecr:CreateRepository",
|
|
3307
|
+
"ecr:DescribeRepositories",
|
|
3308
|
+
"ecr:InitiateLayerUpload",
|
|
3309
|
+
"ecr:PutImage",
|
|
3310
|
+
"ecr:UploadLayerPart",
|
|
3311
|
+
];
|
|
3302
3312
|
class JaypieGitHubDeployRole extends constructs.Construct {
|
|
3303
3313
|
constructor(scope, id = "GitHubDeployRole", props = {}) {
|
|
3304
3314
|
super(scope, id);
|
|
3305
|
-
const { oidcProviderArn = cdk.Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), output = true, repoRestriction: propsRepoRestriction, } = props;
|
|
3315
|
+
const { ecr = true, oidcProviderArn = cdk.Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), output = true, repoRestriction: propsRepoRestriction, sponsor: propsSponsor, } = props;
|
|
3306
3316
|
// Extract account ID from the scope
|
|
3307
3317
|
const accountId = cdk.Stack.of(this).account;
|
|
3308
|
-
// Resolve repoRestriction from props or environment variables
|
|
3318
|
+
// Resolve repoRestriction and sponsor from props or environment variables
|
|
3319
|
+
const envRepo = process.env.CDK_ENV_REPO || process.env.PROJECT_REPO;
|
|
3320
|
+
const envRepoOrganization = envRepo ? envRepo.split("/")[0] : undefined;
|
|
3309
3321
|
let repoRestriction = propsRepoRestriction;
|
|
3310
3322
|
if (!repoRestriction) {
|
|
3311
|
-
|
|
3312
|
-
if (!envRepo) {
|
|
3323
|
+
if (!envRepoOrganization) {
|
|
3313
3324
|
throw new errors.ConfigurationError("No repoRestriction provided. Set repoRestriction prop, CDK_ENV_REPO, or PROJECT_REPO environment variable");
|
|
3314
3325
|
}
|
|
3315
|
-
|
|
3316
|
-
const organization = envRepo.split("/")[0];
|
|
3317
|
-
repoRestriction = `repo:${organization}/*:*`;
|
|
3326
|
+
repoRestriction = `repo:${envRepoOrganization}/*:*`;
|
|
3318
3327
|
}
|
|
3328
|
+
const sponsor = propsSponsor || process.env.PROJECT_SPONSOR || envRepoOrganization;
|
|
3319
3329
|
// Create the IAM role
|
|
3320
3330
|
this._role = new awsIam.Role(this, "GitHubActionsRole", {
|
|
3321
3331
|
assumedBy: new awsIam.FederatedPrincipal(oidcProviderArn, {
|
|
@@ -3363,6 +3373,22 @@ class JaypieGitHubDeployRole extends constructs.Construct {
|
|
|
3363
3373
|
"arn:aws:iam::*:role/cdk-readOnlyRole",
|
|
3364
3374
|
],
|
|
3365
3375
|
}));
|
|
3376
|
+
// Grant ECR auth + push scoped to <sponsor>-* repositories
|
|
3377
|
+
if (ecr) {
|
|
3378
|
+
if (!sponsor) {
|
|
3379
|
+
throw new errors.ConfigurationError("Cannot grant default ECR permissions without a sponsor. Set sponsor prop, PROJECT_SPONSOR, CDK_ENV_REPO, or PROJECT_REPO, or pass `ecr: false`");
|
|
3380
|
+
}
|
|
3381
|
+
this._role.addToPolicy(new awsIam.PolicyStatement({
|
|
3382
|
+
actions: ["ecr:GetAuthorizationToken"],
|
|
3383
|
+
effect: awsIam.Effect.ALLOW,
|
|
3384
|
+
resources: ["*"],
|
|
3385
|
+
}));
|
|
3386
|
+
this._role.addToPolicy(new awsIam.PolicyStatement({
|
|
3387
|
+
actions: ECR_PUSH_ACTIONS,
|
|
3388
|
+
effect: awsIam.Effect.ALLOW,
|
|
3389
|
+
resources: [`arn:aws:ecr:*:${accountId}:repository/${sponsor}-*`],
|
|
3390
|
+
}));
|
|
3391
|
+
}
|
|
3366
3392
|
// Export the ARN of the role
|
|
3367
3393
|
if (output !== false) {
|
|
3368
3394
|
const outputId = typeof output === "string" ? output : "GitHubActionsRoleArn";
|
|
@@ -4210,10 +4236,16 @@ class JaypieSsoSyncApplication extends constructs.Construct {
|
|
|
4210
4236
|
}
|
|
4211
4237
|
}
|
|
4212
4238
|
|
|
4239
|
+
const DEFAULT_RATE_LIMIT = 2000;
|
|
4240
|
+
const DEFAULT_MANAGED_RULES = [
|
|
4241
|
+
"AWSManagedRulesCommonRuleSet",
|
|
4242
|
+
"AWSManagedRulesKnownBadInputsRuleSet",
|
|
4243
|
+
];
|
|
4213
4244
|
class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
4214
4245
|
constructor(scope, id, props = {}) {
|
|
4215
4246
|
super(scope, id);
|
|
4216
|
-
const
|
|
4247
|
+
const { certificate: certificateProp, destination: destinationProp = true, host: propsHost, logBucket: logBucketProp, name: nameProp, responseHeadersPolicy: responseHeadersPolicyProp, roleTag: roleTagProp, securityHeaders: securityHeadersProp, waf: wafProp = true, zone: propsZone, ...bucketProps } = props;
|
|
4248
|
+
const roleTag = roleTagProp || CDK$2.ROLE.HOSTING;
|
|
4217
4249
|
// Environment variable validation
|
|
4218
4250
|
if (process.env.CDK_ENV_WEB_SUBDOMAIN &&
|
|
4219
4251
|
!isValidSubdomain(process.env.CDK_ENV_WEB_SUBDOMAIN)) {
|
|
@@ -4228,8 +4260,19 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4228
4260
|
throw new errors.ConfigurationError("CDK_ENV_HOSTED_ZONE is not a valid hostname");
|
|
4229
4261
|
}
|
|
4230
4262
|
// Determine host from props or environment
|
|
4231
|
-
let host
|
|
4232
|
-
if (
|
|
4263
|
+
let host;
|
|
4264
|
+
if (typeof propsHost === "string") {
|
|
4265
|
+
host = propsHost;
|
|
4266
|
+
}
|
|
4267
|
+
else if (typeof propsHost === "object") {
|
|
4268
|
+
try {
|
|
4269
|
+
host = envHostname(propsHost);
|
|
4270
|
+
}
|
|
4271
|
+
catch {
|
|
4272
|
+
host = undefined;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
else {
|
|
4233
4276
|
try {
|
|
4234
4277
|
host =
|
|
4235
4278
|
process.env.CDK_ENV_WEB_HOST ||
|
|
@@ -4245,7 +4288,7 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4245
4288
|
throw new errors.ConfigurationError("Host is not a valid hostname");
|
|
4246
4289
|
}
|
|
4247
4290
|
// Determine zone from props or environment
|
|
4248
|
-
const zone =
|
|
4291
|
+
const zone = propsZone ||
|
|
4249
4292
|
process.env.CDK_ENV_WEB_HOSTED_ZONE ||
|
|
4250
4293
|
process.env.CDK_ENV_HOSTED_ZONE;
|
|
4251
4294
|
// Create the S3 bucket
|
|
@@ -4253,13 +4296,13 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4253
4296
|
accessControl: s3__namespace.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
|
|
4254
4297
|
autoDeleteObjects: true,
|
|
4255
4298
|
blockPublicAccess: s3__namespace.BlockPublicAccess.BLOCK_ACLS_ONLY,
|
|
4256
|
-
bucketName:
|
|
4299
|
+
bucketName: nameProp || constructEnvName("web"),
|
|
4257
4300
|
publicReadAccess: true,
|
|
4258
4301
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
4259
4302
|
versioned: false,
|
|
4260
4303
|
websiteErrorDocument: "index.html",
|
|
4261
4304
|
websiteIndexDocument: "index.html",
|
|
4262
|
-
...
|
|
4305
|
+
...bucketProps,
|
|
4263
4306
|
});
|
|
4264
4307
|
// Delegate IBucket properties to the bucket
|
|
4265
4308
|
this.bucketArn = this.bucket.bucketArn;
|
|
@@ -4341,7 +4384,7 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4341
4384
|
}
|
|
4342
4385
|
// Use resolveCertificate to create certificate at stack level (enables reuse when swapping constructs)
|
|
4343
4386
|
this.certificate = resolveCertificate(this, {
|
|
4344
|
-
certificate:
|
|
4387
|
+
certificate: certificateProp,
|
|
4345
4388
|
domainName: host,
|
|
4346
4389
|
roleTag,
|
|
4347
4390
|
zone: hostedZone,
|
|
@@ -4351,15 +4394,126 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4351
4394
|
value: this.certificate.certificateArn,
|
|
4352
4395
|
});
|
|
4353
4396
|
}
|
|
4397
|
+
// Resolve response headers policy for security headers
|
|
4398
|
+
let resolvedResponseHeadersPolicy;
|
|
4399
|
+
if (responseHeadersPolicyProp) {
|
|
4400
|
+
resolvedResponseHeadersPolicy = responseHeadersPolicyProp;
|
|
4401
|
+
}
|
|
4402
|
+
else if (securityHeadersProp !== false) {
|
|
4403
|
+
const overrides = typeof securityHeadersProp === "object" ? securityHeadersProp : {};
|
|
4404
|
+
resolvedResponseHeadersPolicy = new cloudfront__namespace.ResponseHeadersPolicy(this, "SecurityHeaders", {
|
|
4405
|
+
customHeadersBehavior: {
|
|
4406
|
+
customHeaders: [
|
|
4407
|
+
{
|
|
4408
|
+
header: "Cache-Control",
|
|
4409
|
+
override: true,
|
|
4410
|
+
value: "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
4411
|
+
},
|
|
4412
|
+
{
|
|
4413
|
+
header: "Cross-Origin-Embedder-Policy",
|
|
4414
|
+
override: true,
|
|
4415
|
+
value: "unsafe-none",
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
header: "Cross-Origin-Opener-Policy",
|
|
4419
|
+
override: true,
|
|
4420
|
+
value: "same-origin",
|
|
4421
|
+
},
|
|
4422
|
+
{
|
|
4423
|
+
header: "Cross-Origin-Resource-Policy",
|
|
4424
|
+
override: true,
|
|
4425
|
+
value: "same-origin",
|
|
4426
|
+
},
|
|
4427
|
+
{
|
|
4428
|
+
header: "Permissions-Policy",
|
|
4429
|
+
override: true,
|
|
4430
|
+
value: overrides.permissionsPolicy ??
|
|
4431
|
+
CDK$2.SECURITY_HEADERS.PERMISSIONS_POLICY,
|
|
4432
|
+
},
|
|
4433
|
+
],
|
|
4434
|
+
},
|
|
4435
|
+
removeHeaders: ["Server"],
|
|
4436
|
+
securityHeadersBehavior: {
|
|
4437
|
+
contentSecurityPolicy: {
|
|
4438
|
+
contentSecurityPolicy: overrides.contentSecurityPolicy ??
|
|
4439
|
+
CDK$2.SECURITY_HEADERS.CONTENT_SECURITY_POLICY,
|
|
4440
|
+
override: true,
|
|
4441
|
+
},
|
|
4442
|
+
contentTypeOptions: { override: true },
|
|
4443
|
+
frameOptions: {
|
|
4444
|
+
frameOption: overrides.frameOption ?? cloudfront__namespace.HeadersFrameOption.DENY,
|
|
4445
|
+
override: true,
|
|
4446
|
+
},
|
|
4447
|
+
referrerPolicy: {
|
|
4448
|
+
referrerPolicy: overrides.referrerPolicy ??
|
|
4449
|
+
cloudfront__namespace.HeadersReferrerPolicy
|
|
4450
|
+
.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
|
|
4451
|
+
override: true,
|
|
4452
|
+
},
|
|
4453
|
+
strictTransportSecurity: {
|
|
4454
|
+
accessControlMaxAge: cdk.Duration.seconds(overrides.hstsMaxAge ?? CDK$2.SECURITY_HEADERS.HSTS_MAX_AGE),
|
|
4455
|
+
includeSubdomains: overrides.hstsIncludeSubdomains ?? true,
|
|
4456
|
+
override: true,
|
|
4457
|
+
preload: true,
|
|
4458
|
+
},
|
|
4459
|
+
},
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
this.responseHeadersPolicy = resolvedResponseHeadersPolicy;
|
|
4463
|
+
// Resolve or create access log bucket
|
|
4464
|
+
let accessLogBucket;
|
|
4465
|
+
const isExternalLogBucket = logBucketProp !== undefined;
|
|
4466
|
+
if (logBucketProp !== undefined) {
|
|
4467
|
+
accessLogBucket = this.resolveLogBucket(logBucketProp);
|
|
4468
|
+
}
|
|
4469
|
+
else if (destinationProp !== false) {
|
|
4470
|
+
const createdBucket = new s3__namespace.Bucket(this, constructEnvName("LogBucket"), {
|
|
4471
|
+
autoDeleteObjects: true,
|
|
4472
|
+
lifecycleRules: [
|
|
4473
|
+
{
|
|
4474
|
+
expiration: cdk.Duration.days(90),
|
|
4475
|
+
transitions: [
|
|
4476
|
+
{
|
|
4477
|
+
storageClass: s3__namespace.StorageClass.INFREQUENT_ACCESS,
|
|
4478
|
+
transitionAfter: cdk.Duration.days(30),
|
|
4479
|
+
},
|
|
4480
|
+
],
|
|
4481
|
+
},
|
|
4482
|
+
],
|
|
4483
|
+
objectOwnership: s3__namespace.ObjectOwnership.OBJECT_WRITER,
|
|
4484
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
4485
|
+
});
|
|
4486
|
+
cdk.Tags.of(createdBucket).add(CDK$2.TAG.ROLE, CDK$2.ROLE.STORAGE);
|
|
4487
|
+
accessLogBucket = createdBucket;
|
|
4488
|
+
}
|
|
4489
|
+
if (accessLogBucket &&
|
|
4490
|
+
destinationProp !== false &&
|
|
4491
|
+
!isExternalLogBucket) {
|
|
4492
|
+
const lambdaDestination = destinationProp === true
|
|
4493
|
+
? new s3n.LambdaDestination(resolveDatadogForwarderFunction(this))
|
|
4494
|
+
: destinationProp;
|
|
4495
|
+
accessLogBucket.addEventNotification(s3__namespace.EventType.OBJECT_CREATED, lambdaDestination);
|
|
4496
|
+
}
|
|
4497
|
+
this.logBucket = accessLogBucket;
|
|
4354
4498
|
// Create CloudFront distribution
|
|
4355
4499
|
this.distribution = new cloudfront__namespace.Distribution(this, "Distribution", {
|
|
4356
4500
|
defaultBehavior: {
|
|
4357
4501
|
cachePolicy: cloudfront__namespace.CachePolicy.CACHING_DISABLED,
|
|
4358
4502
|
origin: new origins__namespace.S3StaticWebsiteOrigin(this.bucket),
|
|
4503
|
+
...(resolvedResponseHeadersPolicy
|
|
4504
|
+
? { responseHeadersPolicy: resolvedResponseHeadersPolicy }
|
|
4505
|
+
: {}),
|
|
4359
4506
|
viewerProtocolPolicy: cloudfront__namespace.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
4360
4507
|
},
|
|
4361
4508
|
certificate: this.certificate,
|
|
4362
4509
|
domainNames: [host],
|
|
4510
|
+
...(accessLogBucket
|
|
4511
|
+
? {
|
|
4512
|
+
enableLogging: true,
|
|
4513
|
+
logBucket: accessLogBucket,
|
|
4514
|
+
logFilePrefix: "cloudfront-logs/",
|
|
4515
|
+
}
|
|
4516
|
+
: {}),
|
|
4363
4517
|
});
|
|
4364
4518
|
cdk.Tags.of(this.distribution).add(CDK$2.TAG.ROLE, roleTag);
|
|
4365
4519
|
// If this is production, enable caching on everything but index.html
|
|
@@ -4367,6 +4521,9 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4367
4521
|
this.distribution.addBehavior("/*", new origins__namespace.S3StaticWebsiteOrigin(this.bucket), {
|
|
4368
4522
|
viewerProtocolPolicy: cloudfront__namespace.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
4369
4523
|
cachePolicy: cloudfront__namespace.CachePolicy.CACHING_OPTIMIZED,
|
|
4524
|
+
...(resolvedResponseHeadersPolicy
|
|
4525
|
+
? { responseHeadersPolicy: resolvedResponseHeadersPolicy }
|
|
4526
|
+
: {}),
|
|
4370
4527
|
});
|
|
4371
4528
|
}
|
|
4372
4529
|
// Create DNS record
|
|
@@ -4391,7 +4548,147 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
|
|
|
4391
4548
|
],
|
|
4392
4549
|
}));
|
|
4393
4550
|
}
|
|
4551
|
+
// Create and attach WAF WebACL
|
|
4552
|
+
let resolvedWebAclArn;
|
|
4553
|
+
const wafConfig = this.resolveWafConfig(wafProp, id);
|
|
4554
|
+
if (wafConfig) {
|
|
4555
|
+
if (wafConfig.webAclArn) {
|
|
4556
|
+
resolvedWebAclArn = wafConfig.webAclArn;
|
|
4557
|
+
this.distribution.attachWebAclId(wafConfig.webAclArn);
|
|
4558
|
+
}
|
|
4559
|
+
else {
|
|
4560
|
+
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES, rateLimitPerIp = DEFAULT_RATE_LIMIT, } = wafConfig;
|
|
4561
|
+
let priority = 0;
|
|
4562
|
+
const rules = [];
|
|
4563
|
+
for (const ruleName of managedRules) {
|
|
4564
|
+
const ruleActionOverrides = managedRuleOverrides?.[ruleName];
|
|
4565
|
+
const scopeDownStatement = managedRuleScopeDowns?.[ruleName];
|
|
4566
|
+
rules.push({
|
|
4567
|
+
name: ruleName,
|
|
4568
|
+
priority: priority++,
|
|
4569
|
+
overrideAction: { none: {} },
|
|
4570
|
+
statement: {
|
|
4571
|
+
managedRuleGroupStatement: {
|
|
4572
|
+
name: ruleName,
|
|
4573
|
+
vendorName: "AWS",
|
|
4574
|
+
...(ruleActionOverrides && { ruleActionOverrides }),
|
|
4575
|
+
...(scopeDownStatement && { scopeDownStatement }),
|
|
4576
|
+
},
|
|
4577
|
+
},
|
|
4578
|
+
visibilityConfig: {
|
|
4579
|
+
cloudWatchMetricsEnabled: true,
|
|
4580
|
+
metricName: ruleName,
|
|
4581
|
+
sampledRequestsEnabled: true,
|
|
4582
|
+
},
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
rules.push({
|
|
4586
|
+
name: "RateLimitPerIp",
|
|
4587
|
+
priority,
|
|
4588
|
+
action: { block: {} },
|
|
4589
|
+
statement: {
|
|
4590
|
+
rateBasedStatement: {
|
|
4591
|
+
aggregateKeyType: "IP",
|
|
4592
|
+
limit: rateLimitPerIp,
|
|
4593
|
+
},
|
|
4594
|
+
},
|
|
4595
|
+
visibilityConfig: {
|
|
4596
|
+
cloudWatchMetricsEnabled: true,
|
|
4597
|
+
metricName: "RateLimitPerIp",
|
|
4598
|
+
sampledRequestsEnabled: true,
|
|
4599
|
+
},
|
|
4600
|
+
});
|
|
4601
|
+
const webAclName = constructEnvName(`${wafConfig.name}-WebAcl`);
|
|
4602
|
+
const webAcl = new wafv2__namespace.CfnWebACL(this, "WebAcl", {
|
|
4603
|
+
defaultAction: { allow: {} },
|
|
4604
|
+
name: webAclName,
|
|
4605
|
+
rules,
|
|
4606
|
+
scope: "CLOUDFRONT",
|
|
4607
|
+
visibilityConfig: {
|
|
4608
|
+
cloudWatchMetricsEnabled: true,
|
|
4609
|
+
metricName: webAclName,
|
|
4610
|
+
sampledRequestsEnabled: true,
|
|
4611
|
+
},
|
|
4612
|
+
});
|
|
4613
|
+
this.webAcl = webAcl;
|
|
4614
|
+
resolvedWebAclArn = webAcl.attrArn;
|
|
4615
|
+
this.distribution.attachWebAclId(webAcl.attrArn);
|
|
4616
|
+
cdk.Tags.of(webAcl).add(CDK$2.TAG.ROLE, roleTag);
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
// Create WAF logging
|
|
4620
|
+
if (resolvedWebAclArn && wafConfig) {
|
|
4621
|
+
const { logBucket: wafLogBucketProp = true } = wafConfig;
|
|
4622
|
+
let wafLogBucket;
|
|
4623
|
+
if (wafLogBucketProp === true) {
|
|
4624
|
+
const wafLogBucketId = constructEnvName(`${wafConfig.name}-WafLogBucket`);
|
|
4625
|
+
const wafLogBucketName = `aws-waf-logs-${constructEnvName(`${wafConfig.name}-waf`).toLowerCase()}`;
|
|
4626
|
+
const createdBucket = new s3__namespace.Bucket(this, wafLogBucketId, {
|
|
4627
|
+
bucketName: wafLogBucketName,
|
|
4628
|
+
lifecycleRules: [
|
|
4629
|
+
{
|
|
4630
|
+
expiration: cdk.Duration.days(90),
|
|
4631
|
+
transitions: [
|
|
4632
|
+
{
|
|
4633
|
+
storageClass: s3__namespace.StorageClass.INFREQUENT_ACCESS,
|
|
4634
|
+
transitionAfter: cdk.Duration.days(30),
|
|
4635
|
+
},
|
|
4636
|
+
],
|
|
4637
|
+
},
|
|
4638
|
+
],
|
|
4639
|
+
objectOwnership: s3__namespace.ObjectOwnership.OBJECT_WRITER,
|
|
4640
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
4641
|
+
});
|
|
4642
|
+
cdk.Tags.of(createdBucket).add(CDK$2.TAG.ROLE, CDK$2.ROLE.MONITORING);
|
|
4643
|
+
if (destinationProp !== false) {
|
|
4644
|
+
const lambdaDestination = destinationProp === true
|
|
4645
|
+
? new s3n.LambdaDestination(resolveDatadogForwarderFunction(this))
|
|
4646
|
+
: destinationProp;
|
|
4647
|
+
createdBucket.addEventNotification(s3__namespace.EventType.OBJECT_CREATED, lambdaDestination);
|
|
4648
|
+
}
|
|
4649
|
+
wafLogBucket = createdBucket;
|
|
4650
|
+
}
|
|
4651
|
+
else if (typeof wafLogBucketProp === "object") {
|
|
4652
|
+
wafLogBucket = wafLogBucketProp;
|
|
4653
|
+
}
|
|
4654
|
+
if (wafLogBucket) {
|
|
4655
|
+
this.wafLogBucket = wafLogBucket;
|
|
4656
|
+
new wafv2__namespace.CfnLoggingConfiguration(this, "WafLoggingConfig", {
|
|
4657
|
+
logDestinationConfigs: [wafLogBucket.bucketArn],
|
|
4658
|
+
resourceArn: resolvedWebAclArn,
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
resolveWafConfig(wafProp, defaultName) {
|
|
4665
|
+
if (wafProp === false)
|
|
4666
|
+
return undefined;
|
|
4667
|
+
if (wafProp === true)
|
|
4668
|
+
return { name: defaultName };
|
|
4669
|
+
if (wafProp.enabled === false)
|
|
4670
|
+
return undefined;
|
|
4671
|
+
return { ...wafProp, name: wafProp.name || defaultName };
|
|
4672
|
+
}
|
|
4673
|
+
isExportNameObject(value) {
|
|
4674
|
+
return (typeof value === "object" &&
|
|
4675
|
+
value !== null &&
|
|
4676
|
+
"exportName" in value &&
|
|
4677
|
+
typeof value.exportName === "string");
|
|
4678
|
+
}
|
|
4679
|
+
resolveLogBucket(logBucketProp) {
|
|
4680
|
+
if (logBucketProp === true) {
|
|
4681
|
+
const bucketName = cdk.Fn.importValue(CDK$2.IMPORT.LOG_BUCKET);
|
|
4682
|
+
return s3__namespace.Bucket.fromBucketName(this, "ImportedLogBucket", bucketName);
|
|
4394
4683
|
}
|
|
4684
|
+
if (this.isExportNameObject(logBucketProp)) {
|
|
4685
|
+
const bucketName = cdk.Fn.importValue(logBucketProp.exportName);
|
|
4686
|
+
return s3__namespace.Bucket.fromBucketName(this, "ImportedLogBucket", bucketName);
|
|
4687
|
+
}
|
|
4688
|
+
if (typeof logBucketProp === "string") {
|
|
4689
|
+
return s3__namespace.Bucket.fromBucketName(this, "ImportedLogBucket", logBucketProp);
|
|
4690
|
+
}
|
|
4691
|
+
return logBucketProp;
|
|
4395
4692
|
}
|
|
4396
4693
|
// Implement remaining IBucket methods by delegating to the bucket
|
|
4397
4694
|
addEventNotification(event, dest, ...filters) {
|