diff --git a/lib/ci-stack.ts b/lib/ci-stack.ts index c906ce9e..f09d6c69 100644 --- a/lib/ci-stack.ts +++ b/lib/ci-stack.ts @@ -25,6 +25,7 @@ import { RunAdditionalCommands } from './compute/run-additional-commands'; import { JenkinsMonitoring } from './monitoring/ci-alarms'; import { JenkinsExternalLoadBalancer } from './network/ci-external-load-balancer'; import { JenkinsSecurityGroups } from './security/ci-security-groups'; +import { JenkinsWAF } from './security/waf'; export interface CIStackProps extends StackProps { /** Should the Jenkins use https */ @@ -202,6 +203,10 @@ export class CIStack extends Stack { accessLogBucket: auditloggingS3Bucket.bucket, }); + const waf = new JenkinsWAF(this, { + loadBalancer: externalLoadBalancer.loadBalancer, + }); + const artifactBucket = new Bucket(this, 'BuildBucket'); this.monitoring = new JenkinsMonitoring(this, externalLoadBalancer, mainJenkinsNode); diff --git a/lib/security/waf.ts b/lib/security/waf.ts new file mode 100644 index 00000000..745a0f93 --- /dev/null +++ b/lib/security/waf.ts @@ -0,0 +1,120 @@ +import { Stack, StackProps } from 'aws-cdk-lib'; +import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { CfnWebACL, CfnWebACLAssociation, CfnWebACLAssociationProps } from 'aws-cdk-lib/aws-wafv2'; +import { Construct } from 'constructs'; + +interface WafRule { + name: string; + rule: CfnWebACL.RuleProperty; +} + +const awsManagedRules: WafRule[] = [ + // AWS IP Reputation list includes known malicious actors/bots and is regularly updated + { + name: 'AWS-AWSManagedRulesAmazonIpReputationList', + rule: { + name: 'AWS-AWSManagedRulesAmazonIpReputationList', + priority: 0, + statement: { + managedRuleGroupStatement: { + vendorName: 'AWS', + name: 'AWSManagedRulesAmazonIpReputationList', + }, + }, + overrideAction: { + none: {}, + }, + visibilityConfig: { + sampledRequestsEnabled: true, + cloudWatchMetricsEnabled: true, + metricName: 'AWSManagedRulesAmazonIpReputationList', + }, + }, + }, + // Blocks common SQL Injection + { + name: 'AWS-AWSManagedRulesSQLiRuleSet', + rule: { + name: 'AWS-AWSManagedRulesSQLiRuleSet', + priority: 1, + statement: { + managedRuleGroupStatement: { + vendorName: 'AWS', + name: 'AWSManagedRulesSQLiRuleSet', + excludedRules: [], + }, + }, + visibilityConfig: { + sampledRequestsEnabled: true, + cloudWatchMetricsEnabled: true, + metricName: 'AWS-AWSManagedRulesSQLiRuleSet', + }, + overrideAction: { + none: {}, + }, + }, + }, + // Block request patterns associated with the exploitation of vulnerabilities specific to WordPress sites. + { + name: 'AWS-AWSManagedRulesWordPressRuleSet', + rule: { + name: 'AWS-AWSManagedRulesWordPressRuleSet', + priority: 2, + visibilityConfig: { + sampledRequestsEnabled: true, + cloudWatchMetricsEnabled: true, + metricName: 'AWS-AWSManagedRulesWordPressRuleSet', + }, + overrideAction: { + none: {}, + }, + statement: { + managedRuleGroupStatement: { + vendorName: 'AWS', + name: 'AWSManagedRulesWordPressRuleSet', + excludedRules: [], + }, + }, + }, + }, +]; + +export class WAF extends CfnWebACL { + constructor(scope: Construct, id: string) { + super(scope, id, { + defaultAction: { allow: {} }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: 'jenkins-WAF', + sampledRequestsEnabled: true, + }, + scope: 'REGIONAL', + name: 'jenkins-WAF', + rules: awsManagedRules.map((wafRule) => wafRule.rule), + }); + } +} + +export class WebACLAssociation extends CfnWebACLAssociation { + constructor(scope: Construct, id: string, props: CfnWebACLAssociationProps) { + super(scope, id, { + resourceArn: props.resourceArn, + webAclArn: props.webAclArn, + }); + } +} + +export interface WafProps extends StackProps{ + loadBalancer: ApplicationLoadBalancer +} + +export class JenkinsWAF { + constructor(stack: Stack, props: WafProps) { + const waf = new WAF(stack, 'WAFv2'); + // Create an association with the alb + new WebACLAssociation(stack, 'wafALBassociation', { + resourceArn: props.loadBalancer.loadBalancerArn, + webAclArn: waf.attrArn, + }); + } +} diff --git a/test/ci-stack.test.ts b/test/ci-stack.test.ts index 85b1aa0b..7d42c8ad 100644 --- a/test/ci-stack.test.ts +++ b/test/ci-stack.test.ts @@ -370,3 +370,114 @@ test('LoadBalancer Access Logging', () => { }, }); }); + +test('WAF rules', () => { + const app = new App({ + context: { + useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0', + }, + }); + + // WHEN + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACL', { + DefaultAction: { + Allow: {}, + }, + Scope: 'REGIONAL', + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: 'jenkins-WAF', + SampledRequestsEnabled: true, + }, + Name: 'jenkins-WAF', + Rules: [ + { + Name: 'AWS-AWSManagedRulesAmazonIpReputationList', + OverrideAction: { + None: {}, + }, + Priority: 0, + Statement: { + ManagedRuleGroupStatement: { + Name: 'AWSManagedRulesAmazonIpReputationList', + VendorName: 'AWS', + }, + }, + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: 'AWSManagedRulesAmazonIpReputationList', + SampledRequestsEnabled: true, + }, + }, + { + Name: 'AWS-AWSManagedRulesSQLiRuleSet', + OverrideAction: { + None: {}, + }, + Priority: 1, + Statement: { + ManagedRuleGroupStatement: { + ExcludedRules: [], + Name: 'AWSManagedRulesSQLiRuleSet', + VendorName: 'AWS', + }, + }, + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: 'AWS-AWSManagedRulesSQLiRuleSet', + SampledRequestsEnabled: true, + }, + }, + { + Name: 'AWS-AWSManagedRulesWordPressRuleSet', + OverrideAction: { + None: {}, + }, + Priority: 2, + Statement: { + ManagedRuleGroupStatement: { + ExcludedRules: [], + Name: 'AWSManagedRulesWordPressRuleSet', + VendorName: 'AWS', + }, + }, + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: 'AWS-AWSManagedRulesWordPressRuleSet', + SampledRequestsEnabled: true, + }, + }, + ], + }); +}); + +test('Test WAF association with ALB', () => { + const app = new App({ + context: { + useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0', + }, + }); + + // WHEN + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACLAssociation', { + ResourceArn: { + Ref: 'JenkinsALB9F3D5428', + }, + WebACLArn: { + 'Fn::GetAtt': [ + 'WAFv2', + 'Arn', + ], + }, + }); +});