@aws/nx-plugin 1.0.0-rc.40 → 1.0.0-rc.41
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/package.json +1 -1
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +42 -0
- package/src/open-api/ts-client/generator.js.map +1 -1
- package/src/open-api/ts-hooks/generator.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +4 -1
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +17 -0
- package/src/ts/lib/__snapshots__/generator.spec.ts.snap +51 -0
- package/src/ts/lib/biome.js +36 -3
- package/src/ts/lib/biome.js.map +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +572 -195
- package/src/ts/sync/generator.js +0 -2
- package/src/ts/sync/generator.js.map +1 -1
- package/src/utils/ast.js.map +1 -1
- package/src/utils/files/terraform/src/core/asset-bucket/asset-bucket.tf.template +61 -70
- package/src/utils/format.js +11 -2
- package/src/utils/format.js.map +1 -1
- package/src/utils/warm-ruff-cache.d.ts +12 -0
- package/src/utils/warm-ruff-cache.js +39 -0
- package/src/utils/warm-ruff-cache.js.map +1 -0
- package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +127 -36
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +77 -74
|
@@ -152,6 +152,18 @@ variable "website_file_path" {
|
|
|
152
152
|
type = string
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
variable "custom_domain_names" {
|
|
156
|
+
description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."
|
|
157
|
+
type = list(string)
|
|
158
|
+
default = []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
variable "acm_certificate_arn" {
|
|
162
|
+
description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."
|
|
163
|
+
type = string
|
|
164
|
+
default = null
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
# Content-Security-Policy enforced on all responses. Restricts scripts and
|
|
156
168
|
# framing to mitigate XSS and clickjacking, while permitting HTTPS/WSS calls
|
|
157
169
|
# (connect-src) to AWS service endpoints such as API Gateway, Cognito and
|
|
@@ -159,6 +171,10 @@ variable "website_file_path" {
|
|
|
159
171
|
# connect-src to your specific origins once they are known.
|
|
160
172
|
locals {
|
|
161
173
|
content_security_policy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
174
|
+
|
|
175
|
+
# Delivery source/destination names have a 60 character maximum. Truncate the
|
|
176
|
+
# website name so the longest delivery name stays within the limit.
|
|
177
|
+
access_logs_name_prefix = substr(lower(var.website_name), 0, 16)
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
|
|
@@ -197,6 +213,26 @@ resource "aws_kms_key" "website_key" {
|
|
|
197
213
|
]
|
|
198
214
|
Resource = "*"
|
|
199
215
|
},
|
|
216
|
+
{
|
|
217
|
+
Sid = "AllowCloudWatchLogs"
|
|
218
|
+
Effect = "Allow"
|
|
219
|
+
Principal = {
|
|
220
|
+
Service = "logs.\${data.aws_region.current.region}.amazonaws.com"
|
|
221
|
+
}
|
|
222
|
+
Action = [
|
|
223
|
+
"kms:Encrypt",
|
|
224
|
+
"kms:Decrypt",
|
|
225
|
+
"kms:ReEncrypt*",
|
|
226
|
+
"kms:GenerateDataKey*",
|
|
227
|
+
"kms:DescribeKey"
|
|
228
|
+
]
|
|
229
|
+
Resource = "*"
|
|
230
|
+
Condition = {
|
|
231
|
+
ArnLike = {
|
|
232
|
+
"kms:EncryptionContext:aws:logs:arn" = "arn:aws:logs:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:log-group:*"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
200
236
|
{
|
|
201
237
|
Sid = "AllowCloudFrontServicePrincipalSSE-KMS"
|
|
202
238
|
Effect = "Allow"
|
|
@@ -228,18 +264,12 @@ resource "aws_kms_alias" "website_key_alias" {
|
|
|
228
264
|
target_key_id = aws_kms_key.website_key.key_id
|
|
229
265
|
}
|
|
230
266
|
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
bucket = "\${lower(var.website_name)}-access-logs-\${random_id.bucket_suffix.hex}"
|
|
238
|
-
force_destroy = true
|
|
239
|
-
|
|
240
|
-
tags = {
|
|
241
|
-
Name = "\${lower(var.website_name)}-access-logs"
|
|
242
|
-
}
|
|
267
|
+
# CloudWatch log group receiving S3 server access logs for the website and
|
|
268
|
+
# distribution log buckets.
|
|
269
|
+
resource "aws_cloudwatch_log_group" "access_logs" {
|
|
270
|
+
name = "/aws/s3/\${lower(var.website_name)}-access-logs-\${random_id.bucket_suffix.hex}"
|
|
271
|
+
retention_in_days = 365
|
|
272
|
+
kms_key_id = aws_kms_key.website_key.arn
|
|
243
273
|
}
|
|
244
274
|
|
|
245
275
|
resource "random_id" "unique_suffix" {
|
|
@@ -250,63 +280,12 @@ resource "random_id" "bucket_suffix" {
|
|
|
250
280
|
byte_length = 8
|
|
251
281
|
}
|
|
252
282
|
|
|
253
|
-
resource "aws_s3_bucket_versioning" "access_logs_versioning" {
|
|
254
|
-
bucket = aws_s3_bucket.access_logs.id
|
|
255
|
-
versioning_configuration {
|
|
256
|
-
status = "Disabled"
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs_encryption" {
|
|
261
|
-
bucket = aws_s3_bucket.access_logs.id
|
|
262
|
-
|
|
263
|
-
rule {
|
|
264
|
-
apply_server_side_encryption_by_default {
|
|
265
|
-
kms_master_key_id = aws_kms_key.website_key.arn
|
|
266
|
-
sse_algorithm = "aws:kms"
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
resource "aws_s3_bucket_public_access_block" "access_logs_pab" {
|
|
272
|
-
bucket = aws_s3_bucket.access_logs.id
|
|
273
|
-
|
|
274
|
-
block_public_acls = true
|
|
275
|
-
block_public_policy = true
|
|
276
|
-
ignore_public_acls = true
|
|
277
|
-
restrict_public_buckets = true
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
resource "aws_s3_bucket_policy" "access_logs_ssl_policy" {
|
|
281
|
-
bucket = aws_s3_bucket.access_logs.id
|
|
282
|
-
|
|
283
|
-
policy = jsonencode({
|
|
284
|
-
Version = "2012-10-17"
|
|
285
|
-
Statement = [
|
|
286
|
-
{
|
|
287
|
-
Sid = "DenyInsecureConnections"
|
|
288
|
-
Effect = "Deny"
|
|
289
|
-
Principal = "*"
|
|
290
|
-
Action = "s3:*"
|
|
291
|
-
Resource = [
|
|
292
|
-
aws_s3_bucket.access_logs.arn,
|
|
293
|
-
"\${aws_s3_bucket.access_logs.arn}/*"
|
|
294
|
-
]
|
|
295
|
-
Condition = {
|
|
296
|
-
Bool = {
|
|
297
|
-
"aws:SecureTransport" = "false"
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
]
|
|
302
|
-
})
|
|
303
|
-
}
|
|
304
|
-
|
|
305
283
|
# Website Bucket
|
|
306
284
|
resource "aws_s3_bucket" "website" {
|
|
307
285
|
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for static website content
|
|
308
286
|
#checkov:skip=CKV_AWS_144:Cross-region replication not required for static website
|
|
309
287
|
#checkov:skip=CKV2_AWS_62:Event notifications not required for static website bucket
|
|
288
|
+
#checkov:skip=CKV_AWS_18:Server access logs are delivered to CloudWatch Logs (see aws_cloudwatch_log_delivery.website)
|
|
310
289
|
bucket = "\${lower(var.website_name)}-website-\${random_id.bucket_suffix.hex}"
|
|
311
290
|
force_destroy = true
|
|
312
291
|
|
|
@@ -350,20 +329,35 @@ resource "aws_s3_bucket_ownership_controls" "website_ownership" {
|
|
|
350
329
|
}
|
|
351
330
|
}
|
|
352
331
|
|
|
353
|
-
|
|
354
|
-
|
|
332
|
+
# Deliver the website bucket's server access logs to CloudWatch Logs.
|
|
333
|
+
resource "aws_cloudwatch_log_delivery_source" "website" {
|
|
334
|
+
name = "\${local.access_logs_name_prefix}-website-logs-\${random_id.bucket_suffix.hex}"
|
|
335
|
+
log_type = "S3_SERVER_ACCESS_LOGS"
|
|
336
|
+
resource_arn = aws_s3_bucket.website.arn
|
|
337
|
+
}
|
|
355
338
|
|
|
356
|
-
|
|
357
|
-
|
|
339
|
+
resource "aws_cloudwatch_log_delivery_destination" "access_logs" {
|
|
340
|
+
name = "\${local.access_logs_name_prefix}-access-logs-\${random_id.bucket_suffix.hex}"
|
|
341
|
+
|
|
342
|
+
delivery_destination_configuration {
|
|
343
|
+
destination_resource_arn = aws_cloudwatch_log_group.access_logs.arn
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
resource "aws_cloudwatch_log_delivery" "website" {
|
|
348
|
+
delivery_source_name = aws_cloudwatch_log_delivery_source.website.name
|
|
349
|
+
delivery_destination_arn = aws_cloudwatch_log_delivery_destination.access_logs.arn
|
|
358
350
|
}
|
|
359
351
|
|
|
360
352
|
|
|
361
|
-
# Distribution Log Bucket
|
|
353
|
+
# Distribution Log Bucket — holds CloudFront standard access logs (CloudFront
|
|
354
|
+
# delivers only to S3). Its own S3 server access logs go to CloudWatch Logs.
|
|
362
355
|
resource "aws_s3_bucket" "distribution_logs" {
|
|
363
356
|
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for CloudFront logs
|
|
364
357
|
#checkov:skip=CKV_AWS_144:Cross-region replication not required for CloudFront logs
|
|
365
358
|
#checkov:skip=CKV2_AWS_62:Event notifications not required for CloudFront logs bucket
|
|
366
359
|
#checkov:skip=CKV_AWS_21:Versioning not required for CloudFront access logs
|
|
360
|
+
#checkov:skip=CKV_AWS_18:Server access logs are delivered to CloudWatch Logs (see aws_cloudwatch_log_delivery.distribution_logs)
|
|
367
361
|
bucket = "\${lower(var.website_name)}-distribution-logs-\${random_id.bucket_suffix.hex}"
|
|
368
362
|
force_destroy = true
|
|
369
363
|
|
|
@@ -409,11 +403,16 @@ resource "aws_s3_bucket_acl" "distribution_logs_acl" {
|
|
|
409
403
|
depends_on = [aws_s3_bucket_ownership_controls.distribution_logs_ownership]
|
|
410
404
|
}
|
|
411
405
|
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
# Deliver the distribution log bucket's server access logs to CloudWatch Logs.
|
|
407
|
+
resource "aws_cloudwatch_log_delivery_source" "distribution_logs" {
|
|
408
|
+
name = "\${local.access_logs_name_prefix}-distribution-logs-\${random_id.bucket_suffix.hex}"
|
|
409
|
+
log_type = "S3_SERVER_ACCESS_LOGS"
|
|
410
|
+
resource_arn = aws_s3_bucket.distribution_logs.arn
|
|
411
|
+
}
|
|
414
412
|
|
|
415
|
-
|
|
416
|
-
|
|
413
|
+
resource "aws_cloudwatch_log_delivery" "distribution_logs" {
|
|
414
|
+
delivery_source_name = aws_cloudwatch_log_delivery_source.distribution_logs.name
|
|
415
|
+
delivery_destination_arn = aws_cloudwatch_log_delivery_destination.access_logs.arn
|
|
417
416
|
}
|
|
418
417
|
|
|
419
418
|
resource "aws_s3_bucket_policy" "distribution_logs_ssl_policy" {
|
|
@@ -568,6 +567,7 @@ resource "aws_cloudfront_distribution" "website" {
|
|
|
568
567
|
is_ipv6_enabled = true
|
|
569
568
|
default_root_object = "index.html"
|
|
570
569
|
web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn
|
|
570
|
+
aliases = var.custom_domain_names
|
|
571
571
|
|
|
572
572
|
logging_config {
|
|
573
573
|
include_cookies = false
|
|
@@ -614,7 +614,10 @@ resource "aws_cloudfront_distribution" "website" {
|
|
|
614
614
|
}
|
|
615
615
|
|
|
616
616
|
viewer_certificate {
|
|
617
|
-
cloudfront_default_certificate =
|
|
617
|
+
cloudfront_default_certificate = var.acm_certificate_arn == null
|
|
618
|
+
acm_certificate_arn = var.acm_certificate_arn
|
|
619
|
+
ssl_support_method = var.acm_certificate_arn == null ? null : "sni-only"
|
|
620
|
+
minimum_protocol_version = var.acm_certificate_arn == null ? "TLSv1" : "TLSv1.2_2021"
|
|
618
621
|
}
|
|
619
622
|
|
|
620
623
|
tags = {
|
|
@@ -1004,7 +1007,10 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
1004
1007
|
"!**/node_modules",
|
|
1005
1008
|
"!**/.nx",
|
|
1006
1009
|
"!**/.venv",
|
|
1007
|
-
"!**/*.css"
|
|
1010
|
+
"!**/*.css",
|
|
1011
|
+
"!**/*.gen.*",
|
|
1012
|
+
"!**/generated/**",
|
|
1013
|
+
"!**/tsconfig*.json"
|
|
1008
1014
|
]
|
|
1009
1015
|
}
|
|
1010
1016
|
}
|
|
@@ -1208,10 +1214,27 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
1208
1214
|
"cwd": "{projectRoot}"
|
|
1209
1215
|
}
|
|
1210
1216
|
},
|
|
1217
|
+
"format": {
|
|
1218
|
+
"executor": "nx:run-commands",
|
|
1219
|
+
"cache": true,
|
|
1220
|
+
"inputs": ["biome"],
|
|
1221
|
+
"options": {
|
|
1222
|
+
"command": "biome format {projectRoot}"
|
|
1223
|
+
},
|
|
1224
|
+
"configurations": {
|
|
1225
|
+
"fix": {
|
|
1226
|
+
"command": "biome format --write {projectRoot}"
|
|
1227
|
+
},
|
|
1228
|
+
"skip-lint": {
|
|
1229
|
+
"command": "node -e \\"\\""
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1211
1233
|
"lint": {
|
|
1212
1234
|
"executor": "nx:run-commands",
|
|
1213
1235
|
"cache": true,
|
|
1214
1236
|
"inputs": ["biome"],
|
|
1237
|
+
"dependsOn": ["format"],
|
|
1215
1238
|
"options": {
|
|
1216
1239
|
"command": "biome lint {projectRoot}"
|
|
1217
1240
|
},
|
|
@@ -1535,6 +1558,7 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
1535
1558
|
CfnResource,
|
|
1536
1559
|
Duration,
|
|
1537
1560
|
Lazy,
|
|
1561
|
+
Names,
|
|
1538
1562
|
RemovalPolicy,
|
|
1539
1563
|
Stack,
|
|
1540
1564
|
} from 'aws-cdk-lib';
|
|
@@ -1543,8 +1567,10 @@ import {
|
|
|
1543
1567
|
HeadersFrameOption,
|
|
1544
1568
|
HeadersReferrerPolicy,
|
|
1545
1569
|
ResponseHeadersPolicy,
|
|
1570
|
+
SecurityPolicyProtocol,
|
|
1546
1571
|
ViewerProtocolPolicy,
|
|
1547
1572
|
} from 'aws-cdk-lib/aws-cloudfront';
|
|
1573
|
+
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
1548
1574
|
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
1549
1575
|
import {
|
|
1550
1576
|
BlockPublicAccess,
|
|
@@ -1561,6 +1587,14 @@ import {
|
|
|
1561
1587
|
import { Construct } from 'constructs';
|
|
1562
1588
|
import { RuntimeConfig } from './runtime-config.js';
|
|
1563
1589
|
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
1590
|
+
import {
|
|
1591
|
+
CfnDelivery,
|
|
1592
|
+
CfnDeliveryDestination,
|
|
1593
|
+
CfnDeliverySource,
|
|
1594
|
+
LogGroup,
|
|
1595
|
+
RetentionDays,
|
|
1596
|
+
} from 'aws-cdk-lib/aws-logs';
|
|
1597
|
+
import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
1564
1598
|
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
1565
1599
|
import { suppressRules } from './checkov.js';
|
|
1566
1600
|
|
|
@@ -1586,6 +1620,15 @@ const CONTENT_SECURITY_POLICY = [
|
|
|
1586
1620
|
export interface StaticWebsiteProps {
|
|
1587
1621
|
readonly websiteName: string;
|
|
1588
1622
|
readonly websiteFilePath: string;
|
|
1623
|
+
/**
|
|
1624
|
+
* Custom domain names for the CloudFront distribution. Requires \`certificate\`.
|
|
1625
|
+
*/
|
|
1626
|
+
readonly domainNames?: string[];
|
|
1627
|
+
/**
|
|
1628
|
+
* ACM certificate for the custom domain names. Must be in us-east-1.
|
|
1629
|
+
* When provided, viewers are required to use TLS 1.2 or later.
|
|
1630
|
+
*/
|
|
1631
|
+
readonly certificate?: ICertificate;
|
|
1589
1632
|
}
|
|
1590
1633
|
|
|
1591
1634
|
/**
|
|
@@ -1604,39 +1647,48 @@ export class StaticWebsite extends Construct {
|
|
|
1604
1647
|
constructor(
|
|
1605
1648
|
scope: Construct,
|
|
1606
1649
|
id: string,
|
|
1607
|
-
{
|
|
1650
|
+
{
|
|
1651
|
+
websiteFilePath,
|
|
1652
|
+
websiteName,
|
|
1653
|
+
domainNames,
|
|
1654
|
+
certificate,
|
|
1655
|
+
}: StaticWebsiteProps,
|
|
1608
1656
|
) {
|
|
1609
1657
|
super(scope, id);
|
|
1610
|
-
this.node.setContext(
|
|
1611
|
-
'@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy',
|
|
1612
|
-
true,
|
|
1613
|
-
);
|
|
1614
1658
|
|
|
1615
1659
|
const websiteKey = new Key(this, 'WebsiteKey', {
|
|
1616
1660
|
enableKeyRotation: true,
|
|
1617
1661
|
});
|
|
1618
1662
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1663
|
+
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
1664
|
+
const stack = Stack.of(this);
|
|
1665
|
+
websiteKey.addToResourcePolicy(
|
|
1666
|
+
new PolicyStatement({
|
|
1667
|
+
effect: Effect.ALLOW,
|
|
1668
|
+
principals: [
|
|
1669
|
+
new ServicePrincipal(\`logs.\${stack.region}.amazonaws.com\`),
|
|
1670
|
+
],
|
|
1671
|
+
actions: [
|
|
1672
|
+
'kms:Encrypt',
|
|
1673
|
+
'kms:Decrypt',
|
|
1674
|
+
'kms:ReEncrypt*',
|
|
1675
|
+
'kms:GenerateDataKey*',
|
|
1676
|
+
'kms:DescribeKey',
|
|
1677
|
+
],
|
|
1678
|
+
resources: ['*'],
|
|
1679
|
+
conditions: {
|
|
1680
|
+
ArnLike: {
|
|
1681
|
+
'kms:EncryptionContext:aws:logs:arn': \`arn:aws:logs:\${stack.region}:\${stack.account}:log-group:*\`,
|
|
1682
|
+
},
|
|
1683
|
+
},
|
|
1684
|
+
}),
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
const accessLogs = new LogGroup(this, 'AccessLogs', {
|
|
1688
|
+
retention: RetentionDays.ONE_YEAR,
|
|
1625
1689
|
encryptionKey: websiteKey,
|
|
1626
|
-
|
|
1627
|
-
publicReadAccess: false,
|
|
1628
|
-
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
1690
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
1629
1691
|
});
|
|
1630
|
-
suppressRules(
|
|
1631
|
-
accessLogsBucket,
|
|
1632
|
-
['CKV_AWS_21'],
|
|
1633
|
-
'Access log bucket does not need versioning enabled',
|
|
1634
|
-
);
|
|
1635
|
-
suppressRules(
|
|
1636
|
-
accessLogsBucket,
|
|
1637
|
-
['CKV_AWS_18'],
|
|
1638
|
-
'Access log bucket does not need an access log bucket',
|
|
1639
|
-
);
|
|
1640
1692
|
|
|
1641
1693
|
// S3 Bucket to hold website files
|
|
1642
1694
|
this.websiteBucket = new Bucket(this, 'WebsiteBucket', {
|
|
@@ -1649,13 +1701,23 @@ export class StaticWebsite extends Construct {
|
|
|
1649
1701
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
1650
1702
|
publicReadAccess: false,
|
|
1651
1703
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
1652
|
-
serverAccessLogsPrefix: 'website-access-logs',
|
|
1653
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
1654
1704
|
});
|
|
1705
|
+
suppressRules(
|
|
1706
|
+
this.websiteBucket,
|
|
1707
|
+
['CKV_AWS_18'],
|
|
1708
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
1709
|
+
);
|
|
1710
|
+
this.deliverAccessLogsToCloudWatch(
|
|
1711
|
+
'Website',
|
|
1712
|
+
this.websiteBucket,
|
|
1713
|
+
accessLogs,
|
|
1714
|
+
);
|
|
1655
1715
|
// Web ACL
|
|
1656
1716
|
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
1657
1717
|
|
|
1658
|
-
//
|
|
1718
|
+
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
1719
|
+
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
1720
|
+
// logs are delivered to CloudWatch Logs.
|
|
1659
1721
|
const logBucket = new Bucket(this, 'DistributionLogBucket', {
|
|
1660
1722
|
enforceSSL: true,
|
|
1661
1723
|
autoDeleteObjects: true,
|
|
@@ -1665,14 +1727,18 @@ export class StaticWebsite extends Construct {
|
|
|
1665
1727
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
1666
1728
|
publicReadAccess: false,
|
|
1667
1729
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
1668
|
-
serverAccessLogsPrefix: 'distribution-access-logs',
|
|
1669
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
1670
1730
|
});
|
|
1671
1731
|
suppressRules(
|
|
1672
1732
|
logBucket,
|
|
1673
1733
|
['CKV_AWS_21'],
|
|
1674
1734
|
'Distribution log bucket does not need versioning enabled',
|
|
1675
1735
|
);
|
|
1736
|
+
suppressRules(
|
|
1737
|
+
logBucket,
|
|
1738
|
+
['CKV_AWS_18'],
|
|
1739
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
1740
|
+
);
|
|
1741
|
+
this.deliverAccessLogsToCloudWatch('Distribution', logBucket, accessLogs);
|
|
1676
1742
|
|
|
1677
1743
|
// Security headers applied to all responses.
|
|
1678
1744
|
const responseHeadersPolicy = new ResponseHeadersPolicy(
|
|
@@ -1712,6 +1778,13 @@ export class StaticWebsite extends Construct {
|
|
|
1712
1778
|
webAclId: wafStack.wafArn,
|
|
1713
1779
|
enableLogging: true,
|
|
1714
1780
|
logBucket: logBucket,
|
|
1781
|
+
...(certificate
|
|
1782
|
+
? {
|
|
1783
|
+
certificate,
|
|
1784
|
+
domainNames,
|
|
1785
|
+
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
1786
|
+
}
|
|
1787
|
+
: {}),
|
|
1715
1788
|
defaultBehavior: {
|
|
1716
1789
|
origin: S3BucketOrigin.withOriginAccessControl(this.websiteBucket),
|
|
1717
1790
|
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
@@ -1732,12 +1805,14 @@ export class StaticWebsite extends Construct {
|
|
|
1732
1805
|
],
|
|
1733
1806
|
},
|
|
1734
1807
|
);
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1808
|
+
if (!certificate) {
|
|
1809
|
+
// See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
|
|
1810
|
+
suppressRules(
|
|
1811
|
+
this.cloudFrontDistribution,
|
|
1812
|
+
['CKV_AWS_174'],
|
|
1813
|
+
'Cloudfront default certificate does not use TLS 1.2',
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1741
1816
|
|
|
1742
1817
|
// Deploy Website
|
|
1743
1818
|
this.bucketDeployment = new BucketDeployment(this, 'WebsiteDeployment', {
|
|
@@ -1786,6 +1861,44 @@ export class StaticWebsite extends Construct {
|
|
|
1786
1861
|
value: this.websiteBucket.bucketName,
|
|
1787
1862
|
});
|
|
1788
1863
|
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Delivers a bucket's S3 server access logs to a CloudWatch log group using
|
|
1867
|
+
* CloudWatch Logs vended log delivery.
|
|
1868
|
+
*/
|
|
1869
|
+
private deliverAccessLogsToCloudWatch(
|
|
1870
|
+
id: string,
|
|
1871
|
+
bucket: IBucket,
|
|
1872
|
+
logGroup: LogGroup,
|
|
1873
|
+
) {
|
|
1874
|
+
const source: CfnDeliverySource = new CfnDeliverySource(
|
|
1875
|
+
this,
|
|
1876
|
+
\`\${id}AccessLogsSource\`,
|
|
1877
|
+
{
|
|
1878
|
+
name: Lazy.string({
|
|
1879
|
+
produce: () => Names.uniqueResourceName(source, { maxLength: 60 }),
|
|
1880
|
+
}),
|
|
1881
|
+
logType: 'S3_SERVER_ACCESS_LOGS',
|
|
1882
|
+
resourceArn: bucket.bucketArn,
|
|
1883
|
+
},
|
|
1884
|
+
);
|
|
1885
|
+
const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
|
|
1886
|
+
this,
|
|
1887
|
+
\`\${id}AccessLogsDestination\`,
|
|
1888
|
+
{
|
|
1889
|
+
name: Lazy.string({
|
|
1890
|
+
produce: () =>
|
|
1891
|
+
Names.uniqueResourceName(destination, { maxLength: 60 }),
|
|
1892
|
+
}),
|
|
1893
|
+
destinationResourceArn: logGroup.logGroupArn,
|
|
1894
|
+
},
|
|
1895
|
+
);
|
|
1896
|
+
const delivery = new CfnDelivery(this, \`\${id}AccessLogsDelivery\`, {
|
|
1897
|
+
deliverySourceName: source.name,
|
|
1898
|
+
deliveryDestinationArn: destination.attrArn,
|
|
1899
|
+
});
|
|
1900
|
+
delivery.addDependency(source);
|
|
1901
|
+
}
|
|
1789
1902
|
}
|
|
1790
1903
|
|
|
1791
1904
|
export class CloudfrontWebAcl extends Stack {
|
|
@@ -1908,9 +2021,13 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
1908
2021
|
"emitDeclarationOnly": false,
|
|
1909
2022
|
"module": "nodenext",
|
|
1910
2023
|
"moduleResolution": "nodenext",
|
|
1911
|
-
"types": [
|
|
2024
|
+
"types": [
|
|
2025
|
+
"node"
|
|
2026
|
+
]
|
|
1912
2027
|
},
|
|
1913
|
-
"include": [
|
|
2028
|
+
"include": [
|
|
2029
|
+
"src/**/*.ts"
|
|
2030
|
+
],
|
|
1914
2031
|
"references": [],
|
|
1915
2032
|
"exclude": [
|
|
1916
2033
|
"vite.config.ts",
|
|
@@ -2104,6 +2221,22 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2104
2221
|
"cwd": "{projectRoot}"
|
|
2105
2222
|
}
|
|
2106
2223
|
},
|
|
2224
|
+
"format": {
|
|
2225
|
+
"executor": "nx:run-commands",
|
|
2226
|
+
"cache": true,
|
|
2227
|
+
"inputs": ["biome"],
|
|
2228
|
+
"options": {
|
|
2229
|
+
"command": "biome format {projectRoot}"
|
|
2230
|
+
},
|
|
2231
|
+
"configurations": {
|
|
2232
|
+
"fix": {
|
|
2233
|
+
"command": "biome format --write {projectRoot}"
|
|
2234
|
+
},
|
|
2235
|
+
"skip-lint": {
|
|
2236
|
+
"command": "node -e \\"\\""
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
},
|
|
2107
2240
|
"lint": {
|
|
2108
2241
|
"executor": "nx:run-commands",
|
|
2109
2242
|
"cache": true,
|
|
@@ -2118,7 +2251,8 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2118
2251
|
"skip-lint": {
|
|
2119
2252
|
"command": "node -e \\"\\""
|
|
2120
2253
|
}
|
|
2121
|
-
}
|
|
2254
|
+
},
|
|
2255
|
+
"dependsOn": ["format"]
|
|
2122
2256
|
},
|
|
2123
2257
|
"load:runtime-config": {
|
|
2124
2258
|
"executor": "nx:run-commands",
|
|
@@ -2334,7 +2468,9 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2334
2468
|
"outDir": "../dist/test-app/tsc",
|
|
2335
2469
|
"tsBuildInfoFile": "../dist/test-app/tsc/tsconfig.lib.tsbuildinfo",
|
|
2336
2470
|
"jsx": "react-jsx",
|
|
2337
|
-
"lib": [
|
|
2471
|
+
"lib": [
|
|
2472
|
+
"DOM"
|
|
2473
|
+
],
|
|
2338
2474
|
"types": [
|
|
2339
2475
|
"node",
|
|
2340
2476
|
"@nx/react/typings/cssmodule.d.ts",
|
|
@@ -2361,7 +2497,12 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2361
2497
|
"vitest.config.ts",
|
|
2362
2498
|
"vitest.config.mts"
|
|
2363
2499
|
],
|
|
2364
|
-
"include": [
|
|
2500
|
+
"include": [
|
|
2501
|
+
"src/**/*.js",
|
|
2502
|
+
"src/**/*.jsx",
|
|
2503
|
+
"src/**/*.ts",
|
|
2504
|
+
"src/**/*.tsx"
|
|
2505
|
+
]
|
|
2365
2506
|
}
|
|
2366
2507
|
"
|
|
2367
2508
|
`;
|
|
@@ -2482,8 +2623,12 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2482
2623
|
"{
|
|
2483
2624
|
"compilerOptions": {
|
|
2484
2625
|
"paths": {
|
|
2485
|
-
":proj/test-app": [
|
|
2486
|
-
|
|
2626
|
+
":proj/test-app": [
|
|
2627
|
+
"./test-app/src/index.ts"
|
|
2628
|
+
],
|
|
2629
|
+
":proj/common-constructs": [
|
|
2630
|
+
"./packages/common/constructs/src/index.ts"
|
|
2631
|
+
]
|
|
2487
2632
|
},
|
|
2488
2633
|
"composite": true,
|
|
2489
2634
|
"rootDir": "."
|
|
@@ -2575,7 +2720,10 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2575
2720
|
"!**/node_modules",
|
|
2576
2721
|
"!**/.nx",
|
|
2577
2722
|
"!**/.venv",
|
|
2578
|
-
"!**/*.css"
|
|
2723
|
+
"!**/*.css",
|
|
2724
|
+
"!**/*.gen.*",
|
|
2725
|
+
"!**/generated/**",
|
|
2726
|
+
"!**/tsconfig*.json"
|
|
2579
2727
|
]
|
|
2580
2728
|
}
|
|
2581
2729
|
}
|
|
@@ -2784,10 +2932,27 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
2784
2932
|
"cwd": "{projectRoot}"
|
|
2785
2933
|
}
|
|
2786
2934
|
},
|
|
2935
|
+
"format": {
|
|
2936
|
+
"executor": "nx:run-commands",
|
|
2937
|
+
"cache": true,
|
|
2938
|
+
"inputs": ["biome"],
|
|
2939
|
+
"options": {
|
|
2940
|
+
"command": "biome format {projectRoot}"
|
|
2941
|
+
},
|
|
2942
|
+
"configurations": {
|
|
2943
|
+
"fix": {
|
|
2944
|
+
"command": "biome format --write {projectRoot}"
|
|
2945
|
+
},
|
|
2946
|
+
"skip-lint": {
|
|
2947
|
+
"command": "node -e \\"\\""
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
},
|
|
2787
2951
|
"lint": {
|
|
2788
2952
|
"executor": "nx:run-commands",
|
|
2789
2953
|
"cache": true,
|
|
2790
2954
|
"inputs": ["biome"],
|
|
2955
|
+
"dependsOn": ["format"],
|
|
2791
2956
|
"options": {
|
|
2792
2957
|
"command": "biome lint {projectRoot}"
|
|
2793
2958
|
},
|
|
@@ -3111,6 +3276,7 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
3111
3276
|
CfnResource,
|
|
3112
3277
|
Duration,
|
|
3113
3278
|
Lazy,
|
|
3279
|
+
Names,
|
|
3114
3280
|
RemovalPolicy,
|
|
3115
3281
|
Stack,
|
|
3116
3282
|
} from 'aws-cdk-lib';
|
|
@@ -3119,8 +3285,10 @@ import {
|
|
|
3119
3285
|
HeadersFrameOption,
|
|
3120
3286
|
HeadersReferrerPolicy,
|
|
3121
3287
|
ResponseHeadersPolicy,
|
|
3288
|
+
SecurityPolicyProtocol,
|
|
3122
3289
|
ViewerProtocolPolicy,
|
|
3123
3290
|
} from 'aws-cdk-lib/aws-cloudfront';
|
|
3291
|
+
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
3124
3292
|
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
3125
3293
|
import {
|
|
3126
3294
|
BlockPublicAccess,
|
|
@@ -3137,6 +3305,14 @@ import {
|
|
|
3137
3305
|
import { Construct } from 'constructs';
|
|
3138
3306
|
import { RuntimeConfig } from './runtime-config.js';
|
|
3139
3307
|
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
3308
|
+
import {
|
|
3309
|
+
CfnDelivery,
|
|
3310
|
+
CfnDeliveryDestination,
|
|
3311
|
+
CfnDeliverySource,
|
|
3312
|
+
LogGroup,
|
|
3313
|
+
RetentionDays,
|
|
3314
|
+
} from 'aws-cdk-lib/aws-logs';
|
|
3315
|
+
import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
3140
3316
|
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
3141
3317
|
import { suppressRules } from './checkov.js';
|
|
3142
3318
|
|
|
@@ -3162,6 +3338,15 @@ const CONTENT_SECURITY_POLICY = [
|
|
|
3162
3338
|
export interface StaticWebsiteProps {
|
|
3163
3339
|
readonly websiteName: string;
|
|
3164
3340
|
readonly websiteFilePath: string;
|
|
3341
|
+
/**
|
|
3342
|
+
* Custom domain names for the CloudFront distribution. Requires \`certificate\`.
|
|
3343
|
+
*/
|
|
3344
|
+
readonly domainNames?: string[];
|
|
3345
|
+
/**
|
|
3346
|
+
* ACM certificate for the custom domain names. Must be in us-east-1.
|
|
3347
|
+
* When provided, viewers are required to use TLS 1.2 or later.
|
|
3348
|
+
*/
|
|
3349
|
+
readonly certificate?: ICertificate;
|
|
3165
3350
|
}
|
|
3166
3351
|
|
|
3167
3352
|
/**
|
|
@@ -3180,39 +3365,48 @@ export class StaticWebsite extends Construct {
|
|
|
3180
3365
|
constructor(
|
|
3181
3366
|
scope: Construct,
|
|
3182
3367
|
id: string,
|
|
3183
|
-
{
|
|
3368
|
+
{
|
|
3369
|
+
websiteFilePath,
|
|
3370
|
+
websiteName,
|
|
3371
|
+
domainNames,
|
|
3372
|
+
certificate,
|
|
3373
|
+
}: StaticWebsiteProps,
|
|
3184
3374
|
) {
|
|
3185
3375
|
super(scope, id);
|
|
3186
|
-
this.node.setContext(
|
|
3187
|
-
'@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy',
|
|
3188
|
-
true,
|
|
3189
|
-
);
|
|
3190
3376
|
|
|
3191
3377
|
const websiteKey = new Key(this, 'WebsiteKey', {
|
|
3192
3378
|
enableKeyRotation: true,
|
|
3193
3379
|
});
|
|
3194
3380
|
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3381
|
+
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
3382
|
+
const stack = Stack.of(this);
|
|
3383
|
+
websiteKey.addToResourcePolicy(
|
|
3384
|
+
new PolicyStatement({
|
|
3385
|
+
effect: Effect.ALLOW,
|
|
3386
|
+
principals: [
|
|
3387
|
+
new ServicePrincipal(\`logs.\${stack.region}.amazonaws.com\`),
|
|
3388
|
+
],
|
|
3389
|
+
actions: [
|
|
3390
|
+
'kms:Encrypt',
|
|
3391
|
+
'kms:Decrypt',
|
|
3392
|
+
'kms:ReEncrypt*',
|
|
3393
|
+
'kms:GenerateDataKey*',
|
|
3394
|
+
'kms:DescribeKey',
|
|
3395
|
+
],
|
|
3396
|
+
resources: ['*'],
|
|
3397
|
+
conditions: {
|
|
3398
|
+
ArnLike: {
|
|
3399
|
+
'kms:EncryptionContext:aws:logs:arn': \`arn:aws:logs:\${stack.region}:\${stack.account}:log-group:*\`,
|
|
3400
|
+
},
|
|
3401
|
+
},
|
|
3402
|
+
}),
|
|
3403
|
+
);
|
|
3404
|
+
|
|
3405
|
+
const accessLogs = new LogGroup(this, 'AccessLogs', {
|
|
3406
|
+
retention: RetentionDays.ONE_YEAR,
|
|
3201
3407
|
encryptionKey: websiteKey,
|
|
3202
|
-
|
|
3203
|
-
publicReadAccess: false,
|
|
3204
|
-
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
3408
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
3205
3409
|
});
|
|
3206
|
-
suppressRules(
|
|
3207
|
-
accessLogsBucket,
|
|
3208
|
-
['CKV_AWS_21'],
|
|
3209
|
-
'Access log bucket does not need versioning enabled',
|
|
3210
|
-
);
|
|
3211
|
-
suppressRules(
|
|
3212
|
-
accessLogsBucket,
|
|
3213
|
-
['CKV_AWS_18'],
|
|
3214
|
-
'Access log bucket does not need an access log bucket',
|
|
3215
|
-
);
|
|
3216
3410
|
|
|
3217
3411
|
// S3 Bucket to hold website files
|
|
3218
3412
|
this.websiteBucket = new Bucket(this, 'WebsiteBucket', {
|
|
@@ -3225,13 +3419,23 @@ export class StaticWebsite extends Construct {
|
|
|
3225
3419
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
3226
3420
|
publicReadAccess: false,
|
|
3227
3421
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
3228
|
-
serverAccessLogsPrefix: 'website-access-logs',
|
|
3229
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
3230
3422
|
});
|
|
3423
|
+
suppressRules(
|
|
3424
|
+
this.websiteBucket,
|
|
3425
|
+
['CKV_AWS_18'],
|
|
3426
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
3427
|
+
);
|
|
3428
|
+
this.deliverAccessLogsToCloudWatch(
|
|
3429
|
+
'Website',
|
|
3430
|
+
this.websiteBucket,
|
|
3431
|
+
accessLogs,
|
|
3432
|
+
);
|
|
3231
3433
|
// Web ACL
|
|
3232
3434
|
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
3233
3435
|
|
|
3234
|
-
//
|
|
3436
|
+
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
3437
|
+
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
3438
|
+
// logs are delivered to CloudWatch Logs.
|
|
3235
3439
|
const logBucket = new Bucket(this, 'DistributionLogBucket', {
|
|
3236
3440
|
enforceSSL: true,
|
|
3237
3441
|
autoDeleteObjects: true,
|
|
@@ -3241,14 +3445,18 @@ export class StaticWebsite extends Construct {
|
|
|
3241
3445
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
3242
3446
|
publicReadAccess: false,
|
|
3243
3447
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
3244
|
-
serverAccessLogsPrefix: 'distribution-access-logs',
|
|
3245
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
3246
3448
|
});
|
|
3247
3449
|
suppressRules(
|
|
3248
3450
|
logBucket,
|
|
3249
3451
|
['CKV_AWS_21'],
|
|
3250
3452
|
'Distribution log bucket does not need versioning enabled',
|
|
3251
3453
|
);
|
|
3454
|
+
suppressRules(
|
|
3455
|
+
logBucket,
|
|
3456
|
+
['CKV_AWS_18'],
|
|
3457
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
3458
|
+
);
|
|
3459
|
+
this.deliverAccessLogsToCloudWatch('Distribution', logBucket, accessLogs);
|
|
3252
3460
|
|
|
3253
3461
|
// Security headers applied to all responses.
|
|
3254
3462
|
const responseHeadersPolicy = new ResponseHeadersPolicy(
|
|
@@ -3288,6 +3496,13 @@ export class StaticWebsite extends Construct {
|
|
|
3288
3496
|
webAclId: wafStack.wafArn,
|
|
3289
3497
|
enableLogging: true,
|
|
3290
3498
|
logBucket: logBucket,
|
|
3499
|
+
...(certificate
|
|
3500
|
+
? {
|
|
3501
|
+
certificate,
|
|
3502
|
+
domainNames,
|
|
3503
|
+
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
3504
|
+
}
|
|
3505
|
+
: {}),
|
|
3291
3506
|
defaultBehavior: {
|
|
3292
3507
|
origin: S3BucketOrigin.withOriginAccessControl(this.websiteBucket),
|
|
3293
3508
|
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
@@ -3308,12 +3523,14 @@ export class StaticWebsite extends Construct {
|
|
|
3308
3523
|
],
|
|
3309
3524
|
},
|
|
3310
3525
|
);
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3526
|
+
if (!certificate) {
|
|
3527
|
+
// See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
|
|
3528
|
+
suppressRules(
|
|
3529
|
+
this.cloudFrontDistribution,
|
|
3530
|
+
['CKV_AWS_174'],
|
|
3531
|
+
'Cloudfront default certificate does not use TLS 1.2',
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3317
3534
|
|
|
3318
3535
|
// Deploy Website
|
|
3319
3536
|
this.bucketDeployment = new BucketDeployment(this, 'WebsiteDeployment', {
|
|
@@ -3362,6 +3579,44 @@ export class StaticWebsite extends Construct {
|
|
|
3362
3579
|
value: this.websiteBucket.bucketName,
|
|
3363
3580
|
});
|
|
3364
3581
|
}
|
|
3582
|
+
|
|
3583
|
+
/**
|
|
3584
|
+
* Delivers a bucket's S3 server access logs to a CloudWatch log group using
|
|
3585
|
+
* CloudWatch Logs vended log delivery.
|
|
3586
|
+
*/
|
|
3587
|
+
private deliverAccessLogsToCloudWatch(
|
|
3588
|
+
id: string,
|
|
3589
|
+
bucket: IBucket,
|
|
3590
|
+
logGroup: LogGroup,
|
|
3591
|
+
) {
|
|
3592
|
+
const source: CfnDeliverySource = new CfnDeliverySource(
|
|
3593
|
+
this,
|
|
3594
|
+
\`\${id}AccessLogsSource\`,
|
|
3595
|
+
{
|
|
3596
|
+
name: Lazy.string({
|
|
3597
|
+
produce: () => Names.uniqueResourceName(source, { maxLength: 60 }),
|
|
3598
|
+
}),
|
|
3599
|
+
logType: 'S3_SERVER_ACCESS_LOGS',
|
|
3600
|
+
resourceArn: bucket.bucketArn,
|
|
3601
|
+
},
|
|
3602
|
+
);
|
|
3603
|
+
const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
|
|
3604
|
+
this,
|
|
3605
|
+
\`\${id}AccessLogsDestination\`,
|
|
3606
|
+
{
|
|
3607
|
+
name: Lazy.string({
|
|
3608
|
+
produce: () =>
|
|
3609
|
+
Names.uniqueResourceName(destination, { maxLength: 60 }),
|
|
3610
|
+
}),
|
|
3611
|
+
destinationResourceArn: logGroup.logGroupArn,
|
|
3612
|
+
},
|
|
3613
|
+
);
|
|
3614
|
+
const delivery = new CfnDelivery(this, \`\${id}AccessLogsDelivery\`, {
|
|
3615
|
+
deliverySourceName: source.name,
|
|
3616
|
+
deliveryDestinationArn: destination.attrArn,
|
|
3617
|
+
});
|
|
3618
|
+
delivery.addDependency(source);
|
|
3619
|
+
}
|
|
3365
3620
|
}
|
|
3366
3621
|
|
|
3367
3622
|
export class CloudfrontWebAcl extends Stack {
|
|
@@ -3484,9 +3739,13 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
3484
3739
|
"emitDeclarationOnly": false,
|
|
3485
3740
|
"module": "nodenext",
|
|
3486
3741
|
"moduleResolution": "nodenext",
|
|
3487
|
-
"types": [
|
|
3742
|
+
"types": [
|
|
3743
|
+
"node"
|
|
3744
|
+
]
|
|
3488
3745
|
},
|
|
3489
|
-
"include": [
|
|
3746
|
+
"include": [
|
|
3747
|
+
"src/**/*.ts"
|
|
3748
|
+
],
|
|
3490
3749
|
"references": [],
|
|
3491
3750
|
"exclude": [
|
|
3492
3751
|
"vite.config.ts",
|
|
@@ -3680,6 +3939,22 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
3680
3939
|
"cwd": "{projectRoot}"
|
|
3681
3940
|
}
|
|
3682
3941
|
},
|
|
3942
|
+
"format": {
|
|
3943
|
+
"executor": "nx:run-commands",
|
|
3944
|
+
"cache": true,
|
|
3945
|
+
"inputs": ["biome"],
|
|
3946
|
+
"options": {
|
|
3947
|
+
"command": "biome format {projectRoot}"
|
|
3948
|
+
},
|
|
3949
|
+
"configurations": {
|
|
3950
|
+
"fix": {
|
|
3951
|
+
"command": "biome format --write {projectRoot}"
|
|
3952
|
+
},
|
|
3953
|
+
"skip-lint": {
|
|
3954
|
+
"command": "node -e \\"\\""
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
},
|
|
3683
3958
|
"lint": {
|
|
3684
3959
|
"executor": "nx:run-commands",
|
|
3685
3960
|
"cache": true,
|
|
@@ -3694,7 +3969,8 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
3694
3969
|
"skip-lint": {
|
|
3695
3970
|
"command": "node -e \\"\\""
|
|
3696
3971
|
}
|
|
3697
|
-
}
|
|
3972
|
+
},
|
|
3973
|
+
"dependsOn": ["format"]
|
|
3698
3974
|
},
|
|
3699
3975
|
"load:runtime-config": {
|
|
3700
3976
|
"executor": "nx:run-commands",
|
|
@@ -4094,7 +4370,9 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
4094
4370
|
"outDir": "../dist/test-app/tsc",
|
|
4095
4371
|
"tsBuildInfoFile": "../dist/test-app/tsc/tsconfig.lib.tsbuildinfo",
|
|
4096
4372
|
"jsx": "react-jsx",
|
|
4097
|
-
"lib": [
|
|
4373
|
+
"lib": [
|
|
4374
|
+
"DOM"
|
|
4375
|
+
],
|
|
4098
4376
|
"types": [
|
|
4099
4377
|
"node",
|
|
4100
4378
|
"@nx/react/typings/cssmodule.d.ts",
|
|
@@ -4121,7 +4399,12 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
4121
4399
|
"vitest.config.ts",
|
|
4122
4400
|
"vitest.config.mts"
|
|
4123
4401
|
],
|
|
4124
|
-
"include": [
|
|
4402
|
+
"include": [
|
|
4403
|
+
"src/**/*.js",
|
|
4404
|
+
"src/**/*.jsx",
|
|
4405
|
+
"src/**/*.ts",
|
|
4406
|
+
"src/**/*.tsx"
|
|
4407
|
+
]
|
|
4125
4408
|
}
|
|
4126
4409
|
"
|
|
4127
4410
|
`;
|
|
@@ -4251,8 +4534,12 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
4251
4534
|
"{
|
|
4252
4535
|
"compilerOptions": {
|
|
4253
4536
|
"paths": {
|
|
4254
|
-
":proj/test-app": [
|
|
4255
|
-
|
|
4537
|
+
":proj/test-app": [
|
|
4538
|
+
"./test-app/src/index.ts"
|
|
4539
|
+
],
|
|
4540
|
+
":proj/common-constructs": [
|
|
4541
|
+
"./packages/common/constructs/src/index.ts"
|
|
4542
|
+
]
|
|
4256
4543
|
},
|
|
4257
4544
|
"composite": true,
|
|
4258
4545
|
"rootDir": "."
|
|
@@ -4614,6 +4901,7 @@ exports[`react-website generator > should generate shared constructs > common/co
|
|
|
4614
4901
|
CfnResource,
|
|
4615
4902
|
Duration,
|
|
4616
4903
|
Lazy,
|
|
4904
|
+
Names,
|
|
4617
4905
|
RemovalPolicy,
|
|
4618
4906
|
Stack,
|
|
4619
4907
|
} from 'aws-cdk-lib';
|
|
@@ -4622,8 +4910,10 @@ import {
|
|
|
4622
4910
|
HeadersFrameOption,
|
|
4623
4911
|
HeadersReferrerPolicy,
|
|
4624
4912
|
ResponseHeadersPolicy,
|
|
4913
|
+
SecurityPolicyProtocol,
|
|
4625
4914
|
ViewerProtocolPolicy,
|
|
4626
4915
|
} from 'aws-cdk-lib/aws-cloudfront';
|
|
4916
|
+
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
4627
4917
|
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
4628
4918
|
import {
|
|
4629
4919
|
BlockPublicAccess,
|
|
@@ -4640,6 +4930,14 @@ import {
|
|
|
4640
4930
|
import { Construct } from 'constructs';
|
|
4641
4931
|
import { RuntimeConfig } from './runtime-config.js';
|
|
4642
4932
|
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
4933
|
+
import {
|
|
4934
|
+
CfnDelivery,
|
|
4935
|
+
CfnDeliveryDestination,
|
|
4936
|
+
CfnDeliverySource,
|
|
4937
|
+
LogGroup,
|
|
4938
|
+
RetentionDays,
|
|
4939
|
+
} from 'aws-cdk-lib/aws-logs';
|
|
4940
|
+
import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
4643
4941
|
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
4644
4942
|
import { suppressRules } from './checkov.js';
|
|
4645
4943
|
|
|
@@ -4665,6 +4963,15 @@ const CONTENT_SECURITY_POLICY = [
|
|
|
4665
4963
|
export interface StaticWebsiteProps {
|
|
4666
4964
|
readonly websiteName: string;
|
|
4667
4965
|
readonly websiteFilePath: string;
|
|
4966
|
+
/**
|
|
4967
|
+
* Custom domain names for the CloudFront distribution. Requires \`certificate\`.
|
|
4968
|
+
*/
|
|
4969
|
+
readonly domainNames?: string[];
|
|
4970
|
+
/**
|
|
4971
|
+
* ACM certificate for the custom domain names. Must be in us-east-1.
|
|
4972
|
+
* When provided, viewers are required to use TLS 1.2 or later.
|
|
4973
|
+
*/
|
|
4974
|
+
readonly certificate?: ICertificate;
|
|
4668
4975
|
}
|
|
4669
4976
|
|
|
4670
4977
|
/**
|
|
@@ -4683,39 +4990,48 @@ export class StaticWebsite extends Construct {
|
|
|
4683
4990
|
constructor(
|
|
4684
4991
|
scope: Construct,
|
|
4685
4992
|
id: string,
|
|
4686
|
-
{
|
|
4993
|
+
{
|
|
4994
|
+
websiteFilePath,
|
|
4995
|
+
websiteName,
|
|
4996
|
+
domainNames,
|
|
4997
|
+
certificate,
|
|
4998
|
+
}: StaticWebsiteProps,
|
|
4687
4999
|
) {
|
|
4688
5000
|
super(scope, id);
|
|
4689
|
-
this.node.setContext(
|
|
4690
|
-
'@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy',
|
|
4691
|
-
true,
|
|
4692
|
-
);
|
|
4693
5001
|
|
|
4694
5002
|
const websiteKey = new Key(this, 'WebsiteKey', {
|
|
4695
5003
|
enableKeyRotation: true,
|
|
4696
5004
|
});
|
|
4697
5005
|
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
5006
|
+
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
5007
|
+
const stack = Stack.of(this);
|
|
5008
|
+
websiteKey.addToResourcePolicy(
|
|
5009
|
+
new PolicyStatement({
|
|
5010
|
+
effect: Effect.ALLOW,
|
|
5011
|
+
principals: [
|
|
5012
|
+
new ServicePrincipal(\`logs.\${stack.region}.amazonaws.com\`),
|
|
5013
|
+
],
|
|
5014
|
+
actions: [
|
|
5015
|
+
'kms:Encrypt',
|
|
5016
|
+
'kms:Decrypt',
|
|
5017
|
+
'kms:ReEncrypt*',
|
|
5018
|
+
'kms:GenerateDataKey*',
|
|
5019
|
+
'kms:DescribeKey',
|
|
5020
|
+
],
|
|
5021
|
+
resources: ['*'],
|
|
5022
|
+
conditions: {
|
|
5023
|
+
ArnLike: {
|
|
5024
|
+
'kms:EncryptionContext:aws:logs:arn': \`arn:aws:logs:\${stack.region}:\${stack.account}:log-group:*\`,
|
|
5025
|
+
},
|
|
5026
|
+
},
|
|
5027
|
+
}),
|
|
5028
|
+
);
|
|
5029
|
+
|
|
5030
|
+
const accessLogs = new LogGroup(this, 'AccessLogs', {
|
|
5031
|
+
retention: RetentionDays.ONE_YEAR,
|
|
4704
5032
|
encryptionKey: websiteKey,
|
|
4705
|
-
|
|
4706
|
-
publicReadAccess: false,
|
|
4707
|
-
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
5033
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
4708
5034
|
});
|
|
4709
|
-
suppressRules(
|
|
4710
|
-
accessLogsBucket,
|
|
4711
|
-
['CKV_AWS_21'],
|
|
4712
|
-
'Access log bucket does not need versioning enabled',
|
|
4713
|
-
);
|
|
4714
|
-
suppressRules(
|
|
4715
|
-
accessLogsBucket,
|
|
4716
|
-
['CKV_AWS_18'],
|
|
4717
|
-
'Access log bucket does not need an access log bucket',
|
|
4718
|
-
);
|
|
4719
5035
|
|
|
4720
5036
|
// S3 Bucket to hold website files
|
|
4721
5037
|
this.websiteBucket = new Bucket(this, 'WebsiteBucket', {
|
|
@@ -4728,13 +5044,23 @@ export class StaticWebsite extends Construct {
|
|
|
4728
5044
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
4729
5045
|
publicReadAccess: false,
|
|
4730
5046
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
4731
|
-
serverAccessLogsPrefix: 'website-access-logs',
|
|
4732
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
4733
5047
|
});
|
|
5048
|
+
suppressRules(
|
|
5049
|
+
this.websiteBucket,
|
|
5050
|
+
['CKV_AWS_18'],
|
|
5051
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
5052
|
+
);
|
|
5053
|
+
this.deliverAccessLogsToCloudWatch(
|
|
5054
|
+
'Website',
|
|
5055
|
+
this.websiteBucket,
|
|
5056
|
+
accessLogs,
|
|
5057
|
+
);
|
|
4734
5058
|
// Web ACL
|
|
4735
5059
|
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
4736
5060
|
|
|
4737
|
-
//
|
|
5061
|
+
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
5062
|
+
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
5063
|
+
// logs are delivered to CloudWatch Logs.
|
|
4738
5064
|
const logBucket = new Bucket(this, 'DistributionLogBucket', {
|
|
4739
5065
|
enforceSSL: true,
|
|
4740
5066
|
autoDeleteObjects: true,
|
|
@@ -4744,14 +5070,18 @@ export class StaticWebsite extends Construct {
|
|
|
4744
5070
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
4745
5071
|
publicReadAccess: false,
|
|
4746
5072
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
4747
|
-
serverAccessLogsPrefix: 'distribution-access-logs',
|
|
4748
|
-
serverAccessLogsBucket: accessLogsBucket,
|
|
4749
5073
|
});
|
|
4750
5074
|
suppressRules(
|
|
4751
5075
|
logBucket,
|
|
4752
5076
|
['CKV_AWS_21'],
|
|
4753
5077
|
'Distribution log bucket does not need versioning enabled',
|
|
4754
5078
|
);
|
|
5079
|
+
suppressRules(
|
|
5080
|
+
logBucket,
|
|
5081
|
+
['CKV_AWS_18'],
|
|
5082
|
+
'Server access logs are delivered to CloudWatch Logs',
|
|
5083
|
+
);
|
|
5084
|
+
this.deliverAccessLogsToCloudWatch('Distribution', logBucket, accessLogs);
|
|
4755
5085
|
|
|
4756
5086
|
// Security headers applied to all responses.
|
|
4757
5087
|
const responseHeadersPolicy = new ResponseHeadersPolicy(
|
|
@@ -4791,6 +5121,13 @@ export class StaticWebsite extends Construct {
|
|
|
4791
5121
|
webAclId: wafStack.wafArn,
|
|
4792
5122
|
enableLogging: true,
|
|
4793
5123
|
logBucket: logBucket,
|
|
5124
|
+
...(certificate
|
|
5125
|
+
? {
|
|
5126
|
+
certificate,
|
|
5127
|
+
domainNames,
|
|
5128
|
+
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
5129
|
+
}
|
|
5130
|
+
: {}),
|
|
4794
5131
|
defaultBehavior: {
|
|
4795
5132
|
origin: S3BucketOrigin.withOriginAccessControl(this.websiteBucket),
|
|
4796
5133
|
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
@@ -4811,12 +5148,14 @@ export class StaticWebsite extends Construct {
|
|
|
4811
5148
|
],
|
|
4812
5149
|
},
|
|
4813
5150
|
);
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
5151
|
+
if (!certificate) {
|
|
5152
|
+
// See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
|
|
5153
|
+
suppressRules(
|
|
5154
|
+
this.cloudFrontDistribution,
|
|
5155
|
+
['CKV_AWS_174'],
|
|
5156
|
+
'Cloudfront default certificate does not use TLS 1.2',
|
|
5157
|
+
);
|
|
5158
|
+
}
|
|
4820
5159
|
|
|
4821
5160
|
// Deploy Website
|
|
4822
5161
|
this.bucketDeployment = new BucketDeployment(this, 'WebsiteDeployment', {
|
|
@@ -4865,6 +5204,44 @@ export class StaticWebsite extends Construct {
|
|
|
4865
5204
|
value: this.websiteBucket.bucketName,
|
|
4866
5205
|
});
|
|
4867
5206
|
}
|
|
5207
|
+
|
|
5208
|
+
/**
|
|
5209
|
+
* Delivers a bucket's S3 server access logs to a CloudWatch log group using
|
|
5210
|
+
* CloudWatch Logs vended log delivery.
|
|
5211
|
+
*/
|
|
5212
|
+
private deliverAccessLogsToCloudWatch(
|
|
5213
|
+
id: string,
|
|
5214
|
+
bucket: IBucket,
|
|
5215
|
+
logGroup: LogGroup,
|
|
5216
|
+
) {
|
|
5217
|
+
const source: CfnDeliverySource = new CfnDeliverySource(
|
|
5218
|
+
this,
|
|
5219
|
+
\`\${id}AccessLogsSource\`,
|
|
5220
|
+
{
|
|
5221
|
+
name: Lazy.string({
|
|
5222
|
+
produce: () => Names.uniqueResourceName(source, { maxLength: 60 }),
|
|
5223
|
+
}),
|
|
5224
|
+
logType: 'S3_SERVER_ACCESS_LOGS',
|
|
5225
|
+
resourceArn: bucket.bucketArn,
|
|
5226
|
+
},
|
|
5227
|
+
);
|
|
5228
|
+
const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
|
|
5229
|
+
this,
|
|
5230
|
+
\`\${id}AccessLogsDestination\`,
|
|
5231
|
+
{
|
|
5232
|
+
name: Lazy.string({
|
|
5233
|
+
produce: () =>
|
|
5234
|
+
Names.uniqueResourceName(destination, { maxLength: 60 }),
|
|
5235
|
+
}),
|
|
5236
|
+
destinationResourceArn: logGroup.logGroupArn,
|
|
5237
|
+
},
|
|
5238
|
+
);
|
|
5239
|
+
const delivery = new CfnDelivery(this, \`\${id}AccessLogsDelivery\`, {
|
|
5240
|
+
deliverySourceName: source.name,
|
|
5241
|
+
deliveryDestinationArn: destination.attrArn,
|
|
5242
|
+
});
|
|
5243
|
+
delivery.addDependency(source);
|
|
5244
|
+
}
|
|
4868
5245
|
}
|
|
4869
5246
|
|
|
4870
5247
|
export class CloudfrontWebAcl extends Stack {
|