@composurecdk/wafv2 0.0.0-stage → 0.13.0

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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -2
  3. package/dist/commonjs/alarm-config.d.ts +60 -0
  4. package/dist/commonjs/alarm-config.d.ts.map +1 -0
  5. package/dist/commonjs/alarm-config.js +3 -0
  6. package/dist/commonjs/alarm-config.js.map +1 -0
  7. package/dist/commonjs/alarm-defaults.d.ts +34 -0
  8. package/dist/commonjs/alarm-defaults.d.ts.map +1 -0
  9. package/dist/commonjs/alarm-defaults.js +37 -0
  10. package/dist/commonjs/alarm-defaults.js.map +1 -0
  11. package/dist/commonjs/alarms.d.ts +18 -0
  12. package/dist/commonjs/alarms.d.ts.map +1 -0
  13. package/dist/commonjs/alarms.js +94 -0
  14. package/dist/commonjs/alarms.js.map +1 -0
  15. package/dist/commonjs/association.d.ts +21 -0
  16. package/dist/commonjs/association.d.ts.map +1 -0
  17. package/dist/commonjs/association.js +61 -0
  18. package/dist/commonjs/association.js.map +1 -0
  19. package/dist/commonjs/custom-response.d.ts +52 -0
  20. package/dist/commonjs/custom-response.d.ts.map +1 -0
  21. package/dist/commonjs/custom-response.js +137 -0
  22. package/dist/commonjs/custom-response.js.map +1 -0
  23. package/dist/commonjs/defaults.d.ts +89 -0
  24. package/dist/commonjs/defaults.d.ts.map +1 -0
  25. package/dist/commonjs/defaults.js +90 -0
  26. package/dist/commonjs/defaults.js.map +1 -0
  27. package/dist/commonjs/index.d.ts +30 -0
  28. package/dist/commonjs/index.d.ts.map +1 -0
  29. package/dist/commonjs/index.js +35 -0
  30. package/dist/commonjs/index.js.map +1 -0
  31. package/dist/commonjs/logging-filter.d.ts +27 -0
  32. package/dist/commonjs/logging-filter.d.ts.map +1 -0
  33. package/dist/commonjs/logging-filter.js +17 -0
  34. package/dist/commonjs/logging-filter.js.map +1 -0
  35. package/dist/commonjs/logging.d.ts +59 -0
  36. package/dist/commonjs/logging.d.ts.map +1 -0
  37. package/dist/commonjs/logging.js +91 -0
  38. package/dist/commonjs/logging.js.map +1 -0
  39. package/dist/commonjs/metrics.d.ts +58 -0
  40. package/dist/commonjs/metrics.d.ts.map +1 -0
  41. package/dist/commonjs/metrics.js +67 -0
  42. package/dist/commonjs/metrics.js.map +1 -0
  43. package/dist/commonjs/names.d.ts +46 -0
  44. package/dist/commonjs/names.d.ts.map +1 -0
  45. package/dist/commonjs/names.js +140 -0
  46. package/dist/commonjs/names.js.map +1 -0
  47. package/dist/commonjs/package.json +3 -0
  48. package/dist/commonjs/rule-builder.d.ts +123 -0
  49. package/dist/commonjs/rule-builder.d.ts.map +1 -0
  50. package/dist/commonjs/rule-builder.js +163 -0
  51. package/dist/commonjs/rule-builder.js.map +1 -0
  52. package/dist/commonjs/scope.d.ts +15 -0
  53. package/dist/commonjs/scope.d.ts.map +1 -0
  54. package/dist/commonjs/scope.js +35 -0
  55. package/dist/commonjs/scope.js.map +1 -0
  56. package/dist/commonjs/web-acl-builder.d.ts +152 -0
  57. package/dist/commonjs/web-acl-builder.d.ts.map +1 -0
  58. package/dist/commonjs/web-acl-builder.js +205 -0
  59. package/dist/commonjs/web-acl-builder.js.map +1 -0
  60. package/dist/esm/alarm-config.d.ts +60 -0
  61. package/dist/esm/alarm-config.d.ts.map +1 -0
  62. package/dist/esm/alarm-config.js +2 -0
  63. package/dist/esm/alarm-config.js.map +1 -0
  64. package/dist/esm/alarm-defaults.d.ts +34 -0
  65. package/dist/esm/alarm-defaults.d.ts.map +1 -0
  66. package/dist/esm/alarm-defaults.js +34 -0
  67. package/dist/esm/alarm-defaults.js.map +1 -0
  68. package/dist/esm/alarms.d.ts +18 -0
  69. package/dist/esm/alarms.d.ts.map +1 -0
  70. package/dist/esm/alarms.js +90 -0
  71. package/dist/esm/alarms.js.map +1 -0
  72. package/dist/esm/association.d.ts +21 -0
  73. package/dist/esm/association.d.ts.map +1 -0
  74. package/dist/esm/association.js +58 -0
  75. package/dist/esm/association.js.map +1 -0
  76. package/dist/esm/custom-response.d.ts +52 -0
  77. package/dist/esm/custom-response.d.ts.map +1 -0
  78. package/dist/esm/custom-response.js +133 -0
  79. package/dist/esm/custom-response.js.map +1 -0
  80. package/dist/esm/defaults.d.ts +89 -0
  81. package/dist/esm/defaults.d.ts.map +1 -0
  82. package/dist/esm/defaults.js +87 -0
  83. package/dist/esm/defaults.js.map +1 -0
  84. package/dist/esm/index.d.ts +30 -0
  85. package/dist/esm/index.d.ts.map +1 -0
  86. package/dist/esm/index.js +23 -0
  87. package/dist/esm/index.js.map +1 -0
  88. package/dist/esm/logging-filter.d.ts +27 -0
  89. package/dist/esm/logging-filter.d.ts.map +1 -0
  90. package/dist/esm/logging-filter.js +14 -0
  91. package/dist/esm/logging-filter.js.map +1 -0
  92. package/dist/esm/logging.d.ts +59 -0
  93. package/dist/esm/logging.d.ts.map +1 -0
  94. package/dist/esm/logging.js +88 -0
  95. package/dist/esm/logging.js.map +1 -0
  96. package/dist/esm/metrics.d.ts +58 -0
  97. package/dist/esm/metrics.d.ts.map +1 -0
  98. package/dist/esm/metrics.js +63 -0
  99. package/dist/esm/metrics.js.map +1 -0
  100. package/dist/esm/names.d.ts +46 -0
  101. package/dist/esm/names.d.ts.map +1 -0
  102. package/dist/esm/names.js +132 -0
  103. package/dist/esm/names.js.map +1 -0
  104. package/dist/esm/package.json +3 -0
  105. package/dist/esm/rule-builder.d.ts +123 -0
  106. package/dist/esm/rule-builder.d.ts.map +1 -0
  107. package/dist/esm/rule-builder.js +160 -0
  108. package/dist/esm/rule-builder.js.map +1 -0
  109. package/dist/esm/scope.d.ts +15 -0
  110. package/dist/esm/scope.d.ts.map +1 -0
  111. package/dist/esm/scope.js +32 -0
  112. package/dist/esm/scope.js.map +1 -0
  113. package/dist/esm/web-acl-builder.d.ts +152 -0
  114. package/dist/esm/web-acl-builder.d.ts.map +1 -0
  115. package/dist/esm/web-acl-builder.js +202 -0
  116. package/dist/esm/web-acl-builder.js.map +1 -0
  117. package/package.json +78 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Duffett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,241 @@
