From e413724ed6526dd3764384ab5b44ae92f8393df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Hord=C4=9Bj=C4=8Duk?= Date: Mon, 28 Mar 2022 16:14:46 +0200 Subject: [PATCH] feat(waf): add basic WAF monitoring (#89) Related #76 --- _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_ --- API.md | 528 +++++++++++++++++- README.md | 1 + lib/facade/MonitoringAspect.ts | 14 + lib/facade/MonitoringFacade.ts | 8 + lib/facade/aspect-types.ts | 2 + .../aws-wafv2/WafV2MetricFactory.ts | 60 ++ lib/monitoring/aws-wafv2/WafV2Monitoring.ts | 119 ++++ lib/monitoring/aws-wafv2/index.ts | 2 + lib/monitoring/index.ts | 1 + test/facade/MonitoringAspect.test.ts | 23 + .../MonitoringAspect.test.ts.snap | 80 +++ .../aws-wafv2/WafV2Monitoring.test.ts | 25 + .../WafV2Monitoring.test.ts.snap | 22 + 13 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 lib/monitoring/aws-wafv2/WafV2MetricFactory.ts create mode 100644 lib/monitoring/aws-wafv2/WafV2Monitoring.ts create mode 100644 lib/monitoring/aws-wafv2/index.ts create mode 100644 test/monitoring/aws-wafv2/WafV2Monitoring.test.ts create mode 100644 test/monitoring/aws-wafv2/__snapshots__/WafV2Monitoring.test.ts.snap diff --git a/API.md b/API.md index 5724896a..bdda8378 100644 --- a/API.md +++ b/API.md @@ -780,6 +780,7 @@ new MonitoringFacade(scope: Construct, id: string, props: MonitoringFacadeProps) | monitorStepFunctionLambdaIntegration | *No description.* | | monitorStepFunctionServiceIntegration | *No description.* | | monitorSyntheticsCanary | *No description.* | +| monitorWebApplicationFirewallAclV2 | *No description.* | --- @@ -1613,6 +1614,18 @@ public monitorSyntheticsCanary(props: SyntheticsCanaryMonitoringProps): Monitori --- +##### `monitorWebApplicationFirewallAclV2` + +```typescript +public monitorWebApplicationFirewallAclV2(props: WafV2MonitoringProps): MonitoringFacade +``` + +###### `props`Required + +- *Type:* WafV2MonitoringProps + +--- + #### Static Functions | **Name** | **Description** | @@ -23313,6 +23326,7 @@ const monitoringAspectProps: MonitoringAspectProps = { ... } | sqs | MonitoringAspectType | *No description.* | | stepFunctions | MonitoringAspectType | *No description.* | | syntheticsCanaries | MonitoringAspectType | *No description.* | +| webApplicationFirewallAclV2 | MonitoringAspectType | *No description.* | --- @@ -23566,6 +23580,16 @@ public readonly syntheticsCanaries: MonitoringAspectType; --- +##### `webApplicationFirewallAclV2`Optional + +```typescript +public readonly webApplicationFirewallAclV2: MonitoringAspectType; +``` + +- *Type:* MonitoringAspectType + +--- + ### MonitoringAspectType #### Initializer @@ -33680,6 +33704,311 @@ The length must be 1 - 255 characters and although the validation rules are undo --- +### WafV2MetricFactoryProps + +#### Initializer + +```typescript +import { WafV2MetricFactoryProps } from 'cdk-monitoring-constructs' + +const wafV2MetricFactoryProps: WafV2MetricFactoryProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| acl | monocdk.aws_wafv2.CfnWebACL | *No description.* | +| region | string | *No description.* | + +--- + +##### `acl`Required + +```typescript +public readonly acl: CfnWebACL; +``` + +- *Type:* monocdk.aws_wafv2.CfnWebACL + +--- + +##### `region`Optional + +```typescript +public readonly region: string; +``` + +- *Type:* string + +--- + +### WafV2MonitoringOptions + +#### Initializer + +```typescript +import { WafV2MonitoringOptions } from 'cdk-monitoring-constructs' + +const wafV2MonitoringOptions: WafV2MonitoringOptions = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| alarmFriendlyName | string | Plain name, used in naming alarms. | +| humanReadableName | string | Human-readable name is a freeform string, used as a caption or description. | +| localAlarmNamePrefixOverride | string | If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. | +| addToAlarmDashboard | boolean | Flag indicating if the widgets should be added to alarm dashboard. | +| addToDetailDashboard | boolean | Flag indicating if the widgets should be added to detailed dashboard. | +| addToSummaryDashboard | boolean | Flag indicating if the widgets should be added to summary dashboard. | +| useCreatedAlarms | IAlarmConsumer | Calls provided function to process all alarms created. | + +--- + +##### `alarmFriendlyName`Optional + +```typescript +public readonly alarmFriendlyName: string; +``` + +- *Type:* string +- *Default:* derives name from the construct itself + +Plain name, used in naming alarms. + +This unique among other resources, and respect the AWS CDK restriction posed on alarm names. +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +--- + +##### `humanReadableName`Optional + +```typescript +public readonly humanReadableName: string; +``` + +- *Type:* string +- *Default:* use alarmFriendlyName + +Human-readable name is a freeform string, used as a caption or description. + +There are no limitations on what it can be. + +--- + +##### `localAlarmNamePrefixOverride`Optional + +```typescript +public readonly localAlarmNamePrefixOverride: string; +``` + +- *Type:* string + +If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. + +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +> [AlarmNamingStrategy for more details on alarm name prefixes](AlarmNamingStrategy for more details on alarm name prefixes) + +--- + +##### `addToAlarmDashboard`Optional + +```typescript +public readonly addToAlarmDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to alarm dashboard. + +--- + +##### `addToDetailDashboard`Optional + +```typescript +public readonly addToDetailDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to detailed dashboard. + +--- + +##### `addToSummaryDashboard`Optional + +```typescript +public readonly addToSummaryDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to summary dashboard. + +--- + +##### `useCreatedAlarms`Optional + +```typescript +public readonly useCreatedAlarms: IAlarmConsumer; +``` + +- *Type:* IAlarmConsumer + +Calls provided function to process all alarms created. + +--- + +### WafV2MonitoringProps + +#### Initializer + +```typescript +import { WafV2MonitoringProps } from 'cdk-monitoring-constructs' + +const wafV2MonitoringProps: WafV2MonitoringProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| acl | monocdk.aws_wafv2.CfnWebACL | *No description.* | +| region | string | *No description.* | +| alarmFriendlyName | string | Plain name, used in naming alarms. | +| humanReadableName | string | Human-readable name is a freeform string, used as a caption or description. | +| localAlarmNamePrefixOverride | string | If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. | +| addToAlarmDashboard | boolean | Flag indicating if the widgets should be added to alarm dashboard. | +| addToDetailDashboard | boolean | Flag indicating if the widgets should be added to detailed dashboard. | +| addToSummaryDashboard | boolean | Flag indicating if the widgets should be added to summary dashboard. | +| useCreatedAlarms | IAlarmConsumer | Calls provided function to process all alarms created. | + +--- + +##### `acl`Required + +```typescript +public readonly acl: CfnWebACL; +``` + +- *Type:* monocdk.aws_wafv2.CfnWebACL + +--- + +##### `region`Optional + +```typescript +public readonly region: string; +``` + +- *Type:* string + +--- + +##### `alarmFriendlyName`Optional + +```typescript +public readonly alarmFriendlyName: string; +``` + +- *Type:* string +- *Default:* derives name from the construct itself + +Plain name, used in naming alarms. + +This unique among other resources, and respect the AWS CDK restriction posed on alarm names. +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +--- + +##### `humanReadableName`Optional + +```typescript +public readonly humanReadableName: string; +``` + +- *Type:* string +- *Default:* use alarmFriendlyName + +Human-readable name is a freeform string, used as a caption or description. + +There are no limitations on what it can be. + +--- + +##### `localAlarmNamePrefixOverride`Optional + +```typescript +public readonly localAlarmNamePrefixOverride: string; +``` + +- *Type:* string + +If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. + +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +> [AlarmNamingStrategy for more details on alarm name prefixes](AlarmNamingStrategy for more details on alarm name prefixes) + +--- + +##### `addToAlarmDashboard`Optional + +```typescript +public readonly addToAlarmDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to alarm dashboard. + +--- + +##### `addToDetailDashboard`Optional + +```typescript +public readonly addToDetailDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to detailed dashboard. + +--- + +##### `addToSummaryDashboard`Optional + +```typescript +public readonly addToSummaryDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to summary dashboard. + +--- + +##### `useCreatedAlarms`Optional + +```typescript +public readonly useCreatedAlarms: IAlarmConsumer; +``` + +- *Type:* IAlarmConsumer + +Calls provided function to process all alarms created. + +--- + ### XaxrMathExpressionProps Custom wrapper class for MathExpressionProps that supports account and region customization. @@ -46733,6 +47062,203 @@ public addMemoryUsagePercentAlarm(percentMetric: Metric | MathExpression, props: +### WafV2MetricFactory + +https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html. + +#### Initializers + +```typescript +import { WafV2MetricFactory } from 'cdk-monitoring-constructs' + +new WafV2MetricFactory(metricFactory: MetricFactory, props: WafV2MetricFactoryProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| metricFactory | MetricFactory | *No description.* | +| props | WafV2MetricFactoryProps | *No description.* | + +--- + +##### `metricFactory`Required + +- *Type:* MetricFactory + +--- + +##### `props`Required + +- *Type:* WafV2MetricFactoryProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| metricAllowedRequests | *No description.* | +| metricBlockedRequests | *No description.* | +| metricBlockedRequestsRate | *No description.* | + +--- + +##### `metricAllowedRequests` + +```typescript +public metricAllowedRequests(): Metric | MathExpression +``` + +##### `metricBlockedRequests` + +```typescript +public metricBlockedRequests(): Metric | MathExpression +``` + +##### `metricBlockedRequestsRate` + +```typescript +public metricBlockedRequestsRate(): Metric | MathExpression +``` + + + + +### WafV2Monitoring + +Monitoring for AWS Web Application Firewall. + +> [https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html) + +#### Initializers + +```typescript +import { WafV2Monitoring } from 'cdk-monitoring-constructs' + +new WafV2Monitoring(scope: MonitoringScope, props: WafV2MonitoringProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | MonitoringScope | *No description.* | +| props | WafV2MonitoringProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* MonitoringScope + +--- + +##### `props`Required + +- *Type:* WafV2MonitoringProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| addAlarm | Adds an alarm. | +| alarmWidgets | Returns widgets for all alarms. | +| createAlarmFactory | Creates a new alarm factory. | +| createdAlarms | Returns all the alarms created. | +| createMetricFactory | Creates a new metric factory. | +| createWidgetFactory | Creates a new widget factory. | +| summaryWidgets | Returns widgets to be placed on the summary dashboard. | +| widgets | Returns widgets to be placed on the main dashboard. | + +--- + +##### `addAlarm` + +```typescript +public addAlarm(alarm: AlarmWithAnnotation): void +``` + +Adds an alarm. + +###### `alarm`Required + +- *Type:* AlarmWithAnnotation + +alarm to add. + +--- + +##### `alarmWidgets` + +```typescript +public alarmWidgets(): IWidget[] +``` + +Returns widgets for all alarms. + +These can go to runbook or to service dashboard. + +##### `createAlarmFactory` + +```typescript +public createAlarmFactory(alarmNamePrefix: string): AlarmFactory +``` + +Creates a new alarm factory. + +Alarms created will be named with the given prefix, unless a local name override is present. + +###### `alarmNamePrefix`Required + +- *Type:* string + +alarm name prefix. + +--- + +##### `createdAlarms` + +```typescript +public createdAlarms(): AlarmWithAnnotation[] +``` + +Returns all the alarms created. + +##### `createMetricFactory` + +```typescript +public createMetricFactory(): MetricFactory +``` + +Creates a new metric factory. + +##### `createWidgetFactory` + +```typescript +public createWidgetFactory(): IWidgetFactory +``` + +Creates a new widget factory. + +##### `summaryWidgets` + +```typescript +public summaryWidgets(): IWidget[] +``` + +Returns widgets to be placed on the summary dashboard. + +##### `widgets` + +```typescript +public widgets(): IWidget[] +``` + +Returns widgets to be placed on the main dashboard. + + + + ### XaxrMathExpression - *Implements:* monocdk.aws_cloudwatch.IMetric @@ -47027,7 +47553,7 @@ Dashboard placement override props. ### IDashboardSegment -- *Implemented By:* ApiGatewayMonitoring, ApiGatewayV2HttpApiMonitoring, AppSyncMonitoring, AutoScalingGroupMonitoring, BillingMonitoring, CertificateManagerMonitoring, CloudFrontDistributionMonitoring, CodeBuildProjectMonitoring, CustomMonitoring, DynamoTableGlobalSecondaryIndexMonitoring, DynamoTableMonitoring, EC2Monitoring, Ec2ServiceMonitoring, ElastiCacheClusterMonitoring, FargateServiceMonitoring, GlueJobMonitoring, KinesisDataAnalyticsMonitoring, KinesisDataStreamMonitoring, KinesisFirehoseMonitoring, LambdaFunctionMonitoring, LogMonitoring, Monitoring, NetworkLoadBalancerMonitoring, OpenSearchClusterMonitoring, RdsClusterMonitoring, RedshiftClusterMonitoring, S3BucketMonitoring, SecretsManagerSecretMonitoring, SingleWidgetDashboardSegment, SnsTopicMonitoring, SqsQueueMonitoring, SqsQueueMonitoringWithDlq, StepFunctionActivityMonitoring, StepFunctionLambdaIntegrationMonitoring, StepFunctionMonitoring, StepFunctionServiceIntegrationMonitoring, SyntheticsCanaryMonitoring, IDashboardSegment +- *Implemented By:* ApiGatewayMonitoring, ApiGatewayV2HttpApiMonitoring, AppSyncMonitoring, AutoScalingGroupMonitoring, BillingMonitoring, CertificateManagerMonitoring, CloudFrontDistributionMonitoring, CodeBuildProjectMonitoring, CustomMonitoring, DynamoTableGlobalSecondaryIndexMonitoring, DynamoTableMonitoring, EC2Monitoring, Ec2ServiceMonitoring, ElastiCacheClusterMonitoring, FargateServiceMonitoring, GlueJobMonitoring, KinesisDataAnalyticsMonitoring, KinesisDataStreamMonitoring, KinesisFirehoseMonitoring, LambdaFunctionMonitoring, LogMonitoring, Monitoring, NetworkLoadBalancerMonitoring, OpenSearchClusterMonitoring, RdsClusterMonitoring, RedshiftClusterMonitoring, S3BucketMonitoring, SecretsManagerSecretMonitoring, SingleWidgetDashboardSegment, SnsTopicMonitoring, SqsQueueMonitoring, SqsQueueMonitoringWithDlq, StepFunctionActivityMonitoring, StepFunctionLambdaIntegrationMonitoring, StepFunctionMonitoring, StepFunctionServiceIntegrationMonitoring, SyntheticsCanaryMonitoring, WafV2Monitoring, IDashboardSegment #### Methods diff --git a/README.md b/README.md index 74463f77..24655546 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ You can also browse the documentation at https://constructs.dev/packages/cdk-mon | AWS SNS Topic (`.monitorSnsTopic()`) | Message count, size, failed notifications | Failed notifications | | | AWS SQS Queue (`.monitorSqsQueue()`, `.monitorSqsQueueWithDlq()`) | Message count, age, size | Message count, age, DLQ incoming messages | | | AWS Step Functions (`.monitorStepFunction()`, `.monitorStepFunctionActivity()`, `monitorStepFunctionLambdaIntegration()`, `.monitorStepFunctionServiceIntegration()`) | Execution count and breakdown per state | Duration, failed, failed rate, aborted, throttled, timed out executions | | +| AWS Web Application Firewall (`.monitorWebApplicationFirewallAcl()`) | Allowed/blocked requests | | | | CloudWatch Logs (`.monitorLog()`) | Patterns present in the log group | | | | Custom metrics (`.monitorCustom()`) | Addition of custom metrics into the dashboard (each group is a widget) | | Supports anomaly detection | diff --git a/lib/facade/MonitoringAspect.ts b/lib/facade/MonitoringAspect.ts index 9411da48..05591650 100644 --- a/lib/facade/MonitoringAspect.ts +++ b/lib/facade/MonitoringAspect.ts @@ -22,6 +22,7 @@ import * as sns from "monocdk/aws-sns"; import * as sqs from "monocdk/aws-sqs"; import * as stepfunctions from "monocdk/aws-stepfunctions"; import * as synthetics from "monocdk/aws-synthetics"; +import * as wafv2 from "monocdk/aws-wafv2"; import { ElastiCacheClusterType } from "../monitoring"; import { MonitoringAspectProps, MonitoringAspectType } from "./aspect-types"; @@ -64,6 +65,7 @@ export class MonitoringAspect implements IAspect { this.monitorSqs(node); this.monitorStepFunctions(node); this.monitorSyntheticsCanaries(node); + this.monitorWebApplicationFirewallV2Acls(node); if (!this.addedNodeIndependentMonitoringToScope) { this.addedNodeIndependentMonitoringToScope = true; @@ -365,6 +367,18 @@ export class MonitoringAspect implements IAspect { }); } } + + private monitorWebApplicationFirewallV2Acls(node: IConstruct) { + const [isEnabled, props] = this.getMonitoringDetails( + this.props.webApplicationFirewallAclV2 + ); + if (isEnabled && node instanceof wafv2.CfnWebACL) { + this.monitoringFacade.monitorWebApplicationFirewallAclV2({ + acl: node, + ...props, + }); + } + } } export * from "./aspect-types"; diff --git a/lib/facade/MonitoringFacade.ts b/lib/facade/MonitoringFacade.ts index 0145889a..5c7802a1 100644 --- a/lib/facade/MonitoringFacade.ts +++ b/lib/facade/MonitoringFacade.ts @@ -103,6 +103,8 @@ import { getQueueProcessingFargateServiceMonitoring, SyntheticsCanaryMonitoringProps, SyntheticsCanaryMonitoring, + WafV2MonitoringProps, + WafV2Monitoring, } from "../monitoring"; import { MonitoringAspect, MonitoringAspectProps } from "./MonitoringAspect"; @@ -613,6 +615,12 @@ export class MonitoringFacade extends MonitoringScope { return this; } + monitorWebApplicationFirewallAclV2(props: WafV2MonitoringProps) { + const segment = new WafV2Monitoring(this, props); + this.addSegment(segment, props); + return this; + } + monitorBilling(props?: BillingMonitoringProps) { const segment = new BillingMonitoring(this, props ?? {}); this.addSegment(segment, props); diff --git a/lib/facade/aspect-types.ts b/lib/facade/aspect-types.ts index 8b02b7d5..1c7b733f 100644 --- a/lib/facade/aspect-types.ts +++ b/lib/facade/aspect-types.ts @@ -24,6 +24,7 @@ import { SqsQueueMonitoringOptions, StepFunctionMonitoringOptions, SyntheticsCanaryMonitoringOptions, + WafV2MonitoringOptions, } from "../monitoring"; export interface MonitoringAspectType { @@ -66,4 +67,5 @@ export interface MonitoringAspectProps { readonly sqs?: MonitoringAspectType; readonly stepFunctions?: MonitoringAspectType; readonly syntheticsCanaries?: MonitoringAspectType; + readonly webApplicationFirewallAclV2?: MonitoringAspectType; } diff --git a/lib/monitoring/aws-wafv2/WafV2MetricFactory.ts b/lib/monitoring/aws-wafv2/WafV2MetricFactory.ts new file mode 100644 index 00000000..dd91492d --- /dev/null +++ b/lib/monitoring/aws-wafv2/WafV2MetricFactory.ts @@ -0,0 +1,60 @@ +import { DimensionHash } from "monocdk/aws-cloudwatch"; +import { CfnWebACL } from "monocdk/aws-wafv2"; +import { MetricFactory, MetricStatistic } from "../../common"; + +const MetricNamespace = "AWS/WAFV2"; +const AllRulesDimensionValue = "ALL"; + +export interface WafV2MetricFactoryProps { + readonly region?: string; + readonly acl: CfnWebACL; +} + +/** + * https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html + */ +export class WafV2MetricFactory { + protected readonly metricFactory: MetricFactory; + protected readonly dimensions: DimensionHash; + + constructor(metricFactory: MetricFactory, props: WafV2MetricFactoryProps) { + this.metricFactory = metricFactory; + this.dimensions = { + Rule: AllRulesDimensionValue, + WebACL: props.acl.name, + }; + } + + metricAllowedRequests() { + return this.metricFactory.createMetric( + "AllowedRequests", + MetricStatistic.SUM, + "Allowed", + this.dimensions, + undefined, + MetricNamespace + ); + } + + metricBlockedRequests() { + return this.metricFactory.createMetric( + "BlockedRequests", + MetricStatistic.SUM, + "Blocked", + this.dimensions, + undefined, + MetricNamespace + ); + } + + metricBlockedRequestsRate() { + return this.metricFactory.createMetricMath( + "100 * (blocked / (allowed + blocked))", + { + allowed: this.metricAllowedRequests(), + blocked: this.metricBlockedRequests(), + }, + "Blocked (rate)" + ); + } +} diff --git a/lib/monitoring/aws-wafv2/WafV2Monitoring.ts b/lib/monitoring/aws-wafv2/WafV2Monitoring.ts new file mode 100644 index 00000000..680fafef --- /dev/null +++ b/lib/monitoring/aws-wafv2/WafV2Monitoring.ts @@ -0,0 +1,119 @@ +import { GraphWidget, IWidget } from "monocdk/aws-cloudwatch"; +import { + BaseMonitoringProps, + CountAxisFromZero, + DefaultGraphWidgetHeight, + DefaultSummaryWidgetHeight, + MetricWithAlarmSupport, + Monitoring, + MonitoringScope, + RateAxisFromZero, + ThirdWidth, +} from "../../common"; +import { + MonitoringHeaderWidget, + MonitoringNamingStrategy, +} from "../../dashboard"; +import { + WafV2MetricFactory, + WafV2MetricFactoryProps, +} from "./WafV2MetricFactory"; + +export interface WafV2MonitoringOptions extends BaseMonitoringProps {} + +export interface WafV2MonitoringProps + extends WafV2MetricFactoryProps, + WafV2MonitoringOptions {} + +/** + * Monitoring for AWS Web Application Firewall. + * + * @see https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html + */ +export class WafV2Monitoring extends Monitoring { + protected readonly humanReadableName: string; + + protected readonly allowedRequestsMetric: MetricWithAlarmSupport; + protected readonly blockedRequestsMetric: MetricWithAlarmSupport; + protected readonly blockedRequestsRateMetric: MetricWithAlarmSupport; + + constructor(scope: MonitoringScope, props: WafV2MonitoringProps) { + super(scope, props); + + const namingStrategy = new MonitoringNamingStrategy({ + ...props, + namedConstruct: props.acl, + }); + this.humanReadableName = namingStrategy.resolveHumanReadableName(); + + const metricFactory = new WafV2MetricFactory( + scope.createMetricFactory(), + props + ); + + this.allowedRequestsMetric = metricFactory.metricAllowedRequests(); + this.blockedRequestsMetric = metricFactory.metricBlockedRequests(); + this.blockedRequestsRateMetric = metricFactory.metricBlockedRequestsRate(); + } + + summaryWidgets(): IWidget[] { + return [ + this.createTitleWidget(), + this.createAllowedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight), + this.createBlockedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight), + this.createBlockedRequestsRateWidget( + ThirdWidth, + DefaultSummaryWidgetHeight + ), + ]; + } + + widgets(): IWidget[] { + return [ + this.createTitleWidget(), + this.createAllowedRequestsWidget(ThirdWidth, DefaultGraphWidgetHeight), + this.createBlockedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight), + this.createBlockedRequestsRateWidget( + ThirdWidth, + DefaultSummaryWidgetHeight + ), + ]; + } + + protected createTitleWidget() { + return new MonitoringHeaderWidget({ + family: "Web Application Firewall", + title: this.humanReadableName, + }); + } + + protected createAllowedRequestsWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Allowed Requests", + left: [this.allowedRequestsMetric], + leftYAxis: CountAxisFromZero, + }); + } + + protected createBlockedRequestsWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Blocked Requests", + left: [this.blockedRequestsMetric], + leftYAxis: CountAxisFromZero, + }); + } + + protected createBlockedRequestsRateWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Blocked Requests (rate)", + left: [this.blockedRequestsRateMetric], + leftYAxis: RateAxisFromZero, + }); + } +} diff --git a/lib/monitoring/aws-wafv2/index.ts b/lib/monitoring/aws-wafv2/index.ts new file mode 100644 index 00000000..03c1fdd8 --- /dev/null +++ b/lib/monitoring/aws-wafv2/index.ts @@ -0,0 +1,2 @@ +export * from "./WafV2MetricFactory"; +export * from "./WafV2Monitoring"; diff --git a/lib/monitoring/index.ts b/lib/monitoring/index.ts index e68414f2..a7468576 100644 --- a/lib/monitoring/index.ts +++ b/lib/monitoring/index.ts @@ -24,4 +24,5 @@ export * from "./aws-sns"; export * from "./aws-sqs"; export * from "./aws-step-functions"; export * from "./aws-synthetics"; +export * from "./aws-wafv2"; export * from "./custom"; diff --git a/test/facade/MonitoringAspect.test.ts b/test/facade/MonitoringAspect.test.ts index 1a07b419..51e9582b 100644 --- a/test/facade/MonitoringAspect.test.ts +++ b/test/facade/MonitoringAspect.test.ts @@ -26,6 +26,7 @@ import * as sns from "monocdk/aws-sns"; import * as sqs from "monocdk/aws-sqs"; import * as stepfunctions from "monocdk/aws-stepfunctions"; import * as synthetics from "monocdk/aws-synthetics"; +import { CfnWebACL } from "monocdk/aws-wafv2"; import { DefaultDashboardFactory, @@ -535,4 +536,26 @@ describe("MonitoringAspect", () => { // THEN expect(Template.fromStack(stack)).toMatchSnapshot(); }); + + test("WAF v2", () => { + // GIVEN + const stack = new Stack(); + const facade = createDummyMonitoringFacade(stack); + + new CfnWebACL(stack, "DummyAcl", { + defaultAction: { allow: {} }, + scope: "REGIONAL", + visibilityConfig: { + sampledRequestsEnabled: true, + cloudWatchMetricsEnabled: true, + metricName: "DummyMetricName", + }, + }); + + // WHEN + facade.monitorScope(stack, defaultAspectProps); + + // THEN + expect(Template.fromStack(stack)).toMatchSnapshot(); + }); }); diff --git a/test/facade/__snapshots__/MonitoringAspect.test.ts.snap b/test/facade/__snapshots__/MonitoringAspect.test.ts.snap index ecf031c2..0baf5f42 100644 --- a/test/facade/__snapshots__/MonitoringAspect.test.ts.snap +++ b/test/facade/__snapshots__/MonitoringAspect.test.ts.snap @@ -7283,3 +7283,83 @@ Object { }, } `; + +exports[`MonitoringAspect WAF v2 1`] = ` +Object { + "Resources": Object { + "DashboardFactoryAlarmDashboard6286FAD3": Object { + "Properties": Object { + "DashboardBody": "{\\"start\\":\\"-PT8H\\",\\"widgets\\":[]}", + "DashboardName": "DummyDashboard-Alarms", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "DashboardFactoryDashboard3E20AD6E": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-PT8H\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Web Application Firewall **DummyAcl**\\"}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Allowed Requests\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/WAFV2\\",\\"AllowedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Allowed\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":6,\\"x\\":8,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Blocked Requests\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/WAFV2\\",\\"BlockedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Blocked\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":6,\\"x\\":16,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Blocked Requests (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Blocked (rate)\\",\\"expression\\":\\"100 * (blocked / (allowed + blocked))\\"}],[\\"AWS/WAFV2\\",\\"AllowedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Allowed\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"allowed\\"}],[\\"AWS/WAFV2\\",\\"BlockedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Blocked\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"blocked\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "DummyDashboard", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "DashboardFactorySummaryDashboard5F4BC8C5": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-P14D\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Web Application Firewall **DummyAcl**\\"}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":6,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Allowed Requests\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/WAFV2\\",\\"AllowedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Allowed\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":6,\\"x\\":8,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Blocked Requests\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/WAFV2\\",\\"BlockedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Blocked\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":6,\\"x\\":16,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Blocked Requests (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Blocked (rate)\\",\\"expression\\":\\"100 * (blocked / (allowed + blocked))\\"}],[\\"AWS/WAFV2\\",\\"AllowedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Allowed\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"allowed\\"}],[\\"AWS/WAFV2\\",\\"BlockedRequests\\",\\"Rule\\",\\"ALL\\",{\\"label\\":\\"Blocked\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"blocked\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "DummyDashboard-Summary", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "DummyAcl": Object { + "Properties": Object { + "DefaultAction": Object { + "Allow": Object {}, + }, + "Scope": "REGIONAL", + "VisibilityConfig": Object { + "CloudWatchMetricsEnabled": true, + "MetricName": "DummyMetricName", + "SampledRequestsEnabled": true, + }, + }, + "Type": "AWS::WAFv2::WebACL", + }, + }, +} +`; diff --git a/test/monitoring/aws-wafv2/WafV2Monitoring.test.ts b/test/monitoring/aws-wafv2/WafV2Monitoring.test.ts new file mode 100644 index 00000000..9f9da4c6 --- /dev/null +++ b/test/monitoring/aws-wafv2/WafV2Monitoring.test.ts @@ -0,0 +1,25 @@ +import { Stack } from "monocdk"; +import { Template } from "monocdk/assertions"; +import { CfnWebACL } from "monocdk/aws-wafv2"; + +import { WafV2Monitoring } from "../../../lib"; +import { TestMonitoringScope } from "../TestMonitoringScope"; + +test("snapshot test: no alarms", () => { + const stack = new Stack(); + const acl = new CfnWebACL(stack, "DummyAcl", { + defaultAction: { allow: {} }, + scope: "REGIONAL", + visibilityConfig: { + sampledRequestsEnabled: true, + cloudWatchMetricsEnabled: true, + metricName: "DummyMetricName", + }, + }); + + const scope = new TestMonitoringScope(stack, "Scope"); + + new WafV2Monitoring(scope, { acl }); + + expect(Template.fromStack(stack)).toMatchSnapshot(); +}); diff --git a/test/monitoring/aws-wafv2/__snapshots__/WafV2Monitoring.test.ts.snap b/test/monitoring/aws-wafv2/__snapshots__/WafV2Monitoring.test.ts.snap new file mode 100644 index 00000000..482fbb25 --- /dev/null +++ b/test/monitoring/aws-wafv2/__snapshots__/WafV2Monitoring.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test: no alarms 1`] = ` +Object { + "Resources": Object { + "DummyAcl": Object { + "Properties": Object { + "DefaultAction": Object { + "Allow": Object {}, + }, + "Scope": "REGIONAL", + "VisibilityConfig": Object { + "CloudWatchMetricsEnabled": true, + "MetricName": "DummyMetricName", + "SampledRequestsEnabled": true, + }, + }, + "Type": "AWS::WAFv2::WebACL", + }, + }, +} +`;