import * as cdk from "aws-cdk-lib"; import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import * as ssm from "aws-cdk-lib/aws-ssm"; import { Construct } from "constructs"; /** * Shared CloudFront WAF WebACL for Sea Haven app distributions (audit M-17). * * AWS managed rule groups (Common + Known Bad Inputs) plus an IP rate limit. * CLOUDFRONT-scope WebACLs must live in us-east-1 — which is where this stack * is — so it can be referenced by any app CloudFront distribution by ARN. * * The ARN is published to SSM (`/seahaven/waf/app-web-acl-arn`) so app stacks in * other repos can consume it via `{{resolve:ssm:...}}` without a hard CFN export. */ export interface AppWebAclProps { /** * Deletion policy for the WebACL only. The SSM parameter keeps the default * Delete policy so a later stack update can remove the parameter. Prod does * not set this. Management sets RETAIN first because the live WebACL is * already gone and a Delete call would fail and roll back into a recreate. */ readonly webAclRemovalPolicy?: cdk.RemovalPolicy; /** * Physical id of a WebACL CloudFormation already tracks. When set, the SSM * parameter and output publish `global/webacl/seahaven-app-waf/` with * account and region tokens, and do not reference WebACL.Arn. GetAtt on a * missing ACL calls GetWebACL and fails the stack update. */ readonly existingWebAclId?: string; } export class AppWebAcl extends Construct { constructor(scope: Construct, id: string, props?: AppWebAclProps) { super(scope, id); const vis = (metric: string): wafv2.CfnWebACL.VisibilityConfigProperty => ({ cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: metric, }); const webAcl = new wafv2.CfnWebACL(this, "AppWebAcl", { name: "seahaven-app-waf", scope: "CLOUDFRONT", defaultAction: { allow: {} }, visibilityConfig: vis("seahaven-app-waf"), rules: [ { name: "AWSCommonRuleSet", priority: 1, overrideAction: { none: {} }, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesCommonRuleSet", }, }, visibilityConfig: vis("AWSCommonRuleSet"), }, { name: "AWSKnownBadInputs", priority: 2, overrideAction: { none: {} }, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesKnownBadInputsRuleSet", }, }, visibilityConfig: vis("AWSKnownBadInputs"), }, { name: "RateLimitPerIp", priority: 3, action: { block: {} }, statement: { rateBasedStatement: { limit: 2000, aggregateKeyType: "IP" }, }, visibilityConfig: vis("RateLimitPerIp"), }, ], }); if (props?.webAclRemovalPolicy) { webAcl.applyRemovalPolicy(props.webAclRemovalPolicy); } const publishedArn = props?.existingWebAclId ? cdk.Stack.of(this).formatArn({ service: "wafv2", resource: "global/webacl", resourceName: `seahaven-app-waf/${props.existingWebAclId}`, arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, }) : webAcl.attrArn; new ssm.StringParameter(this, "AppWebAclArnParam", { parameterName: "/seahaven/waf/app-web-acl-arn", stringValue: publishedArn, description: "ARN of the shared CloudFront WAF WebACL (audit M-17)", }); new cdk.CfnOutput(this, "AppWebAclArn", { value: publishedArn }); } }