@jaypie/constructs 1.2.31 → 1.2.33

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.
@@ -7,7 +7,7 @@ export { JaypieCertificate, JaypieCertificateProps } from "./JaypieCertificate";
7
7
  export { JaypieDatadogBucket, JaypieDatadogBucketProps, } from "./JaypieDatadogBucket";
8
8
  export { JaypieDatadogForwarder, JaypieDatadogForwarderProps, } from "./JaypieDatadogForwarder";
9
9
  export { JaypieDatadogSecret } from "./JaypieDatadogSecret";
10
- export { JaypieDistribution, JaypieDistributionProps, SecurityHeadersOverrides, } from "./JaypieDistribution";
10
+ export { JaypieDistribution, JaypieDistributionProps, JaypieWafConfig, SecurityHeadersOverrides, } from "./JaypieDistribution";
11
11
  export { JaypieDnsRecord, JaypieDnsRecordProps } from "./JaypieDnsRecord";
12
12
  export { JaypieDynamoDb, JaypieDynamoDbProps } from "./JaypieDynamoDb";
13
13
  export type { IndexDefinition } from "@jaypie/fabric";
@@ -5,8 +5,38 @@ import * as lambda from "aws-cdk-lib/aws-lambda";
5
5
  import * as route53 from "aws-cdk-lib/aws-route53";
6
6
  import * as s3 from "aws-cdk-lib/aws-s3";
7
7
  import { LambdaDestination } from "aws-cdk-lib/aws-s3-notifications";
8
+ import * as wafv2 from "aws-cdk-lib/aws-wafv2";
8
9
  import { Construct } from "constructs";
9
10
  import { HostConfig } from "./helpers";
