@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.
@@ -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
- # Access Logs Bucket
232
- resource "aws_s3_bucket" "access_logs" {
233
- #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access logs bucket
234
- #checkov:skip=CKV_AWS_144:Cross-region replication not required for access logs
235
- #checkov:skip=CKV2_AWS_62:Event notifications not required for access logs bucket
236
- #checkov:skip=CKV_AWS_21:Versioning disabled for access logs to reduce storage costs
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
- resource "aws_s3_bucket_logging" "website_logging" {
354
- bucket = aws_s3_bucket.website.id
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
- target_bucket = aws_s3_bucket.access_logs.id
357
- target_prefix = "website-access-logs/"
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
- resource "aws_s3_bucket_logging" "distribution_logs_logging" {
413
- bucket = aws_s3_bucket.distribution_logs.id
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
- target_bucket = aws_s3_bucket.access_logs.id
416
- target_prefix = "distribution-access-logs/"
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 = true
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
- { websiteFilePath, websiteName }: StaticWebsiteProps,
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
- const accessLogsBucket = new Bucket(this, 'AccessLogsBucket', {
1620
- versioned: false,
1621
- enforceSSL: true,
1622
- autoDeleteObjects: true,
1623
- removalPolicy: RemovalPolicy.DESTROY,
1624
- encryption: BucketEncryption.KMS,
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
- objectOwnership: ObjectOwnership.OBJECT_WRITER,
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
- // Cloudfront Distribution
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
- // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
1736
- suppressRules(
1737
- this.cloudFrontDistribution,
1738
- ['CKV_AWS_174'],
1739
- 'Cloudfront default certificate does not use TLS 1.2',
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": ["node"]
2024
+ "types": [
2025
+ "node"
2026
+ ]
1912
2027
  },
1913
- "include": ["src/**/*.ts"],
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": ["DOM"],
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": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
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": ["./test-app/src/index.ts"],
2486
- ":proj/common-constructs": ["./packages/common/constructs/src/index.ts"]
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
- { websiteFilePath, websiteName }: StaticWebsiteProps,
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
- const accessLogsBucket = new Bucket(this, 'AccessLogsBucket', {
3196
- versioned: false,
3197
- enforceSSL: true,
3198
- autoDeleteObjects: true,
3199
- removalPolicy: RemovalPolicy.DESTROY,
3200
- encryption: BucketEncryption.KMS,
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
- objectOwnership: ObjectOwnership.OBJECT_WRITER,
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
- // Cloudfront Distribution
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
- // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
3312
- suppressRules(
3313
- this.cloudFrontDistribution,
3314
- ['CKV_AWS_174'],
3315
- 'Cloudfront default certificate does not use TLS 1.2',
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": ["node"]
3742
+ "types": [
3743
+ "node"
3744
+ ]
3488
3745
  },
3489
- "include": ["src/**/*.ts"],
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": ["DOM"],
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": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
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": ["./test-app/src/index.ts"],
4255
- ":proj/common-constructs": ["./packages/common/constructs/src/index.ts"]
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
- { websiteFilePath, websiteName }: StaticWebsiteProps,
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
- const accessLogsBucket = new Bucket(this, 'AccessLogsBucket', {
4699
- versioned: false,
4700
- enforceSSL: true,
4701
- autoDeleteObjects: true,
4702
- removalPolicy: RemovalPolicy.DESTROY,
4703
- encryption: BucketEncryption.KMS,
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
- objectOwnership: ObjectOwnership.OBJECT_WRITER,
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
- // Cloudfront Distribution
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
- // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
4815
- suppressRules(
4816
- this.cloudFrontDistribution,
4817
- ['CKV_AWS_174'],
4818
- 'Cloudfront default certificate does not use TLS 1.2',
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 {