1
- # Temporary Holding Version
1
+ # @composurecdk/wafv2
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AWS WAF web ACL builder for [ComposureCDK](../../README.md).
4
+
5
+ This package provides a fluent builder for [AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) web ACLs, their rules and their associations. CDK has no L2 for WAF, so it wraps the [`CfnWebACL`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnWebACL.html) and [`CfnWebACLAssociation`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnWebACLAssociation.html) L1s directly, and handles the parts of them that only fail at deploy time.
6
+
7
+ ```ts
8
+ import { compose, ref } from "@composurecdk/core";
9
+ import { createRestApiBuilder, type RestApiBuilderResult } from "@composurecdk/apigateway";
10
+ import { createWebAclBuilder } from "@composurecdk/wafv2";
11
+
12
+ compose(
13
+ {
14
+ api: createRestApiBuilder(),
15
+ waf: createWebAclBuilder()
16
+ .associateWith(ref("api", (r: RestApiBuilderResult) => r.api.deploymentStage))
17
+ .addRule("BlockAdmin", (rule) =>
18
+ rule
19
+ .statement({
20
+ byteMatchStatement: {
21
+ fieldToMatch: { uriPath: {} },
22
+ positionalConstraint: "STARTS_WITH",
23
+ searchString: "/admin",
24
+ textTransformations: [{ priority: 0, type: "NONE" }],
25
+ },
26
+ })
27
+ .respond(404, { json: { message: "Not found" } }),
28
+ ),
29
+ },
30
+ { api: [], waf: ["api"] },
31
+ );
32
+ ```
33
+
34
+ The result is `{ webAcl, associations, logGroup?, logging?, metrics, alarms }`:
35
+
36
+ | Field | Type | Description |
37
+ | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------- |
38
+ | `webAcl` | `CfnWebACL` | The web ACL. Its ARN is `webAcl.attrArn`. |
39
+ | `associations` | `CfnWebACLAssociation[]` | One per `associateWith` call, in that order. Always empty for a CLOUDFRONT web ACL. |
40
+ | `logGroup` | `LogGroup \| undefined` | The `aws-waf-logs-` log group WAF writes to. Absent when logging is off or goes to your own destination. |
41
+ | `logging` | `CfnLoggingConfiguration \| undefined` | The logging configuration. Absent when logging is off. |
42
+ | `metrics` | `WebAclMetrics` | Metrics for the web ACL, its default action and each rule — see [Metrics](#metrics). |
43
+ | `alarms` | `Record<string, Alarm>` | Recommended and custom alarms, keyed by alarm key — see [Alarms](#alarms). |
44
+
45
+ ## Defaults
46
+
47
+ | Setting | Default | Rationale |
48
+ | ------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `scope` | `REGIONAL` | API Gateway, Application Load Balancers, AppSync, Cognito, App Runner and Verified Access are regional. Set `.scope("CLOUDFRONT")` for a distribution — see [CloudFront](#cloudfront). |
50
+ | default action | Allow | A web ACL in front of an application usually lets through what no rule blocks. Use `.defaultAction("block")` for an allow-list. |
51
+ | CloudWatch metrics | on, ACL and rules | Free, and the only way to see what the rules are doing. Not configurable. |
52
+ | metric name | from name or path | The web ACL's `name` when set; otherwise a name derived from its construct path, so two web ACLs never share a metric name. Each rule's metric name is its own name. |
53
+ | `sampledRequests` | `false` | A sampled request keeps every header, including credentials, and [logging redaction does not apply to it](https://docs.aws.amazon.com/waf/latest/developerguide/waf-data-protection-and-logging.html). Turning it on without a `dataProtectionConfig` adds the warning `@composurecdk/wafv2:sampling-unredacted`. |
54
+ | logging | on, non-Allow only | WAF logs go to a created `aws-waf-logs-` log group, keeping the requests it blocks, counts or challenges and redacting `authorization` and `cookie`. See [Logging](#logging). |
55
+ | managed rule groups | none | Every managed group blocks some legitimate traffic until it is tuned. None is added for you. |
56
+
57
+ `WEB_ACL_DEFAULTS` and `WEB_ACL_VISIBILITY_DEFAULTS` export these for inspection. Everything else in `CfnWebACLProps` — `name`, `description`, `dataProtectionConfig`, `captchaConfig`, `challengeConfig`, `tokenDomains`, `associationConfig` and the rest — is set with its own builder method and passed through.
58
+
59
+ ## Rules
60
+
61
+ `addRule(name, configure)` adds a rule. `configure` receives a rule builder:
62
+
63
+ | Method | What it does |
64
+ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
65
+ | `statement(raw)` | The rule's statement, as CloudFormation's `StatementProperty`. Exactly one per rule. |
66
+ | `block()` · `allow()` · `count()` · `captcha()` · `challenge()` | The action. `block()` is the default. |
67
+ | `respond(status, body?, headers?)` | Block with a custom response — see [Custom responses](#custom-responses). |
68
+ | `labels(...labels)` | Labels to add to matching requests, for later rules to match on. |
69
+ | `priority(n)` | Overrides the priority — see [Priorities](#priorities). |
70
+ | `metricName(name)` | The rule's CloudWatch metric name. Defaults to the rule's name. |
71
+ | `sampledRequests(enabled)` | Overrides the web ACL's sampling setting for this rule. |
72
+ | `captchaConfig(…)` · `challengeConfig(…)` | Passed through to the rule. |
73
+
74
+ Rule names are checked when the rule is added: 1–128 characters of `[A-Za-z0-9_-]`, and unique within the web ACL.
75
+
76
+ ### Raw statements and CloudFormation casing
77
+
78
+ `statement()` takes CDK's `CfnWebACL.StatementProperty`. Most of it is camelCase, as usual, but CDK types several fields as `any` and passes them through untouched, so **they need CloudFormation's PascalCase**. A wrong case is accepted at synth and fails at deploy.
79
+
80
+ ```ts
81
+ rule.statement({
82
+ sizeConstraintStatement: {
83
+ fieldToMatch: { singleHeader: { Name: "x-api-key" } }, // Name, not name
84
+ comparisonOperator: "GT",
85
+ size: 0,
86
+ textTransformations: [{ priority: 0, type: "NONE" }],
87
+ },
88
+ });
89
+ ```
90
+
91
+ The `any`-typed fields are `FieldToMatch`'s `singleHeader`, `singleQueryArgument`, `allQueryArguments`, `queryString`, `uriPath` and `method`, and a rate-based statement's custom keys `ip`, `forwardedIp`, `httpMethod` and `asn`. Fields that are empty objects (`uriPath: {}`) are unaffected.
92
+
93
+ ### Priorities
94
+
95
+ WAF evaluates rules from the lowest priority up. Each rule's priority defaults to ten times its position in `addRule` order — 0, 10, 20, … — so rules run in the order they are declared, and there is room to insert one with an explicit `.priority(n)` without renumbering the rest. Two rules with the same priority throw at build, naming both. Changing a priority updates the web ACL in place.
96
+
97
+ ### Rule groups
98
+
99
+ A rule whose statement is a `managedRuleGroupStatement` or a `ruleGroupReferenceStatement` evaluates a group of rules that keep their own actions, so CloudFormation gives it an `overrideAction` instead of an `action`. The builder emits `overrideAction: { None: {} }`, or `{ Count: {} }` when the rule calls `.count()` — the way to try a group out before it blocks anything. Any other action throws.
100
+
101
+ ```ts
102
+ createWebAclBuilder().addRule("CommonRuleSet", (rule) =>
103
+ rule
104
+ .statement({
105
+ managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesCommonRuleSet" },
106
+ })
107
+ .count(),
108
+ );
109
+ ```
110
+
111
+ ### Other checks at synth
112
+
113
+ - A rate-based rule cannot Allow, and a web ACL can have at most ten of them.
114
+ - Labels are 1–128 characters of `[A-Za-z0-9_-]` per colon-separated part, with at most five namespaces, and none of WAF's reserved words (`awswaf`, `aws`, `waf`, `rulegroup`, `webacl`, `regexpatternset`, `ipset`, `managed`). WAF adds the `awswaf:<account>:webacl:<name>:` prefix itself.
115
+ - Metric names follow the rule for names, and `All` and `Default_Action` are reserved.
116
+
117
+ The builder does not compute [web ACL capacity units](https://docs.aws.amazon.com/waf/latest/developerguide/aws-waf-capacity-units.html): only WAF's `CheckCapacity` is authoritative. A web ACL over 1,500 WCU is billed extra, and 5,000 is the hard limit.
118
+
119
+ ## Custom responses
120
+
121
+ `respond(status, body?, headers?)` blocks with your own status code, body and headers rather than WAF's `403`. CloudFormation wants the body declared once on the web ACL and referenced by key from the rule; the builder does that for you, keying each body by the first rule that uses it and sharing one key between identical bodies.
122
+
123
+ ```ts
124
+ rule.respond(429, { json: { reason: "throttled" } }, { "Retry-After": "60" });
125
+ ```
126
+
127
+ The body is `{ json }` (serialised unless it is already a string), `{ text }` or `{ html }`, which set the content type. The default action can respond too: `.defaultAction("block", { status: 403, body: { text: "Forbidden" } })`.
128
+
129
+ These are checked at synth, against the [supported status codes](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-status-codes.html) and [quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html):
130
+
131
+ - **Status:** one of 200, 201, 202, 204, 206, 300, 301, 302, 303, 304, 307, 308, 400, 401, 403, 404, 405, 408, 409, 411, 412, 413, 414, 415, 416, 421, 429, 500, 501, 502, 503, 504 or 505. Notably not 410, and 402 is reserved for Monetize. Exported as `SUPPORTED_CUSTOM_RESPONSE_STATUS_CODES`.
132
+ - **Body:** at most 4 KB each, 50 distinct bodies and 50 KB in total per web ACL.
133
+ - **Headers:** at most 10, names of 1–64 characters of `[a-zA-Z0-9._$-]`, no duplicates, and not `content-type`, which WAF sets from the body.
134
+
135
+ A custom response only applies to Block, so `respond()` after another action, or another action after `respond()`, throws.
136
+
137
+ ## Logging
138
+
139
+ Logging is on by default, because the [Well-Architected security pillar](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/sec_detect_investigate_events_app_service_logging.html) asks for it and cdk-nag's NIST 800-53, PCI DSS and HIPAA packs fail a web ACL without it (`WAFv2LoggingEnabled`). The builder:
140
+
141
+ - creates a CloudWatch log group through [`@composurecdk/logs`](../logs/README.md), so it gets that package's retention and removal defaults, named `aws-waf-logs-<web ACL name>` (or a name derived from the construct path), since WAF only logs to names with that prefix;
142
+ - points an `AWS::WAFv2::LoggingConfiguration` at it, using the log group ARN **without** the trailing `:*` that `logGroup.logGroupArn` has, which WAF rejects;
143
+ - keeps only the requests WAF blocks, counts, or answers with a CAPTCHA or challenge (`DEFAULT_LOGGING_FILTER`). Filtering is AWS's main lever on log [volume and cost](https://docs.aws.amazon.com/waf/latest/developerguide/logging-management.html);
144
+ - redacts the `authorization` and `cookie` headers (`DEFAULT_LOGGING_REDACTED_HEADERS`), which carry replayable credentials and are no help when tuning a rule.
145
+
146
+ ```ts
147
+ createWebAclBuilder().logging({
148
+ filter: "all",
149
+ redact: { headers: ["x-amz-security-token"], queryString: true },
150
+ configure: (logGroup) => logGroup.retention(RetentionDays.ONE_YEAR),
151
+ });
152
+ ```
153
+
154
+ | Option | Effect |
155
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
+ | `filter` | `"non-allow"` (default), `"all"`, or a typed `{ defaultBehavior, filters }` whose conditions are `{ action }` or `{ label }`. The builder emits CloudFormation's PascalCase JSON, which CDK types as `any`. |
157
+ | `redact` | `headers`, `queryString`, `uriPath`, `method`. **`headers` adds to `authorization` and `cookie`; it does not replace them.** At most 100 fields. |
158
+ | `configure` | Customizes the created log group's builder. A name that does not start with `aws-waf-logs-` throws, since the deploy would fail. |
159
+ | `destination` | Logs to an existing CloudWatch log group, S3 bucket or Firehose stream instead, by ARN or `ref`. Its name must start with `aws-waf-logs-`; an S3 bucket must use SSE-S3 or a customer managed key, and a Firehose stream must be DirectPut. Cannot be combined with `configure`. |
160
+
161
+ `.logging(false)` turns logging off.
162
+
163
+ WAF grants `delivery.logs.amazonaws.com` write access to the log group itself when the logging configuration is created. It does this with a policy on the log group, not an account-level one, so it does not count toward CloudWatch Logs' limit of 10 resource policies per Region. The identity that creates the configuration (CloudFormation's execution role, under a CDK bootstrap) needs `logs:PutResourcePolicy`, `logs:DescribeResourcePolicies` and `logs:CreateLogDelivery` alongside its WAF permissions. Without them, the logging configuration fails to create and the deploy rolls back; logging is never left silently off. To see the policy, run `aws logs describe-resource-policies --policy-scope RESOURCE --resource-arn <log group ARN>`. A plain `describe-resource-policies` lists only account-level policies. Redaction applies to logs only — see `sampledRequests` in [Defaults](#defaults).
164
+
165
+ ## Metrics
166
+
167
+ `result.metrics` builds the [`AWS/WAFV2` metrics](https://docs.aws.amazon.com/waf/latest/developerguide/waf-metrics.html) from the names the builder gave the web ACL and its rules, so there is nothing to thread through by hand:
168
+
169
+ ```ts
170
+ const { metrics } = createWebAclBuilder().addRule("PerIpRate", …).build(stack, "Waf");
171
+
172
+ metrics.blockedRequests(); // every rule: Rule=ALL
173
+ metrics.rule("PerIpRate").blockedRequests(); // one rule, by the name given to addRule
174
+ metrics.defaultAction.allowedRequests(); // requests no rule terminated
175
+ metrics.daysToExpiry({ vendorName: "AWS", name: "AWSManagedRulesCommonRuleSet", version: "Version_1.12" });
176
+ ```
177
+
178
+ Each request metric — `allowedRequests`, `blockedRequests`, `countedRequests`, `captchaRequests`, `challengeRequests` — is a Sum over 5 minutes unless you pass `MetricOptions`.
179
+
180
+ The dimensions follow what WAF actually publishes, which differs from its documentation in two places (checked against a deployed web ACL):
181
+
182
+ - **`WebACL` is the web ACL's _name_**, not its `VisibilityConfig` metric name. An unnamed web ACL's dimension reads the generated name from its `Ref`.
183
+ - **The default action is published as `Rule=<the web ACL's metric name>`**, not `Rule=Default_Action`.
184
+ - `Rule` is the rule's metric name, and `Region` is present for a regional web ACL only.
185
+
186
+ ## Alarms
187
+
188
+ The recommended alarms are per rule. AWS publishes [no recommended alarms](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Best_Practice_Recommended_Alarms_AWS_Services.html) for WAF, so the defaults below (`WEB_ACL_RULE_ALARM_DEFAULTS`) are this library's starting points. Every alarm treats missing data as not breaching, because WAF publishes a datapoint only when its count is non-zero. No alarm actions are configured — apply them from the result or with [`alarmActionsPolicy`](../cloudwatch/README.md).
189
+
190
+ | Alarm key | Created for | Metric | Default threshold | Why |
191
+ | ----------------------- | ------------------------------------------ | ------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
192
+ | `<rule>BlockedRequests` | a rule you wrote whose action is Block | `BlockedRequests` (Sum, 5 min) | > 0 | A block by your own rule is a signal: a throttled client, a request without its credentials. Raise it on a rule that blocks routinely. |
193
+ | `<rule>BlockedRequests` | a rule-group rule — **opt-in** | `BlockedRequests` (Sum, 5 min) | > 0 | Managed rules block internet background noise all day; on by default, it would page constantly. |
194
+ | `<rule>DaysToExpiry` | a managed rule group pinned to a `version` | `DaysToExpiry` (Min, 1 hour) | < 30 | [Once a pinned version expires](https://docs.aws.amazon.com/waf/latest/developerguide/waf-managed-rule-groups-versioning-expiration.html), WAF rejects every update to the web ACL that does not move it on. |
195
+
196
+ `<rule>` is the rule's name in camelCase: `PerIpRate` and `per-ip-rate` both become `perIpRate`, so two rules whose names differ only that way cannot both have alarms; the build throws, naming them. No alarm is created for a rule that counts, allows, or answers with a CAPTCHA or challenge.
197
+
198
+ Tune or disable a rule's alarms with `.alarms()`, all of them with `.recommendedAlarms(false)`, and add your own with `addAlarm`, whose metric factory receives `WebAclMetrics`:
199
+
200
+ ```ts
201
+ createWebAclBuilder()
202
+ .addRule("PerIpRate", (rule) => rule.statement(…).alarms({ blockedRequests: { threshold: 50 } }))
203
+ .addRule("CommonRuleSet", (rule) => rule.statement(…).alarms({ blockedRequests: { threshold: 1000 } }))
204
+ .addRule("Probe", (rule) => rule.statement(…).count().alarms(false))
205
+ .addAlarm("probeCounted", (alarm) =>
206
+ alarm.metric((m) => m.rule("Probe").countedRequests()).threshold(100),
207
+ );
208
+ ```
209
+
210
+ A CLOUDFRONT web ACL's metrics are published in `us-east-1`, where the web ACL must be, so its alarms need nothing special.
211
+
212
+ ## Associations
213
+
214
+ `associateWith(target)` protects a regional resource. Call it once per resource; one web ACL can protect many. The target is an ARN, a CDK reference that `CfnWebACLAssociation` accepts in your `aws-cdk-lib` (recent releases take an Application Load Balancer or AppSync API), or an API Gateway stage, as a value or a `ref`.
215
+
216
+ - The association references the web ACL by `webAcl.attrArn`, so tools that check for one by reference, such as cdk-nag's `AwsSolutions-APIG3`, find it.
217
+ - A stage's ARN references the stage, so the association waits for it. A construct target is also depended on explicitly.
218
+ - AWS accepts one `AssociateWebACL` call every two seconds, so each association depends on the one before it rather than racing it.
219
+ - Each association's construct id is derived from its target, not its position, so removing one target does not replace — and briefly disassociate — the others.
220
+
221
+ ## CloudFront
222
+
223
+ A web ACL for a CloudFront distribution has `.scope("CLOUDFRONT")` and must be created in `us-east-1`. The builder throws at synth when its stack is in another region, falls back to `CDK_DEFAULT_REGION` for an environment-agnostic stack, and warns (`@composurecdk/wafv2:cloudfront-region`) when it cannot tell. A distribution attaches the web ACL itself, through its `webAclId`, so `associateWith` throws:
224
+
225
+ ```ts
226
+ const { webAcl } = createWebAclBuilder().scope("CLOUDFRONT").build(usEast1Stack, "EdgeWaf");
227
+ // distribution.webAclId(webAcl.attrArn)
228
+ ```
229
+
230
+ ## Constraints
231
+
232
+ Names, metric names and labels are validated at build against the AWS rules above ([ADR-0010](../../docs/adr/0010-aws-property-constraints.md)). The validators are exported for direct use:
233
+
234
+ ```ts
235
+ import { constraints } from "@composurecdk/wafv2";
236
+
237
+ constraints.validate.webAclName("api-waf");
238
+ constraints.validate.ruleName("PerIpRate");
239
+ constraints.validate.metricName("PerIpRate");
240
+ constraints.validate.ruleLabel("auth:missing-token");
241
+ ```
@@ -0,0 +1,60 @@
1
+ import type { AlarmConfig } from "@composurecdk/cloudwatch";
2
+ /**
3
+ * Controls the web ACL's recommended alarms. They are per rule — see
4
+ * {@link WebAclRuleAlarmConfig} — so this is the master switch.
5
+ */
6
+ export interface WebAclAlarmConfig {
7
+ /**
8
+ * Set to `false` to create none of the recommended alarms. Alarms added
9
+ * with `addAlarm()` are unaffected.
10
+ * @default true
11
+ */
12
+ enabled?: boolean;
13
+ }
14
+ /**
15
+ * Controls one rule's recommended alarms, set with the rule builder's
16
+ * `.alarms()`. Set an alarm to `false` to disable it, or to an
17
+ * {@link AlarmConfig} to enable it (if off by default) or tune it. Defaults
18
+ * are in {@link WEB_ACL_RULE_ALARM_DEFAULTS}.
19
+ *
20
+ * Every alarm treats missing data as not breaching: WAF publishes a
21
+ * datapoint only when the count is non-zero.
22
+ *
23
+ * AWS publishes no recommended alarms for WAF, so the thresholds are this
24
+ * library's starting points.
25
+ */
26
+ export interface WebAclRuleAlarmConfig {
27
+ /**
28
+ * Set to `false` to disable all of this rule's recommended alarms.
29
+ * @default true
30
+ */
31
+ enabled?: boolean;
32
+ /**
33
+ * Alarm when the rule blocks requests: `BlockedRequests` (Sum, 5 minutes)
34
+ * above the threshold.
35
+ *
36
+ * On by default for a rule you wrote whose action is Block, where a block
37
+ * is a signal — a throttled client, a request missing its credentials.
38
+ * Off by default for a rule-group rule, whose managed rules block internet
39
+ * background noise all day; pass an {@link AlarmConfig} to turn it on.
40
+ * Never created for a rule that does not block.
41
+ *
42
+ * Default threshold: > 0 blocked requests in one 5-minute period.
43
+ */
44
+ blockedRequests?: AlarmConfig | false;
45
+ /**
46
+ * Alarm when a pinned managed rule group version is about to expire:
47
+ * `DaysToExpiry` (Minimum, 1 hour) below the threshold. Once a version
48
+ * expires, WAF rejects every update to the web ACL that does not move
49
+ * the group to a current version.
50
+ *
51
+ * On by default for a managed rule group with a `version`; never created
52
+ * otherwise.
53
+ *
54
+ * Default threshold: < 30 days.
55
+ *
56
+ * @see https://docs.aws.amazon.com/waf/latest/developerguide/waf-managed-rule-groups-versioning-expiration.html
57
+ */
58
+ daysToExpiry?: AlarmConfig | false;
59
+ }
60
+ //# sourceMappingURL=alarm-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-config.d.ts","sourceRoot":"","sources":["../../src/alarm-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;IAEtC;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;CACpC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=alarm-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-config.js","sourceRoot":"","sources":["../../src/alarm-config.ts"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ import { TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
2
+ /**
3
+ * Default configuration of each rule's recommended alarms. AWS publishes no
4
+ * recommended alarms for WAF, so these are this library's starting points.
5
+ * Both treat missing data as not breaching, because WAF publishes a
6
+ * datapoint only when its value is non-zero.
7
+ */
8
+ export declare const WEB_ACL_RULE_ALARM_DEFAULTS: {
9
+ /**
10
+ * Any block in a 5-minute period. A rule you wrote encodes your own
11
+ * contract — a credential every caller should send, a rate no client
12
+ * should reach — so a single block is worth a look. Raise the threshold
13
+ * on a rule that blocks routinely.
14
+ */
15
+ blockedRequests: {
16
+ threshold: number;
17
+ evaluationPeriods: number;
18
+ datapointsToAlarm: number;
19
+ treatMissingData: TreatMissingData.NOT_BREACHING;
20
+ };
21
+ /**
22
+ * Fewer than 30 days before a pinned version expires, which leaves time to
23
+ * test and move to a newer one.
24
+ *
25
+ * @see https://docs.aws.amazon.com/waf/latest/developerguide/waf-managed-rule-groups-best-practice.html
26
+ */
27
+ daysToExpiry: {
28
+ threshold: number;
29
+ evaluationPeriods: number;
30
+ datapointsToAlarm: number;
31
+ treatMissingData: TreatMissingData.NOT_BREACHING;
32
+ };
33
+ };
34
+ //# sourceMappingURL=alarm-defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-defaults.d.ts","sourceRoot":"","sources":["../../src/alarm-defaults.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAG9D;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B;IACtC;;;;;OAKG;;;;;;;IAQH;;;;;OAKG;;;;;;;CAO0C,CAAC"}
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEB_ACL_RULE_ALARM_DEFAULTS = void 0;
4
+ const aws_cloudwatch_1 = require("aws-cdk-lib/aws-cloudwatch");
5
+ /**
6
+ * Default configuration of each rule's recommended alarms. AWS publishes no
7
+ * recommended alarms for WAF, so these are this library's starting points.
8
+ * Both treat missing data as not breaching, because WAF publishes a
9
+ * datapoint only when its value is non-zero.
10
+ */
11
+ exports.WEB_ACL_RULE_ALARM_DEFAULTS = {
12
+ /**
13
+ * Any block in a 5-minute period. A rule you wrote encodes your own
14
+ * contract — a credential every caller should send, a rate no client
15
+ * should reach — so a single block is worth a look. Raise the threshold
16
+ * on a rule that blocks routinely.
17
+ */
18
+ blockedRequests: {
19
+ threshold: 0,
20
+ evaluationPeriods: 1,
21
+ datapointsToAlarm: 1,
22
+ treatMissingData: aws_cloudwatch_1.TreatMissingData.NOT_BREACHING,
23
+ },
24
+ /**
25
+ * Fewer than 30 days before a pinned version expires, which leaves time to
26
+ * test and move to a newer one.
27
+ *
28
+ * @see https://docs.aws.amazon.com/waf/latest/developerguide/waf-managed-rule-groups-best-practice.html
29
+ */
30
+ daysToExpiry: {
31
+ threshold: 30,
32
+ evaluationPeriods: 1,
33
+ datapointsToAlarm: 1,
34
+ treatMissingData: aws_cloudwatch_1.TreatMissingData.NOT_BREACHING,
35
+ },
36
+ };
37
+ //# sourceMappingURL=alarm-defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-defaults.js","sourceRoot":"","sources":["../../src/alarm-defaults.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAG9D;;;;;GAKG;AACU,QAAA,2BAA2B,GAAG;IACzC;;;;;OAKG;IACH,eAAe,EAAE;QACf,SAAS,EAAE,CAAC;QACZ,iBAAiB,EAAE,CAAC;QACpB,iBAAiB,EAAE,CAAC;QACpB,gBAAgB,EAAE,iCAAgB,CAAC,aAAa;KACjD;IAED;;;;;OAKG;IACH,YAAY,EAAE;QACZ,SAAS,EAAE,EAAE;QACb,iBAAiB,EAAE,CAAC;QACpB,iBAAiB,EAAE,CAAC;QACpB,gBAAgB,EAAE,iCAAgB,CAAC,aAAa;KACjD;CAC4C,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type AlarmDefinition } from "@composurecdk/cloudwatch";
2
+ import type { WebAclAlarmConfig } from "./alarm-config.js";
3
+ import { type WebAclMetrics } from "./metrics.js";
4
+ import type { BuiltRule } from "./rule-builder.js";
5
+ /**
6
+ * The alarm-key prefix for a rule: its name in camelCase, so `PerIpRate` and
7
+ * `per-ip-rate` both become `perIpRate`.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare function ruleAlarmKey(name: string): string;
12
+ /**
13
+ * Resolves each rule's recommended alarms into {@link AlarmDefinition}s.
14
+ *
15
+ * @internal
16
+ */
17
+ export declare function resolveWebAclAlarmDefinitions(owner: string, metrics: WebAclMetrics, rules: readonly BuiltRule[], config: WebAclAlarmConfig | false | undefined): AlarmDefinition[];
18
+ //# sourceMappingURL=alarms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarms.d.ts","sourceRoot":"","sources":["../../src/alarms.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAInD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIjD;AAiBD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,GAC5C,eAAe,EAAE,CAkBnB"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ruleAlarmKey = ruleAlarmKey;
4
+ exports.resolveWebAclAlarmDefinitions = resolveWebAclAlarmDefinitions;
5
+ const aws_cdk_lib_1 = require("aws-cdk-lib");
6
+ const aws_cloudwatch_1 = require("aws-cdk-lib/aws-cloudwatch");
7
+ const cloudwatch_1 = require("@composurecdk/cloudwatch");
8
+ const alarm_defaults_js_1 = require("./alarm-defaults.js");
9
+ const metrics_js_1 = require("./metrics.js");
10
+ const PERIOD_LABEL = `${String(metrics_js_1.REQUEST_METRIC_PERIOD.toMinutes())} minutes`;
11
+ /**
12
+ * The alarm-key prefix for a rule: its name in camelCase, so `PerIpRate` and
13
+ * `per-ip-rate` both become `perIpRate`.
14
+ *
15
+ * @internal
16
+ */
17
+ function ruleAlarmKey(name) {
18
+ const parts = name.split(/[-_]+/).filter(Boolean);
19
+ const pascal = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ function isOff(config) {
23
+ return config === false || config?.enabled === false;
24
+ }
25
+ /** The managed rule group a statement pins to a version, if any. */
26
+ function pinnedGroup(statement) {
27
+ // The rule builder only takes a `StatementProperty`, never a token.
28
+ const group = statement.managedRuleGroupStatement;
29
+ if (group === undefined || aws_cdk_lib_1.Token.isUnresolved(group))
30
+ return undefined;
31
+ const { vendorName, name, version } = group;
32
+ return version === undefined ? undefined : { vendorName, name, version };
33
+ }
34
+ /**
35
+ * Resolves each rule's recommended alarms into {@link AlarmDefinition}s.
36
+ *
37
+ * @internal
38
+ */
39
+ function resolveWebAclAlarmDefinitions(owner, metrics, rules, config) {
40
+ if (isOff(config))
41
+ return [];
42
+ const keys = new Map();
43
+ return rules.flatMap((rule) => {
44
+ const definitions = resolveRuleAlarms(metrics, rule);
45
+ if (definitions.length === 0)
46
+ return [];
47
+ const { name } = rule.property;
48
+ const key = ruleAlarmKey(name);
49
+ const other = keys.get(key);
50
+ if (other !== undefined) {
51
+ throw new Error(`${owner}: rules "${other}" and "${name}" both have alarm key "${key}", because ` +
52
+ `alarm keys are rule names in camelCase. Rename one of them.`);
53
+ }
54
+ keys.set(key, name);
55
+ return definitions;
56
+ });
57
+ }
58
+ function definition(key, metric, comparisonOperator, userConfig, defaults, describe) {
59
+ const { alarmName, threshold, evaluationPeriods, datapointsToAlarm, treatMissingData } = (0, cloudwatch_1.resolveAlarmConfig)(userConfig, defaults);
60
+ return {
61
+ key,
62
+ alarmName,
63
+ metric,
64
+ threshold,
65
+ comparisonOperator,
66
+ evaluationPeriods,
67
+ datapointsToAlarm,
68
+ treatMissingData,
69
+ description: describe(threshold),
70
+ };
71
+ }
72
+ function resolveRuleAlarms(metrics, rule) {
73
+ const config = rule.alarms;
74
+ if (isOff(config))
75
+ return [];
76
+ const { name } = rule.property;
77
+ const key = ruleAlarmKey(name);
78
+ const definitions = [];
79
+ const blocked = config ? config.blockedRequests : undefined;
80
+ // A rule group's managed rules block background noise, so its alarm is opt-in.
81
+ if (rule.blocks && blocked !== false && (!rule.ruleGroup || blocked !== undefined)) {
82
+ definitions.push(definition(`${key}BlockedRequests`, metrics.rule(name).blockedRequests(), aws_cloudwatch_1.ComparisonOperator.GREATER_THAN_THRESHOLD, blocked, alarm_defaults_js_1.WEB_ACL_RULE_ALARM_DEFAULTS.blockedRequests, (t) => `WAF rule "${name}" is blocking requests. Threshold: > ${String(t)} blocked requests in ${PERIOD_LABEL}.`));
83
+ }
84
+ const group = pinnedGroup(rule.property.statement);
85
+ const expiry = config ? config.daysToExpiry : undefined;
86
+ if (group && expiry !== false) {
87
+ const version = aws_cdk_lib_1.Token.isUnresolved(group.version)
88
+ ? "its pinned version"
89
+ : `version ${group.version}`;
90
+ definitions.push(definition(`${key}DaysToExpiry`, metrics.daysToExpiry(group), aws_cloudwatch_1.ComparisonOperator.LESS_THAN_THRESHOLD, expiry, alarm_defaults_js_1.WEB_ACL_RULE_ALARM_DEFAULTS.daysToExpiry, (t) => `WAF rule "${name}" pins ${group.name} at ${version}, which expires in under ${String(t)} days. Move it to a newer version; WAF rejects updates to the web ACL once it expires.`));
91
+ }
92
+ return definitions;
93
+ }
94
+ //# sourceMappingURL=alarms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarms.js","sourceRoot":"","sources":["../../src/alarms.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { Stage } from "aws-cdk-lib/aws-apigateway";
2
+ import { CfnWebACLAssociation, type CfnWebACLAssociationProps } from "aws-cdk-lib/aws-wafv2";
3
+ import { type IConstruct } from "constructs";
4
+ /**
5
+ * A resource a regional web ACL can protect: an ARN, a CDK reference CDK
6
+ * itself accepts (an Application Load Balancer, an AppSync API, …), or an API
7
+ * Gateway REST API stage.
8
+ *
9
+ * The ARN and reference types are read from CDK's own `resourceArn` prop, so
10
+ * they track the installed `aws-cdk-lib` (ADR-0018).
11
+ */
12
+ export type WebAclAssociationTarget = NonNullable<CfnWebACLAssociationProps["resourceArn"]> | Pick<Stage, "stageArn" | "node">;
13
+ /**
14
+ * Creates one `AWS::WAFv2::WebACLAssociation` per target, each depending on
15
+ * the one before it: AWS allows one `AssociateWebACL` call every two seconds
16
+ * per account and Region, so parallel associations throttle themselves.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare function associate(scope: IConstruct, id: string, webAclArn: string, targets: WebAclAssociationTarget[]): CfnWebACLAssociation[];
21
+ //# sourceMappingURL=association.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"association.d.ts","sourceRoot":"","sources":["../../src/association.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GACjC,WAAW,CAAC,yBAAyB,CAAC,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,CAAC;AA2B3F;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,uBAAuB,EAAE,GACjC,oBAAoB,EAAE,CAsBxB"}