11
+ export interface JaypieWafConfig {
12
+ /**
13
+ * Whether WAF is enabled
14
+ * @default true
15
+ */
16
+ enabled?: boolean;
17
+ /**
18
+ * WAF logging bucket.
19
+ * - true/undefined: create a logging bucket with Datadog forwarding (default)
20
+ * - false: disable WAF logging
21
+ * - IBucket: use an existing bucket (must have "aws-waf-logs-" prefix)
22
+ * @default true
23
+ */
24
+ logBucket?: boolean | s3.IBucket;
25
+ /**
26
+ * Managed rule group names to apply
27
+ * @default ["AWSManagedRulesCommonRuleSet", "AWSManagedRulesKnownBadInputsRuleSet"]
28
+ */
29
+ managedRules?: string[];
30
+ /**
31
+ * Rate limit per IP per 5-minute window
32
+ * @default 2000
33
+ */
34
+ rateLimitPerIp?: number;
35
+ /**
36
+ * Use an existing WebACL ARN instead of creating one
37
+ */
38
+ webAclArn?: string;
39
+ }
10
40
  export interface SecurityHeadersOverrides {
11
41
  contentSecurityPolicy?: string;
12
42
  frameOption?: cloudfront.HeadersFrameOption;
@@ -99,6 +129,14 @@ export interface JaypieDistributionProps extends Omit<cloudfront.DistributionPro
99
129
  * @default CDK.ROLE.HOSTING
100
130
  */
101
131
  roleTag?: string;
132
+ /**
133
+ * WAF WebACL configuration for the CloudFront distribution.
134
+ * - true/undefined: create and attach a WebACL with sensible defaults
135
+ * - false: disable WAF
136
+ * - JaypieWafConfig: customize WAF behavior
137
+ * @default true
138
+ */
139
+ waf?: boolean | JaypieWafConfig;
102
140
  /**
103
141
  * The hosted zone for DNS records
104
142
  * @default CDK_ENV_API_HOSTED_ZONE || CDK_ENV_HOSTED_ZONE
@@ -116,11 +154,14 @@ export declare class JaypieDistribution extends Construct implements cloudfront.
116
154
  readonly host?: string;
117
155
  readonly logBucket?: s3.IBucket;
118
156
  readonly responseHeadersPolicy?: cloudfront.IResponseHeadersPolicy;
157
+ readonly wafLogBucket?: s3.IBucket;
158
+ readonly webAcl?: wafv2.CfnWebACL;
119
159
  constructor(scope: Construct, id: string, props: JaypieDistributionProps);
120
160
  private isIOrigin;
121
161
  private isIFunctionUrl;
122
162
  private isIFunction;
123
163
  private isExportNameObject;
164
+ private resolveWafConfig;
124
165
  private resolveLogBucket;
125
166
  get env(): {
126
167
  account: string;
@@ -10,6 +10,7 @@ export interface JaypieEnvSecretProps {
10
10
  export?: string;
11
11
  generateSecretString?: secretsmanager.SecretStringGenerator;
12
12
  provider?: boolean;
13
+ removalPolicy?: boolean | RemovalPolicy;
13
14
  roleTag?: string;
14
15
  vendorTag?: string;
15
16
  value?: string;
@@ -1,4 +1,5 @@
1
1
  import { IBucket } from "aws-cdk-lib/aws-s3";
2
+ import { CfnAnalyzer } from "aws-cdk-lib/aws-accessanalyzer";
2
3
  import { Trail } from "aws-cdk-lib/aws-cloudtrail";
3
4
  import { Construct } from "constructs";
4
5
  export interface JaypieOrganizationTrailProps {
@@ -26,11 +27,26 @@ export interface JaypieOrganizationTrailProps {
26
27
  * Optional project tag value
27
28
  */
28
29
  project?: string;
30
+ /**
31
+ * Whether to enable IAM Access Analyzer (organization-level)
32
+ * @default true
33
+ */
34
+ enableAccessAnalyzer?: boolean;
29
35
  /**
30
36
  * Whether to enable file validation for the trail
31
- * @default false
37
+ * @default true
32
38
  */
33
39
  enableFileValidation?: boolean;
40
+ /**
41
+ * Whether to enable Lambda data events in CloudTrail
42
+ * @default true
43
+ */
44
+ enableLambdaDataEvents?: boolean;
45
+ /**
46
+ * Whether to enable S3 data events in CloudTrail
47
+ * @default false (opt-in due to potential high volume/cost)
48
+ */
49
+ enableS3DataEvents?: boolean;
34
50
  /**
35
51
  * Number of days before logs expire
36
52
  * @default 365
@@ -53,6 +69,7 @@ export interface JaypieOrganizationTrailProps {
53
69
  enableDatadogNotifications?: boolean;
54
70
  }
55
71
  export declare class JaypieOrganizationTrail extends Construct {
72
+ readonly analyzer?: CfnAnalyzer;
56
73
  readonly bucket: IBucket;
57
74
  readonly trail: Trail;
58
75
  /**
@@ -7,7 +7,7 @@ export { JaypieCertificate, JaypieCertificateProps } from "./JaypieCertificate";
7
7
  export { JaypieDatadogBucket, JaypieDatadogBucketProps, } from "./JaypieDatadogBucket";
8
8
  export { JaypieDatadogForwarder, JaypieDatadogForwarderProps, } from "./JaypieDatadogForwarder";
9
9
  export { JaypieDatadogSecret } from "./JaypieDatadogSecret";
10
- export { JaypieDistribution, JaypieDistributionProps, SecurityHeadersOverrides, } from "./JaypieDistribution";
10
+ export { JaypieDistribution, JaypieDistributionProps, JaypieWafConfig, SecurityHeadersOverrides, } from "./JaypieDistribution";
11
11
  export { JaypieDnsRecord, JaypieDnsRecordProps } from "./JaypieDnsRecord";
12
12
  export { JaypieDynamoDb, JaypieDynamoDbProps } from "./JaypieDynamoDb";
13
13
  export type { IndexDefinition } from "@jaypie/fabric";
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as cdk from 'aws-cdk-lib';
2
- import { Tags, Stack, Fn, CfnOutput, SecretValue, Duration, RemovalPolicy, CfnStack } from 'aws-cdk-lib';
2
+ import { Tags, Stack, Fn, CfnOutput, SecretValue, RemovalPolicy, Duration, CfnStack } from 'aws-cdk-lib';
3
3
  import * as s3 from 'aws-cdk-lib/aws-s3';
4
4
  import { Bucket, StorageClass, BucketAccessControl, EventType } from 'aws-cdk-lib/aws-s3';
5
5
  import { Construct } from 'constructs';
@@ -24,10 +24,12 @@ import { Rule, RuleTargetInput } from 'aws-cdk-lib/aws-events';
24
24
  import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
25
25
  import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
26
26
  import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
27
+ import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
27
28
  import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
28
29
  import { generateIndexName, DEFAULT_SORT_KEY } from '@jaypie/fabric';
29
30
  import { Nextjs } from 'cdk-nextjs-standalone';
30
31
  import * as path from 'path';
32
+ import { CfnAnalyzer } from 'aws-cdk-lib/aws-accessanalyzer';
31
33
  import { Trail, ReadWriteType } from 'aws-cdk-lib/aws-cloudtrail';
32
34
  import { CfnPermissionSet, CfnAssignment } from 'aws-cdk-lib/aws-sso';
33
35
  import { CfnApplication } from 'aws-cdk-lib/aws-sam';
@@ -911,7 +913,7 @@ class JaypieEnvSecret extends Construct {
911
913
  process.env[idOrEnvKey] !== "";
912
914
  const id = treatAsEnvKey ? `EnvSecret_${idOrEnvKey}` : idOrEnvKey;
913
915
  super(scope, id);
914
- const { consumer = checkEnvIsConsumer$1(), envKey: envKeyProp, export: exportParam, generateSecretString, provider = checkEnvIsProvider$1(), roleTag, vendorTag, value, } = props || {};
916
+ const { consumer = checkEnvIsConsumer$1(), envKey: envKeyProp, export: exportParam, generateSecretString, provider = checkEnvIsProvider$1(), removalPolicy, roleTag, vendorTag, value, } = props || {};
915
917
  const envKey = treatAsEnvKey ? idOrEnvKey : envKeyProp;
916
918
  this._envKey = envKey;
917
919
  let exportName;
@@ -938,6 +940,14 @@ class JaypieEnvSecret extends Construct {
938
940
  : undefined,
939
941
  };
940
942
  this._secret = new secretsmanager.Secret(this, id, secretProps);
943
+ if (removalPolicy !== undefined) {
944
+ const policy = typeof removalPolicy === "boolean"
945
+ ? removalPolicy
946
+ ? RemovalPolicy.RETAIN
947
+ : RemovalPolicy.DESTROY
948
+ : removalPolicy;
949
+ this._secret.applyRemovalPolicy(policy);
950
+ }
941
951
  if (roleTag) {
942
952
  Tags.of(this._secret).add(CDK$2.TAG.ROLE, roleTag);
943
953
  }
@@ -2332,10 +2342,15 @@ class JaypieDatadogSecret extends JaypieEnvSecret {
2332
2342
  }
2333
2343
  }
2334
2344
 
2345
+ const DEFAULT_RATE_LIMIT = 2000;
2346
+ const DEFAULT_MANAGED_RULES = [
2347
+ "AWSManagedRulesCommonRuleSet",
2348
+ "AWSManagedRulesKnownBadInputsRuleSet",
2349
+ ];
2335
2350
  class JaypieDistribution extends Construct {
2336
2351
  constructor(scope, id, props) {
2337
2352
  super(scope, id);
2338
- const { certificate: certificateProp = true, defaultBehavior: propsDefaultBehavior, destination: destinationProp = true, handler, host: propsHost, logBucket: logBucketProp, originReadTimeout = Duration.seconds(CDK$2.DURATION.CLOUDFRONT_API), responseHeadersPolicy: responseHeadersPolicyProp, roleTag = CDK$2.ROLE.API, securityHeaders: securityHeadersProp, streaming = false, zone: propsZone, ...distributionProps } = props;
2353
+ const { certificate: certificateProp = true, defaultBehavior: propsDefaultBehavior, destination: destinationProp = true, handler, host: propsHost, logBucket: logBucketProp, originReadTimeout = Duration.seconds(CDK$2.DURATION.CLOUDFRONT_API), responseHeadersPolicy: responseHeadersPolicyProp, roleTag = CDK$2.ROLE.API, securityHeaders: securityHeadersProp, streaming = false, waf: wafProp = true, zone: propsZone, ...distributionProps } = props;
2339
2354
  // Validate environment variables
2340
2355
  if (process.env.CDK_ENV_API_SUBDOMAIN &&
2341
2356
  !isValidSubdomain(process.env.CDK_ENV_API_SUBDOMAIN)) {
@@ -2578,6 +2593,119 @@ class JaypieDistribution extends Construct {
2578
2593
  this.distributionDomainName = this.distribution.distributionDomainName;
2579
2594
  this.distributionId = this.distribution.distributionId;
2580
2595
  this.domainName = this.distribution.domainName;
2596
+ // Create and attach WAF WebACL
2597
+ let resolvedWebAclArn;
2598
+ const wafConfig = this.resolveWafConfig(wafProp);
2599
+ if (wafConfig) {
2600
+ if (wafConfig.webAclArn) {
2601
+ // Use existing WebACL
2602
+ resolvedWebAclArn = wafConfig.webAclArn;
2603
+ this.distribution.attachWebAclId(wafConfig.webAclArn);
2604
+ }
2605
+ else {
2606
+ // Create new WebACL
2607
+ const { managedRules = DEFAULT_MANAGED_RULES, rateLimitPerIp = DEFAULT_RATE_LIMIT, } = wafConfig;
2608
+ let priority = 0;
2609
+ const rules = [];
2610
+ // Add managed rule groups
2611
+ for (const ruleName of managedRules) {
2612
+ rules.push({
2613
+ name: ruleName,
2614
+ priority: priority++,
2615
+ overrideAction: { none: {} },
2616
+ statement: {
2617
+ managedRuleGroupStatement: {
2618
+ name: ruleName,
2619
+ vendorName: "AWS",
2620
+ },
2621
+ },
2622
+ visibilityConfig: {
2623
+ cloudWatchMetricsEnabled: true,
2624
+ metricName: ruleName,
2625
+ sampledRequestsEnabled: true,
2626
+ },
2627
+ });
2628
+ }
2629
+ // Add rate-based rule
2630
+ rules.push({
2631
+ name: "RateLimitPerIp",
2632
+ priority: priority++,
2633
+ action: { block: {} },
2634
+ statement: {
2635
+ rateBasedStatement: {
2636
+ aggregateKeyType: "IP",
2637
+ limit: rateLimitPerIp,
2638
+ },
2639
+ },
2640
+ visibilityConfig: {
2641
+ cloudWatchMetricsEnabled: true,
2642
+ metricName: "RateLimitPerIp",
2643
+ sampledRequestsEnabled: true,
2644
+ },
2645
+ });
2646
+ const webAcl = new wafv2.CfnWebACL(this, "WebAcl", {
2647
+ defaultAction: { allow: {} },
2648
+ name: constructEnvName("WebAcl"),
2649
+ rules,
2650
+ scope: "CLOUDFRONT",
2651
+ visibilityConfig: {
2652
+ cloudWatchMetricsEnabled: true,
2653
+ metricName: constructEnvName("WebAcl"),
2654
+ sampledRequestsEnabled: true,
2655
+ },
2656
+ });
2657
+ this.webAcl = webAcl;
2658
+ resolvedWebAclArn = webAcl.attrArn;
2659
+ this.distribution.attachWebAclId(webAcl.attrArn);
2660
+ Tags.of(webAcl).add(CDK$2.TAG.ROLE, roleTag);
2661
+ }
2662
+ }
2663
+ // Create WAF logging
2664
+ if (resolvedWebAclArn && wafConfig) {
2665
+ const { logBucket: wafLogBucketProp = true } = wafConfig;
2666
+ let wafLogBucket;
2667
+ if (wafLogBucketProp === true) {
2668
+ // Create inline WAF logging bucket with Datadog forwarding
2669
+ const createdBucket = new s3.Bucket(this, constructEnvName("WafLogBucket"), {
2670
+ autoDeleteObjects: true,
2671
+ bucketName: `aws-waf-logs-${constructEnvName("waf").toLowerCase()}`,
2672
+ lifecycleRules: [
2673
+ {
2674
+ expiration: Duration.days(90),
2675
+ transitions: [
2676
+ {
2677
+ storageClass: s3.StorageClass.INFREQUENT_ACCESS,
2678
+ transitionAfter: Duration.days(30),
2679
+ },
2680
+ ],
2681
+ },
2682
+ ],
2683
+ objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
2684
+ removalPolicy: RemovalPolicy.DESTROY,
2685
+ });
2686
+ Tags.of(createdBucket).add(CDK$2.TAG.ROLE, CDK$2.ROLE.MONITORING);
2687
+ // Add Datadog forwarder notification
2688
+ if (destinationProp !== false) {
2689
+ const lambdaDestination = destinationProp === true
2690
+ ? new LambdaDestination(resolveDatadogForwarderFunction(this))
2691
+ : destinationProp;
2692
+ createdBucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination);
2693
+ }
2694
+ wafLogBucket = createdBucket;
2695
+ }
2696
+ else if (typeof wafLogBucketProp === "object") {
2697
+ // Use provided IBucket
2698
+ wafLogBucket = wafLogBucketProp;
2699
+ }
2700
+ // wafLogBucketProp === false → no logging
2701
+ if (wafLogBucket) {
2702
+ this.wafLogBucket = wafLogBucket;
2703
+ new wafv2.CfnLoggingConfiguration(this, "WafLoggingConfig", {
2704
+ logDestinationConfigs: [wafLogBucket.bucketArn],
2705
+ resourceArn: resolvedWebAclArn,
2706
+ });
2707
+ }
2708
+ }
2581
2709
  // Create DNS records if we have host and zone
2582
2710
  if (host && hostedZone) {
2583
2711
  const aRecord = new route53.ARecord(this, "AliasRecord", {
@@ -2624,6 +2752,15 @@ class JaypieDistribution extends Construct {
2624
2752
  "exportName" in value &&
2625
2753
  typeof value.exportName === "string");
2626
2754
  }
2755
+ resolveWafConfig(wafProp) {
2756
+ if (wafProp === false)
2757
+ return undefined;
2758
+ if (wafProp === true)
2759
+ return {};
2760
+ if (wafProp.enabled === false)
2761
+ return undefined;
2762
+ return wafProp;
2763
+ }
2627
2764
  resolveLogBucket(logBucketProp) {
2628
2765
  // true = use account logging bucket
2629
2766
  if (logBucketProp === true) {
@@ -3463,7 +3600,7 @@ class JaypieOrganizationTrail extends Construct {
3463
3600
  // Resolve options with defaults
3464
3601
  const { bucketName = process.env.PROJECT_NONCE
3465
3602
  ? `organization-cloudtrail-${process.env.PROJECT_NONCE}`
3466
- : "organization-cloudtrail", enableDatadogNotifications = true, enableFileValidation = false, expirationDays = 365, glacierTransitionDays = 180, infrequentAccessTransitionDays = 30, project, service = CDK$2.SERVICE.INFRASTRUCTURE, trailName = process.env.PROJECT_NONCE
3603
+ : "organization-cloudtrail", enableAccessAnalyzer = true, enableDatadogNotifications = true, enableFileValidation = true, enableLambdaDataEvents = true, enableS3DataEvents = false, expirationDays = 365, glacierTransitionDays = 180, infrequentAccessTransitionDays = 30, project, service = CDK$2.SERVICE.INFRASTRUCTURE, trailName = process.env.PROJECT_NONCE
3467
3604
  ? `organization-cloudtrail-${process.env.PROJECT_NONCE}`
3468
3605
  : "organization-cloudtrail", } = props;
3469
3606
  // Create the S3 bucket for CloudTrail logs
@@ -3523,12 +3660,34 @@ class JaypieOrganizationTrail extends Construct {
3523
3660
  managementEvents: ReadWriteType.ALL,
3524
3661
  trailName,
3525
3662
  });
3663
+ // Add data event selectors
3664
+ if (enableLambdaDataEvents) {
3665
+ this.trail.logAllLambdaDataEvents();
3666
+ }
3667
+ if (enableS3DataEvents) {
3668
+ this.trail.logAllS3DataEvents();
3669
+ }
3526
3670
  // Add tags to trail
3527
3671
  cdk.Tags.of(this.trail).add(CDK$2.TAG.SERVICE, service);
3528
3672
  cdk.Tags.of(this.trail).add(CDK$2.TAG.ROLE, CDK$2.ROLE.MONITORING);
3529
3673
  if (project) {
3530
3674
  cdk.Tags.of(this.trail).add(CDK$2.TAG.PROJECT, project);
3531
3675
  }
3676
+ // Create IAM Access Analyzer
3677
+ if (enableAccessAnalyzer) {
3678
+ const analyzerName = process.env.PROJECT_NONCE
3679
+ ? `organization-access-analyzer-${process.env.PROJECT_NONCE}`
3680
+ : "organization-access-analyzer";
3681
+ this.analyzer = new CfnAnalyzer(this, "AccessAnalyzer", {
3682
+ analyzerName,
3683
+ type: "ORGANIZATION",
3684
+ });
3685
+ cdk.Tags.of(this.analyzer).add(CDK$2.TAG.SERVICE, service);
3686
+ cdk.Tags.of(this.analyzer).add(CDK$2.TAG.ROLE, CDK$2.ROLE.MONITORING);
3687
+ if (project) {
3688
+ cdk.Tags.of(this.analyzer).add(CDK$2.TAG.PROJECT, project);
3689
+ }
3690
+ }
3532
3691
  }
3533
3692
  }
3534
3693