@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/esm/index.js
CHANGED
|
@@ -2395,8 +2395,8 @@ class JaypieDatadogSecret extends JaypieEnvSecret {
|
|
|
2395
2395
|
}
|
|
2396
2396
|
}
|
|
2397
2397
|
|
|
2398
|
-
const DEFAULT_RATE_LIMIT = 2000;
|
|
2399
|
-
const DEFAULT_MANAGED_RULES = [
|
|
2398
|
+
const DEFAULT_RATE_LIMIT$1 = 2000;
|
|
2399
|
+
const DEFAULT_MANAGED_RULES$1 = [
|
|
2400
2400
|
"AWSManagedRulesCommonRuleSet",
|
|
2401
2401
|
"AWSManagedRulesKnownBadInputsRuleSet",
|
|
2402
2402
|
];
|
|
@@ -2657,7 +2657,7 @@ class JaypieDistribution extends Construct {
|
|
|
2657
2657
|
}
|
|
2658
2658
|
else {
|
|
2659
2659
|
// Create new WebACL
|
|
2660
|
-
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES, rateLimitPerIp = DEFAULT_RATE_LIMIT, } = wafConfig;
|
|
2660
|
+
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES$1, rateLimitPerIp = DEFAULT_RATE_LIMIT$1, } = wafConfig;
|
|
2661
2661
|
let priority = 0;
|
|
2662
2662
|
const rules = [];
|
|
2663
2663
|
// Add managed rule groups
|
|
@@ -3263,23 +3263,33 @@ class JaypieExpressLambda extends JaypieLambda {
|
|
|
3263
3263
|
}
|
|
3264
3264
|
}
|
|
3265
3265
|
|
|
3266
|
+
const ECR_PUSH_ACTIONS = [
|
|
3267
|
+
"ecr:BatchCheckLayerAvailability",
|
|
3268
|
+
"ecr:BatchGetImage",
|
|
3269
|
+
"ecr:CompleteLayerUpload",
|
|
3270
|
+
"ecr:CreateRepository",
|
|
3271
|
+
"ecr:DescribeRepositories",
|
|
3272
|
+
"ecr:InitiateLayerUpload",
|
|
3273
|
+
"ecr:PutImage",
|
|
3274
|
+
"ecr:UploadLayerPart",
|
|
3275
|
+
];
|
|
3266
3276
|
class JaypieGitHubDeployRole extends Construct {
|
|
3267
3277
|
constructor(scope, id = "GitHubDeployRole", props = {}) {
|
|
3268
3278
|
super(scope, id);
|
|
3269
|
-
const { oidcProviderArn = Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), output = true, repoRestriction: propsRepoRestriction, } = props;
|
|
3279
|
+
const { ecr = true, oidcProviderArn = Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), output = true, repoRestriction: propsRepoRestriction, sponsor: propsSponsor, } = props;
|
|
3270
3280
|
// Extract account ID from the scope
|
|
3271
3281
|
const accountId = Stack.of(this).account;
|
|
3272
|
-
// Resolve repoRestriction from props or environment variables
|
|
3282
|
+
// Resolve repoRestriction and sponsor from props or environment variables
|
|
3283
|
+
const envRepo = process.env.CDK_ENV_REPO || process.env.PROJECT_REPO;
|
|
3284
|
+
const envRepoOrganization = envRepo ? envRepo.split("/")[0] : undefined;
|
|
3273
3285
|
let repoRestriction = propsRepoRestriction;
|
|
3274
3286
|
if (!repoRestriction) {
|
|
3275
|
-
|
|
3276
|
-
if (!envRepo) {
|
|
3287
|
+
if (!envRepoOrganization) {
|
|
3277
3288
|
throw new ConfigurationError("No repoRestriction provided. Set repoRestriction prop, CDK_ENV_REPO, or PROJECT_REPO environment variable");
|
|
3278
3289
|
}
|
|
3279
|
-
|
|
3280
|
-
const organization = envRepo.split("/")[0];
|
|
3281
|
-
repoRestriction = `repo:${organization}/*:*`;
|
|
3290
|
+
repoRestriction = `repo:${envRepoOrganization}/*:*`;
|
|
3282
3291
|
}
|
|
3292
|
+
const sponsor = propsSponsor || process.env.PROJECT_SPONSOR || envRepoOrganization;
|
|
3283
3293
|
// Create the IAM role
|
|
3284
3294
|
this._role = new Role(this, "GitHubActionsRole", {
|
|
3285
3295
|
assumedBy: new FederatedPrincipal(oidcProviderArn, {
|
|
@@ -3327,6 +3337,22 @@ class JaypieGitHubDeployRole extends Construct {
|
|
|
3327
3337
|
"arn:aws:iam::*:role/cdk-readOnlyRole",
|
|
3328
3338
|
],
|
|
3329
3339
|
}));
|
|
3340
|
+
// Grant ECR auth + push scoped to <sponsor>-* repositories
|
|
3341
|
+
if (ecr) {
|
|
3342
|
+
if (!sponsor) {
|
|
3343
|
+
throw new ConfigurationError("Cannot grant default ECR permissions without a sponsor. Set sponsor prop, PROJECT_SPONSOR, CDK_ENV_REPO, or PROJECT_REPO, or pass `ecr: false`");
|
|
3344
|
+
}
|
|
3345
|
+
this._role.addToPolicy(new PolicyStatement({
|
|
3346
|
+
actions: ["ecr:GetAuthorizationToken"],
|
|
3347
|
+
effect: Effect.ALLOW,
|
|
3348
|
+
resources: ["*"],
|
|
3349
|
+
}));
|
|
3350
|
+
this._role.addToPolicy(new PolicyStatement({
|
|
3351
|
+
actions: ECR_PUSH_ACTIONS,
|
|
3352
|
+
effect: Effect.ALLOW,
|
|
3353
|
+
resources: [`arn:aws:ecr:*:${accountId}:repository/${sponsor}-*`],
|
|
3354
|
+
}));
|
|
3355
|
+
}
|
|
3330
3356
|
// Export the ARN of the role
|
|
3331
3357
|
if (output !== false) {
|
|
3332
3358
|
const outputId = typeof output === "string" ? output : "GitHubActionsRoleArn";
|
|
@@ -4174,10 +4200,16 @@ class JaypieSsoSyncApplication extends Construct {
|
|
|
4174
4200
|
}
|
|
4175
4201
|
}
|
|
4176
4202
|
|
|
4203
|
+
const DEFAULT_RATE_LIMIT = 2000;
|
|
4204
|
+
const DEFAULT_MANAGED_RULES = [
|
|
4205
|
+
"AWSManagedRulesCommonRuleSet",
|
|
4206
|
+
"AWSManagedRulesKnownBadInputsRuleSet",
|
|
4207
|
+
];
|
|
4177
4208
|
class JaypieWebDeploymentBucket extends Construct {
|
|
4178
4209
|
constructor(scope, id, props = {}) {
|
|
4179
4210
|
super(scope, id);
|
|
4180
|
-
const
|
|
4211
|
+
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;
|
|
4212
|
+
const roleTag = roleTagProp || CDK$2.ROLE.HOSTING;
|
|
4181
4213
|
// Environment variable validation
|
|
4182
4214
|
if (process.env.CDK_ENV_WEB_SUBDOMAIN &&
|
|
4183
4215
|
!isValidSubdomain(process.env.CDK_ENV_WEB_SUBDOMAIN)) {
|
|
@@ -4192,8 +4224,19 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4192
4224
|
throw new ConfigurationError("CDK_ENV_HOSTED_ZONE is not a valid hostname");
|
|
4193
4225
|
}
|
|
4194
4226
|
// Determine host from props or environment
|
|
4195
|
-
let host
|
|
4196
|
-
if (
|
|
4227
|
+
let host;
|
|
4228
|
+
if (typeof propsHost === "string") {
|
|
4229
|
+
host = propsHost;
|
|
4230
|
+
}
|
|
4231
|
+
else if (typeof propsHost === "object") {
|
|
4232
|
+
try {
|
|
4233
|
+
host = envHostname(propsHost);
|
|
4234
|
+
}
|
|
4235
|
+
catch {
|
|
4236
|
+
host = undefined;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
else {
|
|
4197
4240
|
try {
|
|
4198
4241
|
host =
|
|
4199
4242
|
process.env.CDK_ENV_WEB_HOST ||
|
|
@@ -4209,7 +4252,7 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4209
4252
|
throw new ConfigurationError("Host is not a valid hostname");
|
|
4210
4253
|
}
|
|
4211
4254
|
// Determine zone from props or environment
|
|
4212
|
-
const zone =
|
|
4255
|
+
const zone = propsZone ||
|
|
4213
4256
|
process.env.CDK_ENV_WEB_HOSTED_ZONE ||
|
|
4214
4257
|
process.env.CDK_ENV_HOSTED_ZONE;
|
|
4215
4258
|
// Create the S3 bucket
|
|
@@ -4217,13 +4260,13 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4217
4260
|
accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
|
|
4218
4261
|
autoDeleteObjects: true,
|
|
4219
4262
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS_ONLY,
|
|
4220
|
-
bucketName:
|
|
4263
|
+
bucketName: nameProp || constructEnvName("web"),
|
|
4221
4264
|
publicReadAccess: true,
|
|
4222
4265
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
4223
4266
|
versioned: false,
|
|
4224
4267
|
websiteErrorDocument: "index.html",
|
|
4225
4268
|
websiteIndexDocument: "index.html",
|
|
4226
|
-
...
|
|
4269
|
+
...bucketProps,
|
|
4227
4270
|
});
|
|
4228
4271
|
// Delegate IBucket properties to the bucket
|
|
4229
4272
|
this.bucketArn = this.bucket.bucketArn;
|
|
@@ -4305,7 +4348,7 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4305
4348
|
}
|
|
4306
4349
|
// Use resolveCertificate to create certificate at stack level (enables reuse when swapping constructs)
|
|
4307
4350
|
this.certificate = resolveCertificate(this, {
|
|
4308
|
-
certificate:
|
|
4351
|
+
certificate: certificateProp,
|
|
4309
4352
|
domainName: host,
|
|
4310
4353
|
roleTag,
|
|
4311
4354
|
zone: hostedZone,
|
|
@@ -4315,15 +4358,126 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4315
4358
|
value: this.certificate.certificateArn,
|
|
4316
4359
|
});
|
|
4317
4360
|
}
|
|
4361
|
+
// Resolve response headers policy for security headers
|
|
4362
|
+
let resolvedResponseHeadersPolicy;
|
|
4363
|
+
if (responseHeadersPolicyProp) {
|
|
4364
|
+
resolvedResponseHeadersPolicy = responseHeadersPolicyProp;
|
|
4365
|
+
}
|
|
4366
|
+
else if (securityHeadersProp !== false) {
|
|
4367
|
+
const overrides = typeof securityHeadersProp === "object" ? securityHeadersProp : {};
|
|
4368
|
+
resolvedResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, "SecurityHeaders", {
|
|
4369
|
+
customHeadersBehavior: {
|
|
4370
|
+
customHeaders: [
|
|
4371
|
+
{
|
|
4372
|
+
header: "Cache-Control",
|
|
4373
|
+
override: true,
|
|
4374
|
+
value: "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
4375
|
+
},
|
|
4376
|
+
{
|
|
4377
|
+
header: "Cross-Origin-Embedder-Policy",
|
|
4378
|
+
override: true,
|
|
4379
|
+
value: "unsafe-none",
|
|
4380
|
+
},
|
|
4381
|
+
{
|
|
4382
|
+
header: "Cross-Origin-Opener-Policy",
|
|
4383
|
+
override: true,
|
|
4384
|
+
value: "same-origin",
|
|
4385
|
+
},
|
|
4386
|
+
{
|
|
4387
|
+
header: "Cross-Origin-Resource-Policy",
|
|
4388
|
+
override: true,
|
|
4389
|
+
value: "same-origin",
|
|
4390
|
+
},
|
|
4391
|
+
{
|
|
4392
|
+
header: "Permissions-Policy",
|
|
4393
|
+
override: true,
|
|
4394
|
+
value: overrides.permissionsPolicy ??
|
|
4395
|
+
CDK$2.SECURITY_HEADERS.PERMISSIONS_POLICY,
|
|
4396
|
+
},
|
|
4397
|
+
],
|
|
4398
|
+
},
|
|
4399
|
+
removeHeaders: ["Server"],
|
|
4400
|
+
securityHeadersBehavior: {
|
|
4401
|
+
contentSecurityPolicy: {
|
|
4402
|
+
contentSecurityPolicy: overrides.contentSecurityPolicy ??
|
|
4403
|
+
CDK$2.SECURITY_HEADERS.CONTENT_SECURITY_POLICY,
|
|
4404
|
+
override: true,
|
|
4405
|
+
},
|
|
4406
|
+
contentTypeOptions: { override: true },
|
|
4407
|
+
frameOptions: {
|
|
4408
|
+
frameOption: overrides.frameOption ?? cloudfront.HeadersFrameOption.DENY,
|
|
4409
|
+
override: true,
|
|
4410
|
+
},
|
|
4411
|
+
referrerPolicy: {
|
|
4412
|
+
referrerPolicy: overrides.referrerPolicy ??
|
|
4413
|
+
cloudfront.HeadersReferrerPolicy
|
|
4414
|
+
.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
|
|
4415
|
+
override: true,
|
|
4416
|
+
},
|
|
4417
|
+
strictTransportSecurity: {
|
|
4418
|
+
accessControlMaxAge: Duration.seconds(overrides.hstsMaxAge ?? CDK$2.SECURITY_HEADERS.HSTS_MAX_AGE),
|
|
4419
|
+
includeSubdomains: overrides.hstsIncludeSubdomains ?? true,
|
|
4420
|
+
override: true,
|
|
4421
|
+
preload: true,
|
|
4422
|
+
},
|
|
4423
|
+
},
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4426
|
+
this.responseHeadersPolicy = resolvedResponseHeadersPolicy;
|
|
4427
|
+
// Resolve or create access log bucket
|
|
4428
|
+
let accessLogBucket;
|
|
4429
|
+
const isExternalLogBucket = logBucketProp !== undefined;
|
|
4430
|
+
if (logBucketProp !== undefined) {
|
|
4431
|
+
accessLogBucket = this.resolveLogBucket(logBucketProp);
|
|
4432
|
+
}
|
|
4433
|
+
else if (destinationProp !== false) {
|
|
4434
|
+
const createdBucket = new s3.Bucket(this, constructEnvName("LogBucket"), {
|
|
4435
|
+
autoDeleteObjects: true,
|
|
4436
|
+
lifecycleRules: [
|
|
4437
|
+
{
|
|
4438
|
+
expiration: Duration.days(90),
|
|
4439
|
+
transitions: [
|
|
4440
|
+
{
|
|
4441
|
+
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
|
|
4442
|
+
transitionAfter: Duration.days(30),
|
|
4443
|
+
},
|
|
4444
|
+
],
|
|
4445
|
+
},
|
|
4446
|
+
],
|
|
4447
|
+
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
|
|
4448
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
4449
|
+
});
|
|
4450
|
+
Tags.of(createdBucket).add(CDK$2.TAG.ROLE, CDK$2.ROLE.STORAGE);
|
|
4451
|
+
accessLogBucket = createdBucket;
|
|
4452
|
+
}
|
|
4453
|
+
if (accessLogBucket &&
|
|
4454
|
+
destinationProp !== false &&
|
|
4455
|
+
!isExternalLogBucket) {
|
|
4456
|
+
const lambdaDestination = destinationProp === true
|
|
4457
|
+
? new LambdaDestination(resolveDatadogForwarderFunction(this))
|
|
4458
|
+
: destinationProp;
|
|
4459
|
+
accessLogBucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination);
|
|
4460
|
+
}
|
|
4461
|
+
this.logBucket = accessLogBucket;
|
|
4318
4462
|
// Create CloudFront distribution
|
|
4319
4463
|
this.distribution = new cloudfront.Distribution(this, "Distribution", {
|
|
4320
4464
|
defaultBehavior: {
|
|
4321
4465
|
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
|
4322
4466
|
origin: new origins.S3StaticWebsiteOrigin(this.bucket),
|
|
4467
|
+
...(resolvedResponseHeadersPolicy
|
|
4468
|
+
? { responseHeadersPolicy: resolvedResponseHeadersPolicy }
|
|
4469
|
+
: {}),
|
|
4323
4470
|
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
4324
4471
|
},
|
|
4325
4472
|
certificate: this.certificate,
|
|
4326
4473
|
domainNames: [host],
|
|
4474
|
+
...(accessLogBucket
|
|
4475
|
+
? {
|
|
4476
|
+
enableLogging: true,
|
|
4477
|
+
logBucket: accessLogBucket,
|
|
4478
|
+
logFilePrefix: "cloudfront-logs/",
|
|
4479
|
+
}
|
|
4480
|
+
: {}),
|
|
4327
4481
|
});
|
|
4328
4482
|
Tags.of(this.distribution).add(CDK$2.TAG.ROLE, roleTag);
|
|
4329
4483
|
// If this is production, enable caching on everything but index.html
|
|
@@ -4331,6 +4485,9 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4331
4485
|
this.distribution.addBehavior("/*", new origins.S3StaticWebsiteOrigin(this.bucket), {
|
|
4332
4486
|
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
4333
4487
|
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
4488
|
+
...(resolvedResponseHeadersPolicy
|
|
4489
|
+
? { responseHeadersPolicy: resolvedResponseHeadersPolicy }
|
|
4490
|
+
: {}),
|
|
4334
4491
|
});
|
|
4335
4492
|
}
|
|
4336
4493
|
// Create DNS record
|
|
@@ -4355,7 +4512,147 @@ class JaypieWebDeploymentBucket extends Construct {
|
|
|
4355
4512
|
],
|
|
4356
4513
|
}));
|
|
4357
4514
|
}
|
|
4515
|
+
// Create and attach WAF WebACL
|
|
4516
|
+
let resolvedWebAclArn;
|
|
4517
|
+
const wafConfig = this.resolveWafConfig(wafProp, id);
|
|
4518
|
+
if (wafConfig) {
|
|
4519
|
+
if (wafConfig.webAclArn) {
|
|
4520
|
+
resolvedWebAclArn = wafConfig.webAclArn;
|
|
4521
|
+
this.distribution.attachWebAclId(wafConfig.webAclArn);
|
|
4522
|
+
}
|
|
4523
|
+
else {
|
|
4524
|
+
const { managedRuleOverrides, managedRuleScopeDowns, managedRules = DEFAULT_MANAGED_RULES, rateLimitPerIp = DEFAULT_RATE_LIMIT, } = wafConfig;
|
|
4525
|
+
let priority = 0;
|
|
4526
|
+
const rules = [];
|
|
4527
|
+
for (const ruleName of managedRules) {
|
|
4528
|
+
const ruleActionOverrides = managedRuleOverrides?.[ruleName];
|
|
4529
|
+
const scopeDownStatement = managedRuleScopeDowns?.[ruleName];
|
|
4530
|
+
rules.push({
|
|
4531
|
+
name: ruleName,
|
|
4532
|
+
priority: priority++,
|
|
4533
|
+
overrideAction: { none: {} },
|
|
4534
|
+
statement: {
|
|
4535
|
+
managedRuleGroupStatement: {
|
|
4536
|
+
name: ruleName,
|
|
4537
|
+
vendorName: "AWS",
|
|
4538
|
+
...(ruleActionOverrides && { ruleActionOverrides }),
|
|
4539
|
+
...(scopeDownStatement && { scopeDownStatement }),
|
|
4540
|
+
},
|
|
4541
|
+
},
|
|
4542
|
+
visibilityConfig: {
|
|
4543
|
+
cloudWatchMetricsEnabled: true,
|
|
4544
|
+
metricName: ruleName,
|
|
4545
|
+
sampledRequestsEnabled: true,
|
|
4546
|
+
},
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4549
|
+
rules.push({
|
|
4550
|
+
name: "RateLimitPerIp",
|
|
4551
|
+
priority,
|
|
4552
|
+
action: { block: {} },
|
|
4553
|
+
statement: {
|
|
4554
|
+
rateBasedStatement: {
|
|
4555
|
+
aggregateKeyType: "IP",
|
|
4556
|
+
limit: rateLimitPerIp,
|
|
4557
|
+
},
|
|
4558
|
+
},
|
|
4559
|
+
visibilityConfig: {
|
|
4560
|
+
cloudWatchMetricsEnabled: true,
|
|
4561
|
+
metricName: "RateLimitPerIp",
|
|
4562
|
+
sampledRequestsEnabled: true,
|
|
4563
|
+
},
|
|
4564
|
+
});
|
|
4565
|
+
const webAclName = constructEnvName(`${wafConfig.name}-WebAcl`);
|
|
4566
|
+
const webAcl = new wafv2.CfnWebACL(this, "WebAcl", {
|
|
4567
|
+
defaultAction: { allow: {} },
|
|
4568
|
+
name: webAclName,
|
|
4569
|
+
rules,
|
|
4570
|
+
scope: "CLOUDFRONT",
|
|
4571
|
+
visibilityConfig: {
|
|
4572
|
+
cloudWatchMetricsEnabled: true,
|
|
4573
|
+
metricName: webAclName,
|
|
4574
|
+
sampledRequestsEnabled: true,
|
|
4575
|
+
},
|
|
4576
|
+
});
|
|
4577
|
+
this.webAcl = webAcl;
|
|
4578
|
+
resolvedWebAclArn = webAcl.attrArn;
|
|
4579
|
+
this.distribution.attachWebAclId(webAcl.attrArn);
|
|
4580
|
+
Tags.of(webAcl).add(CDK$2.TAG.ROLE, roleTag);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
// Create WAF logging
|
|
4584
|
+
if (resolvedWebAclArn && wafConfig) {
|
|
4585
|
+
const { logBucket: wafLogBucketProp = true } = wafConfig;
|
|
4586
|
+
let wafLogBucket;
|
|
4587
|
+
if (wafLogBucketProp === true) {
|
|
4588
|
+
const wafLogBucketId = constructEnvName(`${wafConfig.name}-WafLogBucket`);
|
|
4589
|
+
const wafLogBucketName = `aws-waf-logs-${constructEnvName(`${wafConfig.name}-waf`).toLowerCase()}`;
|
|
4590
|
+
const createdBucket = new s3.Bucket(this, wafLogBucketId, {
|
|
4591
|
+
bucketName: wafLogBucketName,
|
|
4592
|
+
lifecycleRules: [
|
|
4593
|
+
{
|
|
4594
|
+
expiration: Duration.days(90),
|
|
4595
|
+
transitions: [
|
|
4596
|
+
{
|
|
4597
|
+
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
|
|
4598
|
+
transitionAfter: Duration.days(30),
|
|
4599
|
+
},
|
|
4600
|
+
],
|
|
4601
|
+
},
|
|
4602
|
+
],
|
|
4603
|
+
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
|
|
4604
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
4605
|
+
});
|
|
4606
|
+
Tags.of(createdBucket).add(CDK$2.TAG.ROLE, CDK$2.ROLE.MONITORING);
|
|
4607
|
+
if (destinationProp !== false) {
|
|
4608
|
+
const lambdaDestination = destinationProp === true
|
|
4609
|
+
? new LambdaDestination(resolveDatadogForwarderFunction(this))
|
|
4610
|
+
: destinationProp;
|
|
4611
|
+
createdBucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination);
|
|
4612
|
+
}
|
|
4613
|
+
wafLogBucket = createdBucket;
|
|
4614
|
+
}
|
|
4615
|
+
else if (typeof wafLogBucketProp === "object") {
|
|
4616
|
+
wafLogBucket = wafLogBucketProp;
|
|
4617
|
+
}
|
|
4618
|
+
if (wafLogBucket) {
|
|
4619
|
+
this.wafLogBucket = wafLogBucket;
|
|
4620
|
+
new wafv2.CfnLoggingConfiguration(this, "WafLoggingConfig", {
|
|
4621
|
+
logDestinationConfigs: [wafLogBucket.bucketArn],
|
|
4622
|
+
resourceArn: resolvedWebAclArn,
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
resolveWafConfig(wafProp, defaultName) {
|
|
4629
|
+
if (wafProp === false)
|
|
4630
|
+
return undefined;
|
|
4631
|
+
if (wafProp === true)
|
|
4632
|
+
return { name: defaultName };
|
|
4633
|
+
if (wafProp.enabled === false)
|
|
4634
|
+
return undefined;
|
|
4635
|
+
return { ...wafProp, name: wafProp.name || defaultName };
|
|
4636
|
+
}
|
|
4637
|
+
isExportNameObject(value) {
|
|
4638
|
+
return (typeof value === "object" &&
|
|
4639
|
+
value !== null &&
|
|
4640
|
+
"exportName" in value &&
|
|
4641
|
+
typeof value.exportName === "string");
|
|
4642
|
+
}
|
|
4643
|
+
resolveLogBucket(logBucketProp) {
|
|
4644
|
+
if (logBucketProp === true) {
|
|
4645
|
+
const bucketName = Fn.importValue(CDK$2.IMPORT.LOG_BUCKET);
|
|
4646
|
+
return s3.Bucket.fromBucketName(this, "ImportedLogBucket", bucketName);
|
|
4358
4647
|
}
|
|
4648
|
+
if (this.isExportNameObject(logBucketProp)) {
|
|
4649
|
+
const bucketName = Fn.importValue(logBucketProp.exportName);
|
|
4650
|
+
return s3.Bucket.fromBucketName(this, "ImportedLogBucket", bucketName);
|
|
4651
|
+
}
|
|
4652
|
+
if (typeof logBucketProp === "string") {
|
|
4653
|
+
return s3.Bucket.fromBucketName(this, "ImportedLogBucket", logBucketProp);
|
|
4654
|
+
}
|
|
4655
|
+
return logBucketProp;
|
|
4359
4656
|
}
|
|
4360
4657
|
// Implement remaining IBucket methods by delegating to the bucket
|
|
4361
4658
|
addEventNotification(event, dest, ...filters) {
|