Skip to content

Commit e413724

Browse files
authored
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_
1 parent fff9d5d commit e413724

File tree

13 files changed

+884
-1
lines changed

13 files changed

+884
-1
lines changed

API.md

Lines changed: 527 additions & 1 deletion
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ You can also browse the documentation at https://constructs.dev/packages/cdk-mon
9090
| AWS SNS Topic (`.monitorSnsTopic()`) | Message count, size, failed notifications | Failed notifications | |
9191
| AWS SQS Queue (`.monitorSqsQueue()`, `.monitorSqsQueueWithDlq()`) | Message count, age, size | Message count, age, DLQ incoming messages | |
9292
| AWS Step Functions (`.monitorStepFunction()`, `.monitorStepFunctionActivity()`, `monitorStepFunctionLambdaIntegration()`, `.monitorStepFunctionServiceIntegration()`) | Execution count and breakdown per state | Duration, failed, failed rate, aborted, throttled, timed out executions | |
93+
| AWS Web Application Firewall (`.monitorWebApplicationFirewallAcl()`) | Allowed/blocked requests | | |
9394
| CloudWatch Logs (`.monitorLog()`) | Patterns present in the log group | | |
9495
| Custom metrics (`.monitorCustom()`) | Addition of custom metrics into the dashboard (each group is a widget) | | Supports anomaly detection |
9596

lib/facade/MonitoringAspect.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as sns from "monocdk/aws-sns";
2222
import * as sqs from "monocdk/aws-sqs";
2323
import * as stepfunctions from "monocdk/aws-stepfunctions";
2424
import * as synthetics from "monocdk/aws-synthetics";
25+
import * as wafv2 from "monocdk/aws-wafv2";
2526

2627
import { ElastiCacheClusterType } from "../monitoring";
2728
import { MonitoringAspectProps, MonitoringAspectType } from "./aspect-types";
@@ -64,6 +65,7 @@ export class MonitoringAspect implements IAspect {
6465
this.monitorSqs(node);
6566
this.monitorStepFunctions(node);
6667
this.monitorSyntheticsCanaries(node);
68+
this.monitorWebApplicationFirewallV2Acls(node);
6769

6870
if (!this.addedNodeIndependentMonitoringToScope) {
6971
this.addedNodeIndependentMonitoringToScope = true;
@@ -365,6 +367,18 @@ export class MonitoringAspect implements IAspect {
365367
});
366368
}
367369
}
370+
371+
private monitorWebApplicationFirewallV2Acls(node: IConstruct) {
372+
const [isEnabled, props] = this.getMonitoringDetails(
373+
this.props.webApplicationFirewallAclV2
374+
);
375+
if (isEnabled && node instanceof wafv2.CfnWebACL) {
376+
this.monitoringFacade.monitorWebApplicationFirewallAclV2({
377+
acl: node,
378+
...props,
379+
});
380+
}
381+
}
368382
}
369383

370384
export * from "./aspect-types";

lib/facade/MonitoringFacade.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ import {
103103
getQueueProcessingFargateServiceMonitoring,
104104
SyntheticsCanaryMonitoringProps,
105105
SyntheticsCanaryMonitoring,
106+
WafV2MonitoringProps,
107+
WafV2Monitoring,
106108
} from "../monitoring";
107109
import { MonitoringAspect, MonitoringAspectProps } from "./MonitoringAspect";
108110

@@ -613,6 +615,12 @@ export class MonitoringFacade extends MonitoringScope {
613615
return this;
614616
}
615617

618+
monitorWebApplicationFirewallAclV2(props: WafV2MonitoringProps) {
619+
const segment = new WafV2Monitoring(this, props);
620+
this.addSegment(segment, props);
621+
return this;
622+
}
623+
616624
monitorBilling(props?: BillingMonitoringProps) {
617625
const segment = new BillingMonitoring(this, props ?? {});
618626
this.addSegment(segment, props);

lib/facade/aspect-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
SqsQueueMonitoringOptions,
2525
StepFunctionMonitoringOptions,
2626
SyntheticsCanaryMonitoringOptions,
27+
WafV2MonitoringOptions,
2728
} from "../monitoring";
2829

