Skip to content

Commit

Permalink
feat(waf): add basic WAF monitoring (#89)
Browse files Browse the repository at this point in the history
Related #76 

---

_By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_
  • Loading branch information
voho authored Mar 28, 2022
1 parent fff9d5d commit e413724
Show file tree
Hide file tree
Showing 13 changed files with 884 additions and 1 deletion.
528 changes: 527 additions & 1 deletion API.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
14 changes: 14 additions & 0 deletions lib/facade/MonitoringAspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
8 changes: 8 additions & 0 deletions lib/facade/MonitoringFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ import {
getQueueProcessingFargateServiceMonitoring,
SyntheticsCanaryMonitoringProps,
SyntheticsCanaryMonitoring,
WafV2MonitoringProps,
WafV2Monitoring,
} from "../monitoring";
import { MonitoringAspect, MonitoringAspectProps } from "./MonitoringAspect";

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions lib/facade/aspect-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
SqsQueueMonitoringOptions,
StepFunctionMonitoringOptions,
SyntheticsCanaryMonitoringOptions,
WafV2MonitoringOptions,
} from "../monitoring";

export interface MonitoringAspectType<T> {
Expand Down Expand Up @@ -66,4 +67,5 @@ export interface MonitoringAspectProps {
readonly sqs?: MonitoringAspectType<SqsQueueMonitoringOptions>;
readonly stepFunctions?: MonitoringAspectType<StepFunctionMonitoringOptions>;
readonly syntheticsCanaries?: MonitoringAspectType<SyntheticsCanaryMonitoringOptions>;
readonly webApplicationFirewallAclV2?: MonitoringAspectType<WafV2MonitoringOptions>;
}
60 changes: 60 additions & 0 deletions lib/monitoring/aws-wafv2/WafV2MetricFactory.ts
Original file line number Diff line number Diff line change
@@ -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)"
);
}
}
119 changes: 119 additions & 0 deletions lib/monitoring/aws-wafv2/WafV2Monitoring.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
2 changes: 2 additions & 0 deletions lib/monitoring/aws-wafv2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./WafV2MetricFactory";
export * from "./WafV2Monitoring";
1 change: 1 addition & 0 deletions lib/monitoring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
23 changes: 23 additions & 0 deletions test/facade/MonitoringAspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
});
});
80 changes: 80 additions & 0 deletions test/facade/__snapshots__/MonitoringAspect.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e413724

Please sign in to comment.