2930
export interface MonitoringAspectType<T> {
@@ -66,4 +67,5 @@ export interface MonitoringAspectProps {
6667
readonly sqs?: MonitoringAspectType<SqsQueueMonitoringOptions>;
6768
readonly stepFunctions?: MonitoringAspectType<StepFunctionMonitoringOptions>;
6869
readonly syntheticsCanaries?: MonitoringAspectType<SyntheticsCanaryMonitoringOptions>;
70+
readonly webApplicationFirewallAclV2?: MonitoringAspectType<WafV2MonitoringOptions>;
6971
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { DimensionHash } from "monocdk/aws-cloudwatch";
2+
import { CfnWebACL } from "monocdk/aws-wafv2";
3+
import { MetricFactory, MetricStatistic } from "../../common";
4+
5+
const MetricNamespace = "AWS/WAFV2";
6+
const AllRulesDimensionValue = "ALL";
7+
8+
export interface WafV2MetricFactoryProps {
9+
readonly region?: string;
10+
readonly acl: CfnWebACL;
11+
}
12+
13+
/**
14+
* https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html
15+
*/
16+
export class WafV2MetricFactory {
17+
protected readonly metricFactory: MetricFactory;
18+
protected readonly dimensions: DimensionHash;
19+
20+
constructor(metricFactory: MetricFactory, props: WafV2MetricFactoryProps) {
21+
this.metricFactory = metricFactory;
22+
this.dimensions = {
23+
Rule: AllRulesDimensionValue,
24+
WebACL: props.acl.name,
25+
};
26+
}
27+
28+
metricAllowedRequests() {
29+
return this.metricFactory.createMetric(
30+
"AllowedRequests",
31+
MetricStatistic.SUM,
32+
"Allowed",
33+
this.dimensions,
34+
undefined,
35+
MetricNamespace
36+
);
37+
}
38+
39+
metricBlockedRequests() {
40+
return this.metricFactory.createMetric(
41+
"BlockedRequests",
42+
MetricStatistic.SUM,
43+
"Blocked",
44+
this.dimensions,
45+
undefined,
46+
MetricNamespace
47+
);
48+
}
49+
50+
metricBlockedRequestsRate() {
51+
return this.metricFactory.createMetricMath(
52+
"100 * (blocked / (allowed + blocked))",
53+
{
54+
allowed: this.metricAllowedRequests(),
55+
blocked: this.metricBlockedRequests(),
56+
},
57+
"Blocked (rate)"
58+
);
59+
}
60+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { GraphWidget, IWidget } from "monocdk/aws-cloudwatch";
2+
import {
3+
BaseMonitoringProps,
4+
CountAxisFromZero,
5+
DefaultGraphWidgetHeight,
6+
DefaultSummaryWidgetHeight,
7+
MetricWithAlarmSupport,
8+
Monitoring,
9+
MonitoringScope,
10+
RateAxisFromZero,
11+
ThirdWidth,
12+
} from "../../common";
13+
import {
14+
MonitoringHeaderWidget,
15+
MonitoringNamingStrategy,
16+
} from "../../dashboard";
17+
import {
18+
WafV2MetricFactory,
19+
WafV2MetricFactoryProps,
20+
} from "./WafV2MetricFactory";
21+
22+
export interface WafV2MonitoringOptions extends BaseMonitoringProps {}
23+
24+
export interface WafV2MonitoringProps
25+
extends WafV2MetricFactoryProps,
26+
WafV2MonitoringOptions {}
27+
28+
/**
29+
* Monitoring for AWS Web Application Firewall.
30+
*
31+
* @see https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html
32+
*/
33+
export class WafV2Monitoring extends Monitoring {
34+
protected readonly humanReadableName: string;
35+
36+
protected readonly allowedRequestsMetric: MetricWithAlarmSupport;
37+
protected readonly blockedRequestsMetric: MetricWithAlarmSupport;
38+
protected readonly blockedRequestsRateMetric: MetricWithAlarmSupport;
39+
40+
constructor(scope: MonitoringScope, props: WafV2MonitoringProps) {
41+
super(scope, props);
42+
43+
const namingStrategy = new MonitoringNamingStrategy({
44+
...props,
45+
namedConstruct: props.acl,
46+
});
47+
this.humanReadableName = namingStrategy.resolveHumanReadableName();
48+
49+
const metricFactory = new WafV2MetricFactory(
50+
scope.createMetricFactory(),
51+
props
52+
);
53+
54+
this.allowedRequestsMetric = metricFactory.metricAllowedRequests();
55+
this.blockedRequestsMetric = metricFactory.metricBlockedRequests();
56+
this.blockedRequestsRateMetric = metricFactory.metricBlockedRequestsRate();
57+
}
58+
59+
summaryWidgets(): IWidget[] {
60+
return [
61+
this.createTitleWidget(),
62+
this.createAllowedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight),
63+
this.createBlockedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight),
64+
this.createBlockedRequestsRateWidget(
65+
ThirdWidth,
66+
DefaultSummaryWidgetHeight
67+
),
68+
];
69+
}
70+
71+
widgets(): IWidget[] {
72+
return [
73+
this.createTitleWidget(),
74+
this.createAllowedRequestsWidget(ThirdWidth, DefaultGraphWidgetHeight),
75+
this.createBlockedRequestsWidget(ThirdWidth, DefaultSummaryWidgetHeight),
76+
this.createBlockedRequestsRateWidget(
77+
ThirdWidth,
78+
DefaultSummaryWidgetHeight
79+
),
80+
];
81+
}
82+
83+
protected createTitleWidget() {
84+
return new MonitoringHeaderWidget({
85+
family: "Web Application Firewall",
86+
title: this.humanReadableName,
87+
});
88+
}
89+
90+
protected createAllowedRequestsWidget(width: number, height: number) {
91+
return new GraphWidget({
92+
width,
93+
height,
94+
title: "Allowed Requests",
95+
left: [this.allowedRequestsMetric],
96+
leftYAxis: CountAxisFromZero,
97+
});
98+
}
99+
100+
protected createBlockedRequestsWidget(width: number, height: number) {
101+
return new GraphWidget({
102+
width,
103+
height,
104+
title: "Blocked Requests",
105+
left: [this.blockedRequestsMetric],
106+
leftYAxis: CountAxisFromZero,
107+
});
108+
}
109+
110+
protected createBlockedRequestsRateWidget(width: number, height: number) {
111+
return new GraphWidget({
112+
width,
113+
height,
114+
title: "Blocked Requests (rate)",
115+
left: [this.blockedRequestsRateMetric],
116+
leftYAxis: RateAxisFromZero,
117+
});
118+
}
119+
}

lib/monitoring/aws-wafv2/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./WafV2MetricFactory";
2+
export * from "./WafV2Monitoring";

lib/monitoring/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export * from "./aws-sns";
2424
export * from "./aws-sqs";
2525
export * from "./aws-step-functions";
2626
export * from "./aws-synthetics";
27+
export * from "./aws-wafv2";
2728
export * from "./custom";

test/facade/MonitoringAspect.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import * as sns from "monocdk/aws-sns";
2626
import * as sqs from "monocdk/aws-sqs";
2727
import * as stepfunctions from "monocdk/aws-stepfunctions";
2828
import * as synthetics from "monocdk/aws-synthetics";
29+
import { CfnWebACL } from "monocdk/aws-wafv2";
2930

3031
import {
3132
DefaultDashboardFactory,
@@ -535,4 +536,26 @@ describe("MonitoringAspect", () => {
535536
// THEN
536537
expect(Template.fromStack(stack)).toMatchSnapshot();
537538
});
539+
540+
test("WAF v2", () => {
541+
// GIVEN
542+
const stack = new Stack();
543+
const facade = createDummyMonitoringFacade(stack);
544+
545+
new CfnWebACL(stack, "DummyAcl", {
546+
defaultAction: { allow: {} },
547+
scope: "REGIONAL",
548+
visibilityConfig: {
549+
sampledRequestsEnabled: true,
550+
cloudWatchMetricsEnabled: true,
551+
metricName: "DummyMetricName",
552+
},
553+
});
554+
555+
// WHEN
556+
facade.monitorScope(stack, defaultAspectProps);
557+
558+
// THEN
559+
expect(Template.fromStack(stack)).toMatchSnapshot();
560+
});
538561
});

0 commit comments

Comments
 (0)