From 7e33f6219d2c7decc47ba3da39923ca7c57658ab Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 7 Jun 2021 18:27:25 +0200 Subject: [PATCH 01/14] feat(route53resolver): DNS Firewall Add L2s for `FirewallDomainList`, `FirewallRuleGroup` and `FirewallRuleGroupAssociation`. --- .../@aws-cdk/aws-route53resolver/README.md | 87 +++++- .../lib/firewall-domain-list.ts | 153 ++++++++++ .../lib/firewall-rule-group-association.ts | 129 ++++++++ .../lib/firewall-rule-group.ts | 277 ++++++++++++++++++ .../@aws-cdk/aws-route53resolver/lib/index.ts | 4 + .../@aws-cdk/aws-route53resolver/package.json | 12 +- .../test/firewall-domain-list.test.ts | 41 +++ .../test/firewall-rule-group.test.ts | 137 +++++++++ .../test/integ.firewall.expected.json | 264 +++++++++++++++++ .../test/integ.firewall.ts | 41 +++ .../test/route53resolver.test.ts | 6 - 11 files changed, 1143 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json create mode 100644 packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts delete mode 100644 packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index 9cf4ab7748b3d..ce28662f6d33e 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -9,10 +9,95 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- +## DNS Firewall + +With Route 53 Resolver DNS Firewall, you can filter and regulate outbound DNS traffic for your +virtual private connections (VPCs). To do this, you create reusable collections of filtering rules +in DNS Firewall rule groups and associate the rule groups to your VPC. + +DNS Firewall provides protection for outbound DNS requests from your VPCs. These requests route +through Resolver for domain name resolution. A primary use of DNS Firewall protections is to help +prevent DNS exfiltration of your data. DNS exfiltration can happen when a bad actor compromises +an application instance in your VPC and then uses DNS lookup to send data out of the VPC to a domain +that they control. With DNS Firewall, you can monitor and control the domains that your applications +can query. You can deny access to the domains that you know to be bad and allow all other queries +to pass through. Alternately, you can deny access to all domains except for the ones that you +explicitly trust. + +### Domain lists + +Domain lists can be created using a list of strings or text file stored in Amazon S3: + +```ts +const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), +}); + +const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { + domains: route53resolver.FirewallDomains.fromS3Uri('s3://bucket/prefix/object'), +}) +``` + +The file must be a text file and must contain a single domain per line. + +Use `FirewallDomainList.fromFirewallDomainListId()` to import an existing or [AWS managed domain list](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html): + +```ts +// AWSManagedDomainsMalwareDomainList in us-east-1 +const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId(this, 'Malware', 'rslvr-fdl-2c46f2ecbfec4dcc'); +``` + +### Rule group + +Create a rule group: + +```ts +new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList: myBlockList, + action: FirewallRuleAction.block(), // defaults to NODATA + }, + ], +}); +``` + +Rules can be added at construction time or using `addRule()`: + +```ts +ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + // block and reply with NXDOMAIN + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), +}); + +ruleGroup.addRule({ + priority: 20, + firewallDomainList: blockList, + // block and override DNS response with a custom domain + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), +}); +``` + +Use `associate()` to associate a rule group with a VPC: + ```ts -import * as route53resolver from '@aws-cdk/aws-route53resolver'; +ruleGroup.associate({ + priority: 101, + vpc: myVpc, +}) ``` diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts new file mode 100644 index 0000000000000..6d6e6c34c78a8 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -0,0 +1,153 @@ +import { IResource, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnFirewallDomainList } from './route53resolver.generated'; + +/** + * A Firewall Domain List + */ +export interface IFirewallDomainList extends IResource { + /** + * The ID of the domain list + * + * @attribute + */ + readonly firewallDomainListId: string; +} + +/** + * Properties for a Firewall Domain List + */ +export interface FirewallDomainListProps { + /** + * A name for the domain list + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of domains + */ + readonly domains: FirewallDomains; +} + +/** + * A list of domains + */ +export abstract class FirewallDomains { + /** + * Firewall domains created from a list + * + * @param list the list of domains + */ + public static fromList(list: string[]): FirewallDomains { + return { list }; + } + + /** + * Firewall domains created from a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * + * @param s3Uri S3 bucket uri (s3://bucket/prefix/objet). + */ + public static fromS3Uri(s3Uri: string): FirewallDomains { + if (!Token.isUnresolved(s3Uri) && !s3Uri.startsWith('s3://')) { + throw new Error(`The S3 URI must start with s3://, got ${s3Uri}`); + } + + return { s3Uri }; + } + + /** S3 bucket URI of text file with domain list */ + public abstract s3Uri?: string; + + /** List of domains */ + public abstract readonly list?: string[]; +} + +/** + * A Firewall Domain List + */ +export class FirewallDomainList extends Resource implements IFirewallDomainList { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallDomainListId(scope: Construct, id: string, firewallDomainListId: string): IFirewallDomainList { + class Import extends Resource implements IFirewallDomainList { + public readonly firewallDomainListId = firewallDomainListId; + } + return new Import(scope, id); + } + + public readonly firewallDomainListId: string; + + /** + * The ARN (Amazon Resource Name) of the domain list + * @attribute + */ + public readonly firewallDomainListArn: string; + + /** + * The date and time that the domain list was created + * @attribute + */ + public readonly firewallDomainListCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallDomainListCreatorRequestId: string; + + /** + * The number of domains in the list + * @attribute + */ + public readonly firewallDomainListDomainCount: number; + + /** + * The owner of the list, used only for lists that are not managed by you. + * For example, the managed domain list `AWSManagedDomainsMalwareDomainList` + * has the managed owner name `Route 53 Resolver DNS Firewall`. + * @attribute + */ + public readonly firewallDomainListManagedOwnerName: string; + + /** + * The date and time that the domain list was last modified + * @attribute + */ + public readonly firewallDomainListModificationTime: string; + + /** + * The status of the domain list + * @attribute + */ + public readonly firewallDomainListStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallDomainListStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallDomainListProps) { + super(scope, id); + + const domainList = new CfnFirewallDomainList(this, 'Resource', { + name: props.name, + domainFileUrl: props.domains.s3Uri, + domains: props.domains.list, + }); + + this.firewallDomainListId = domainList.attrId; + this.firewallDomainListArn = domainList.attrArn; + this.firewallDomainListCreationTime = domainList.attrCreationTime; + this.firewallDomainListCreatorRequestId = domainList.attrCreatorRequestId; + this.firewallDomainListDomainCount = domainList.attrDomainCount; + this.firewallDomainListManagedOwnerName = domainList.attrManagedOwnerName; + this.firewallDomainListModificationTime = domainList.attrModificationTime; + this.firewallDomainListStatus = domainList.attrStatus; + this.firewallDomainListStatusMessage = domainList.attrStatusMessage; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts new file mode 100644 index 0000000000000..f920003d75c67 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts @@ -0,0 +1,129 @@ +import { IVpc } from '@aws-cdk/aws-ec2'; +import { Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallRuleGroup } from './firewall-rule-group'; +import { CfnFirewallRuleGroupAssociation } from './route53resolver.generated'; + +/** + * Options for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationOptions { + /** + * If enabled, this setting disallows modification or removal of the + * association, to help prevent against accidentally altering DNS firewall + * protections. + * + * @default true + */ + readonly mutationProtection?: boolean; + + /** + * The name of the association + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * The setting that determines the processing order of the rule group among + * the rule groups that are associated with a single VPC. DNS Firewall filters VPC + * traffic starting from rule group with the lowest numeric priority setting. + * + * This value must be greater than 100 and less than 9,000 + */ + readonly priority: number; + + /** + * The VPC that to associate with the rule group. + */ + readonly vpc: IVpc; +} + +/** + * Properties for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationProps extends FirewallRuleGroupAssociationOptions { + /** + * The firewall rule group which must be associated + */ + readonly firewallRuleGroup: IFirewallRuleGroup; +} + +/** + * A Firewall Rule Group Association + */ +export class FirewallRuleGroupAssociation extends Resource { + /** + * The ARN (Amazon Resource Name) of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationArn: string; + + /** + * The date and time that the association was created + * @attribute + */ + public readonly firewallRuleGroupAssociationCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupAssociationCreatorRequestId: string; + + /** + * The ID of the association + * + * @attribute + */ + public readonly firewallRuleGroupAssociationId: string; + + /** + * The owner of the association, used only for lists that are not managed by you. + * If you use AWS Firewall Manager to manage your firewallls from DNS Firewall, + * then this reports Firewall Manager as the managed owner. + * @attribute + */ + public readonly firewallRuleGroupAssociationManagedOwnerName: string; + + /** + * The date and time that the association was last modified + * @attribute + */ + public readonly firewallRuleGroupAssociationModificationTime: string; + + /** + * The status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatus: string; + + /** + * Additional information about the status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupAssociationProps) { + super(scope, id); + + if (!Token.isUnresolved(props.priority) && (props.priority <= 100 || props.priority >= 9000)) { + throw new Error(`Priority must be greater than 100 and less than 9000, got ${props.priority}`); + } + + const association = new CfnFirewallRuleGroupAssociation(this, 'Resource', { + firewallRuleGroupId: props.firewallRuleGroup.firewallRuleGroupId, + priority: props.priority, + vpcId: props.vpc.vpcId, + }); + + this.firewallRuleGroupAssociationArn = association.attrArn; + this.firewallRuleGroupAssociationCreationTime = association.attrCreationTime; + this.firewallRuleGroupAssociationCreatorRequestId = association.attrCreatorRequestId; + this.firewallRuleGroupAssociationId = association.attrId; + this.firewallRuleGroupAssociationManagedOwnerName = association.attrManagedOwnerName; + this.firewallRuleGroupAssociationModificationTime = association.attrModificationTime; + this.firewallRuleGroupAssociationStatus = association.attrStatus; + this.firewallRuleGroupAssociationStatusMessage = association.attrStatusMessage; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts new file mode 100644 index 0000000000000..5e2a4d3c02157 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts @@ -0,0 +1,277 @@ +import { Duration, IResource, Lazy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallDomainList } from './firewall-domain-list'; +import { FirewallRuleGroupAssociation, FirewallRuleGroupAssociationOptions } from './firewall-rule-group-association'; +import { CfnFirewallRuleGroup } from './route53resolver.generated'; + +/** + * A Firewall Rule Group + */ +export interface IFirewallRuleGroup extends IResource { + /** + * The ID of the rule group + * + * @attribute + */ + readonly firewallRuleGroupId: string; +} + +/** + * Properties for a Firewall Rule Group + */ +export interface FirewallRuleGroupProps { + /** + * The name of the rule group. + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of rules for this group + * + * @default - no rules + */ + readonly rules?: FirewallRule[]; +} + +/** + * A Firewall Rule + */ +export interface FirewallRule { + /** + * The action for this rule + */ + readonly action: FirewallRuleAction; + + /** + * The domain list for this rule + */ + readonly firewallDomainList: IFirewallDomainList; + + /** + * The priority of the rule in the rule group. This value must be unique within + * the rule group. + */ + readonly priority: number; +} + +/** + * A Firewall Rule + */ +export abstract class FirewallRuleAction { + /** + * Permit the request to go through + */ + public static allow(): FirewallRuleAction { + return { action: 'ALLOW' }; + } + + /** + * Permit the request to go through but send an alert to the logs + */ + public static alert(): FirewallRuleAction { + return { action: 'ALERT' }; + } + + /** + * Disallow the request + * + * @param [response=DnsBlockResponse.noData()] The way that you want DNS Firewall to block the request + */ + public static block(response?: DnsBlockResponse): FirewallRuleAction { + return { + action: 'BLOCK', + blockResponse: response ?? DnsBlockResponse.noData(), + }; + } + + /** + * The action that DNS Firewall should take on a DNS query when it matches + * one of the domains in the rule's domain list + */ + public abstract readonly action: string; + + /** + * The way that you want DNS Firewall to block the request + */ + public abstract readonly blockResponse?: DnsBlockResponse; +} + +/** + * The way that you want DNS Firewall to block the request + */ +export abstract class DnsBlockResponse { + /** + * Respond indicating that the query was successful, but no + * response is available for it. + */ + public static noData(): DnsBlockResponse { + return { blockResponse: 'NODATA' }; + } + + /** + * Respond indicating that the domain name that's in the query + * doesn't exist. + */ + public static nxDomain(): DnsBlockResponse { + return { blockResponse: 'NXDOMAIN' }; + } + + /** + * Provides a custom override response to the query + * + * @param domain The custom DNS record to send back in response to the query + * @param [ttl=0] The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public static override(domain: string, ttl?: Duration): DnsBlockResponse { + return { + blockResponse: 'OVERRIDE', + blockOverrideDnsType: 'CNAME', + blockOverrideDomain: domain, + blockOverrideTtl: ttl ?? Duration.seconds(0), + }; + } + + /** The DNS record's type */ + public abstract readonly blockOverrideDnsType?: string; + + /** The custom DNS record to send back in response to the query */ + public abstract readonly blockOverrideDomain?: string; + + /** + * The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public abstract readonly blockOverrideTtl?: Duration; + + /** The way that you want DNS Firewall to block the request */ + public abstract readonly blockResponse?: string; +} + +/** + * A Firewall Rule Group + */ +export class FirewallRuleGroup extends Resource implements IFirewallRuleGroup { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallRuleGroupId(scope: Construct, id: string, firewallRuleGroupId: string): IFirewallRuleGroup { + class Import extends Resource implements IFirewallRuleGroup { + public readonly firewallRuleGroupId = firewallRuleGroupId; + } + return new Import(scope, id); + } + + public readonly firewallRuleGroupId: string; + + /** + * The ARN (Amazon Resource Name) of the rule group + * @attribute + */ + public readonly firewallRuleGroupArn: string; + + /** + * The date and time that the rule group was created + * @attribute + */ + public readonly firewallRuleGroupCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupCreatorRequestId: string; + + /** + * The date and time that the rule group was last modified + * @attribute + */ + public readonly firewallRuleGroupModificationTime: string; + + /** + * The AWS account ID for the account that created the rule group + * @attribute + */ + public readonly firewallRuleGroupOwnerId: string; + + /** + * The number of rules in the rule group + * @attribute + */ + public readonly firewallRuleGroupRuleCount: number; + + /** + * Whether the rule group is shared with other AWS accounts, + * or was shared with the current account by another AWS account + * @attribute + */ + public readonly firewallRuleGroupShareStatus: string; + + /** + * The status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatusMessage: string; + + private readonly rules: FirewallRule[]; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupProps = {}) { + super(scope, id); + + this.rules = props.rules ?? []; + + const ruleGroup = new CfnFirewallRuleGroup(this, 'Resource', { + name: props.name, + firewallRules: Lazy.any({ produce: () => this.rules.map(renderRule) }), + }); + + this.firewallRuleGroupId = ruleGroup.attrId; + this.firewallRuleGroupArn= ruleGroup.attrArn; + this.firewallRuleGroupCreationTime = ruleGroup.attrCreationTime; + this.firewallRuleGroupCreatorRequestId = ruleGroup.attrCreatorRequestId; + this.firewallRuleGroupModificationTime = ruleGroup.attrModificationTime; + this.firewallRuleGroupOwnerId = ruleGroup.attrOwnerId; + this.firewallRuleGroupRuleCount = ruleGroup.attrRuleCount; + this.firewallRuleGroupShareStatus = ruleGroup.attrShareStatus; + this.firewallRuleGroupStatus = ruleGroup.attrStatus; + this.firewallRuleGroupStatusMessage = ruleGroup.attrStatusMessage; + } + + /** + * Adds a rule to this group + */ + public addRule(rule: FirewallRule): FirewallRuleGroup { + this.rules.push(rule); + return this; + } + + /** + * Associates this Firewall Rule Group with a VPC + */ + public associate(id: string, props: FirewallRuleGroupAssociationOptions): FirewallRuleGroupAssociation { + return new FirewallRuleGroupAssociation(this, id, { + ...props, + firewallRuleGroup: this, + }); + } +} + +function renderRule(rule: FirewallRule): CfnFirewallRuleGroup.FirewallRuleProperty { + return { + action: rule.action.action, + firewallDomainListId: rule.firewallDomainList.firewallDomainListId, + priority: rule.priority, + blockOverrideDnsType: rule.action.blockResponse?.blockOverrideDnsType, + blockOverrideDomain: rule.action.blockResponse?.blockOverrideDomain, + blockOverrideTtl: rule.action.blockResponse?.blockOverrideTtl?.toSeconds(), + blockResponse: rule.action.blockResponse?.blockResponse, + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/lib/index.ts b/packages/@aws-cdk/aws-route53resolver/lib/index.ts index 7a6d65433e61f..97baf0759d24a 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/index.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/index.ts @@ -1,2 +1,6 @@ +export * from './firewall-domain-list'; +export * from './firewall-rule-group'; +export * from './firewall-rule-group-association'; + // AWS::Route53Resolver CloudFormation Resources: export * from './route53resolver.generated'; diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index 2f297377c17ab..b575a0f319f59 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -74,15 +74,18 @@ "devDependencies": { "@types/jest": "^26.0.23", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -90,7 +93,14 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallDomainListProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupAssociationProps" + ] + }, "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts new file mode 100644 index 0000000000000..dcfedfcbf8cad --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -0,0 +1,41 @@ +import '@aws-cdk/assert-internal/jest'; +import { Stack } from '@aws-cdk/core'; +import { FirewallDomainList, FirewallDomains } from '../lib'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('domain list from strings', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['first-domain.com', 'second-domain.net']), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + Domains: [ + 'first-domain.com', + 'second-domain.net', + ], + }); +}); + +test('domain list from S3 URI', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Uri('s3://bucket/prefix/object'), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + +test('throws with invalid S3 URI', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Uri('https://invalid/bucket/uri'), + })).toThrow(/The S3 URI must start with s3:\/\//); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts new file mode 100644 index 0000000000000..d83fea3ed11d9 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts @@ -0,0 +1,137 @@ +import '@aws-cdk/assert-internal/jest'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Duration, Stack } from '@aws-cdk/core'; +import { DnsBlockResponse, FirewallDomainList, FirewallRuleAction, FirewallRuleGroup, IFirewallDomainList } from '../lib'; + +let stack: Stack; +let firewallDomainList: IFirewallDomainList; +beforeEach(() => { + stack = new Stack(); + firewallDomainList = FirewallDomainList.fromFirewallDomainListId(stack, 'List', 'domain-list-id'); +}); + +test('basic rule group', () => { + // WHEN + new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(), + }, + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockResponse: 'NODATA', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('use addRule to add rules', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.allow(), + }, + ], + }); + + // WHEN + ruleGroup.addRule({ + priority: 20, + firewallDomainList: FirewallDomainList.fromFirewallDomainListId(stack, 'OtherList', 'other-list-id'), + action: FirewallRuleAction.allow(), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'ALLOW', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + { + Action: 'ALLOW', + FirewallDomainListId: 'other-list-id', + Priority: 20, + }, + ], + }); +}); + +test('rule with response override', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.addRule({ + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(DnsBlockResponse.override('amazon.com', Duration.minutes(5))), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockOverrideDnsType: 'CNAME', + BlockOverrideDomain: 'amazon.com', + BlockOverrideTtl: 300, + BlockResponse: 'OVERRIDE', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('associate rule group with a vpc', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroupAssociation', { + FirewallRuleGroupId: { + 'Fn::GetAtt': [ + 'RuleGroup06BA8844', + 'Id', + ], + }, + Priority: 101, + VpcId: { + Ref: 'Vpc8378EB38', + }, + }); +}); + +test('throws when associating with a priority not between 100-9,000', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // THEN + expect(() => ruleGroup.associate('Association', { + priority: 100, + vpc, + })).toThrow(/Priority must be greater than 100 and less than 9000/); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json new file mode 100644 index 0000000000000..11849dea69327 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json @@ -0,0 +1,264 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "BlockListC03D0423": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "bad-domain.com", + "bot-domain.net" + ] + } + }, + "OverrideListF573FB0F": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "override-domain.com" + ] + } + }, + "RuleGroup06BA8844": { + "Type": "AWS::Route53Resolver::FirewallRuleGroup", + "Properties": { + "FirewallRules": [ + { + "Action": "BLOCK", + "BlockResponse": "NODATA", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "BlockListC03D0423", + "Id" + ] + }, + "Priority": 10 + }, + { + "Action": "BLOCK", + "BlockOverrideDnsType": "CNAME", + "BlockOverrideDomain": "amazon.com", + "BlockOverrideTtl": 0, + "BlockResponse": "OVERRIDE", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "OverrideListF573FB0F", + "Id" + ] + }, + "Priority": 20 + } + ] + } + }, + "RuleGroupAssociation5494BFB1": { + "Type": "AWS::Route53Resolver::FirewallRuleGroupAssociation", + "Properties": { + "FirewallRuleGroupId": { + "Fn::GetAtt": [ + "RuleGroup06BA8844", + "Id" + ] + }, + "Priority": 101, + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts new file mode 100644 index 0000000000000..730ad0f5176a2 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts @@ -0,0 +1,41 @@ +import { Vpc } from '@aws-cdk/aws-ec2'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as route53resolver from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new Vpc(this, 'Vpc', { maxAzs: 1 }); + + const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), + }); + const overrideList = new route53resolver.FirewallDomainList(this, 'OverrideList', { + domains: route53resolver.FirewallDomains.fromList(['override-domain.com']), + }); + + const ruleGroup = new route53resolver.FirewallRuleGroup(this, 'RuleGroup'); + + ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + action: route53resolver.FirewallRuleAction.block(), + }); + ruleGroup.addRule({ + priority: 20, + firewallDomainList: overrideList, + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), + }); + + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-route53-resolver-firewall'); +app.synth(); diff --git a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts b/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts deleted file mode 100644 index c4505ad966984..0000000000000 --- a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert-internal/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); From 93dec854dfa8c8e48ec8a9206e5c9aa9f0faac72 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 10 Jun 2021 16:38:47 +0200 Subject: [PATCH 02/14] FirewallDomains.fromS3() --- .../aws-route53resolver/lib/firewall-domain-list.ts | 13 ++++++++++++- .../test/firewall-domain-list.test.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index 6d6e6c34c78a8..e819b2ad60e8e 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -45,7 +45,7 @@ export abstract class FirewallDomains { } /** - * Firewall domains created from a file stored in Amazon S3. + * Firewall domains created from the URI of a file stored in Amazon S3. * The file must be a text file and must contain a single domain per line. * * @param s3Uri S3 bucket uri (s3://bucket/prefix/objet). @@ -58,6 +58,17 @@ export abstract class FirewallDomains { return { s3Uri }; } + /** + * Firewall domains created from a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * + * @param bucket S3 bucket + * @param key S3 key + */ + public static fromS3(bucket: string, key: string): FirewallDomains { + return this.fromS3Uri(`s3://${bucket}/${key.replace(/^\//, '')}`); + } + /** S3 bucket URI of text file with domain list */ public abstract s3Uri?: string; diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index dcfedfcbf8cad..e042e2507b727 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -34,6 +34,18 @@ test('domain list from S3 URI', () => { }); }); +test('domain list from S3', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3('bucket', 'prefix/object'), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + test('throws with invalid S3 URI', () => { expect(() => new FirewallDomainList(stack, 'List', { domains: FirewallDomains.fromS3Uri('https://invalid/bucket/uri'), From 7a963869163535c7df7bb057ed23ab4a2128d7d8 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Sun, 13 Jun 2021 08:30:29 +0200 Subject: [PATCH 03/14] fromS3 with IBucket --- .../@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts | 5 +++-- packages/@aws-cdk/aws-route53resolver/package.json | 2 ++ .../aws-route53resolver/test/firewall-domain-list.test.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index e819b2ad60e8e..fe376b91800e9 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -1,3 +1,4 @@ +import { IBucket } from '@aws-cdk/aws-s3'; import { IResource, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnFirewallDomainList } from './route53resolver.generated'; @@ -65,8 +66,8 @@ export abstract class FirewallDomains { * @param bucket S3 bucket * @param key S3 key */ - public static fromS3(bucket: string, key: string): FirewallDomains { - return this.fromS3Uri(`s3://${bucket}/${key.replace(/^\//, '')}`); + public static fromS3(bucket: IBucket, key: string): FirewallDomains { + return this.fromS3Uri(bucket.s3UrlForObject(key)); } /** S3 bucket URI of text file with domain list */ diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index b575a0f319f59..bc34b9334d2b8 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -81,11 +81,13 @@ }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index e042e2507b727..0c62cf570ca07 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert-internal/jest'; +import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import { FirewallDomainList, FirewallDomains } from '../lib'; @@ -37,7 +38,7 @@ test('domain list from S3 URI', () => { test('domain list from S3', () => { // WHEN new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromS3('bucket', 'prefix/object'), + domains: FirewallDomains.fromS3(Bucket.fromBucketName(stack, 'Bucket', 'bucket'), 'prefix/object'), }); // THEN From fee2a6daf28796445e9a67ff799c3261612d9cd5 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 18 Jun 2021 14:22:28 +0200 Subject: [PATCH 04/14] FirewallDomains.fromAsset() --- .../lib/firewall-domain-list.ts | 31 +++++++++- .../lib/firewall-rule-group-association.ts | 2 +- .../@aws-cdk/aws-route53resolver/package.json | 2 + .../test/firewall-domain-list.test.ts | 58 ++++++++++++++++++- .../test/firewall-rule-group.test.ts | 2 +- .../test/integ.firewall.expected.json | 57 ++++++++++++++++++ .../test/integ.firewall.ts | 5 ++ 7 files changed, 152 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index fe376b91800e9..bdf22cebab211 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -1,4 +1,6 @@ +import * as path from 'path'; import { IBucket } from '@aws-cdk/aws-s3'; +import { Asset } from '@aws-cdk/aws-s3-assets'; import { IResource, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnFirewallDomainList } from './route53resolver.generated'; @@ -37,7 +39,7 @@ export interface FirewallDomainListProps { */ export abstract class FirewallDomains { /** - * Firewall domains created from a list + * Firewall domains created from a list of domains * * @param list the list of domains */ @@ -48,6 +50,7 @@ export abstract class FirewallDomains { /** * Firewall domains created from the URI of a file stored in Amazon S3. * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. * * @param s3Uri S3 bucket uri (s3://bucket/prefix/objet). */ @@ -62,6 +65,7 @@ export abstract class FirewallDomains { /** * Firewall domains created from a file stored in Amazon S3. * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. * * @param bucket S3 bucket * @param key S3 key @@ -70,6 +74,29 @@ export abstract class FirewallDomains { return this.fromS3Uri(bucket.s3UrlForObject(key)); } + /** + * Firewall domains created from a local disk path to a text file. + * The file must be a text file (`.txt` extension) and must contain a single + * domain per line. + * + * @param assetPath path to the text file + */ + public static fromAsset(scope: Construct, id: string, assetPath: string): FirewallDomains { + // cdk-assets will correctly set the content type for the S3 object + // if the file has the correct extension + if (path.extname(assetPath) !== '.txt') { + throw new Error(`FirewallDomains.fromAsset() expects a file with the .txt extension, got ${assetPath}`); + } + + const asset = new Asset(scope, id, { path: assetPath }); + + if (!asset.isFile) { + throw new Error('FirewallDomains.fromAsset() expects a file'); + } + + return this.fromS3Uri(asset.s3ObjectUrl); + } + /** S3 bucket URI of text file with domain list */ public abstract s3Uri?: string; @@ -162,4 +189,4 @@ export class FirewallDomainList extends Resource implements IFirewallDomainList this.firewallDomainListStatus = domainList.attrStatus; this.firewallDomainListStatusMessage = domainList.attrStatusMessage; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts index f920003d75c67..10281eba1dda1 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts @@ -126,4 +126,4 @@ export class FirewallRuleGroupAssociation extends Resource { this.firewallRuleGroupAssociationStatus = association.attrStatus; this.firewallRuleGroupAssociationStatusMessage = association.attrStatusMessage; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index e1ad13cbec388..1c062b0df6c78 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -84,12 +84,14 @@ "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index 0c62cf570ca07..54837662cab34 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import '@aws-cdk/assert-internal/jest'; import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; @@ -47,8 +48,63 @@ test('domain list from S3', () => { }); }); +test('domain list from asset', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset(stack, 'Domains', path.join(__dirname, 'domains.txt')), + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: { + 'Fn::Join': [ + '', + [ + 's3://', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673', + }, + '/', + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D', + }, + ], + }, + ], + }, + ], + ], + }, + }); +}); + +test('throws with fromAsset and not .txt', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset(stack, 'Domains', 'image.jpg'), + })).toThrow(/expects a file with the .txt extension/); +}); + test('throws with invalid S3 URI', () => { expect(() => new FirewallDomainList(stack, 'List', { domains: FirewallDomains.fromS3Uri('https://invalid/bucket/uri'), })).toThrow(/The S3 URI must start with s3:\/\//); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts index d83fea3ed11d9..aeb8b429bbb4b 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts @@ -134,4 +134,4 @@ test('throws when associating with a priority not between 100-9,000', () => { priority: 100, vpc, })).toThrow(/Priority must be greater than 100 and less than 9000/); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json index 11849dea69327..c6b43a9c3ccfd 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json @@ -213,6 +213,49 @@ ] } }, + "OtherListBA4427B5": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "DomainFileUrl": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + } + ] + ] + } + } + }, "RuleGroup06BA8844": { "Type": "AWS::Route53Resolver::FirewallRuleGroup", "Properties": { @@ -260,5 +303,19 @@ } } } + }, + "Parameters": { + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673": { + "Type": "String", + "Description": "S3 bucket for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D": { + "Type": "String", + "Description": "S3 key for asset version \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aArtifactHashFF61A347": { + "Type": "String", + "Description": "Artifact hash for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts index 730ad0f5176a2..81713348b113c 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { Vpc } from '@aws-cdk/aws-ec2'; import { App, Stack, StackProps } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -16,6 +17,10 @@ class TestStack extends Stack { domains: route53resolver.FirewallDomains.fromList(['override-domain.com']), }); + new route53resolver.FirewallDomainList(this, 'OtherList', { + domains: route53resolver.FirewallDomains.fromAsset(this, 'Domains', path.join(__dirname, 'domains.txt')), + }); + const ruleGroup = new route53resolver.FirewallRuleGroup(this, 'RuleGroup'); ruleGroup.addRule({ From 88dbdc8c22c9d240db301633b9071645d1bdc7ae Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 18 Jun 2021 14:29:22 +0200 Subject: [PATCH 05/14] validate domains --- .../aws-route53resolver/lib/firewall-domain-list.ts | 5 +++++ .../aws-route53resolver/test/firewall-domain-list.test.ts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index bdf22cebab211..2acc7e1df3d54 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -44,6 +44,11 @@ export abstract class FirewallDomains { * @param list the list of domains */ public static fromList(list: string[]): FirewallDomains { + for (const domain of list) { + if (!/^[\w-.]{1,128}$/.test(domain)) { + throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + } return { list }; } diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index 54837662cab34..82cf4a5592530 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -97,6 +97,12 @@ test('domain list from asset', () => { }); }); +test('throws with invalid domain', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['valid.fr', 'inv@lid.com']), + })).toThrow(/Invalid domain/); +}); + test('throws with fromAsset and not .txt', () => { expect(() => new FirewallDomainList(stack, 'List', { domains: FirewallDomains.fromAsset(stack, 'Domains', 'image.jpg'), From c62af08f60357049192980442fce2597d072741a Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 18 Jun 2021 14:30:43 +0200 Subject: [PATCH 06/14] add missing test file --- packages/@aws-cdk/aws-route53resolver/test/domains.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/@aws-cdk/aws-route53resolver/test/domains.txt diff --git a/packages/@aws-cdk/aws-route53resolver/test/domains.txt b/packages/@aws-cdk/aws-route53resolver/test/domains.txt new file mode 100644 index 0000000000000..872337c768ca7 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/domains.txt @@ -0,0 +1,4 @@ +amazon.com +amazon.co.uk +amazon.fr +amazon.de From 59b3e5cdddc1b146218c58adaeced1e47b6fb773 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 18 Jun 2021 14:34:43 +0200 Subject: [PATCH 07/14] README --- packages/@aws-cdk/aws-route53resolver/README.md | 9 +++++++-- .../aws-route53resolver/lib/firewall-domain-list.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index ce28662f6d33e..80f4ef47e298e 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -38,7 +38,8 @@ explicitly trust. ### Domain lists -Domain lists can be created using a list of strings or text file stored in Amazon S3: +Domain lists can be created using a list of strings, a text file stored in Amazon S3 or a local +text file: ```ts const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { @@ -47,7 +48,11 @@ const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { domains: route53resolver.FirewallDomains.fromS3Uri('s3://bucket/prefix/object'), -}) +}); + +const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', { + domains: route53resolver.FirewallDomains.fromAsset(this, 'Asset', '/path/to/domains.txt'), +}); ``` The file must be a text file and must contain a single domain per line. diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index 2acc7e1df3d54..3bba7cf14a066 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -82,7 +82,7 @@ export abstract class FirewallDomains { /** * Firewall domains created from a local disk path to a text file. * The file must be a text file (`.txt` extension) and must contain a single - * domain per line. + * domain per line. It will be uploaded to S3. * * @param assetPath path to the text file */ From 29af36f30fcc4cabfa41eccd911757d24645dce6 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 21 Jun 2021 14:53:35 +0200 Subject: [PATCH 08/14] DomainConfig --- .../@aws-cdk/aws-route53resolver/README.md | 2 +- .../lib/firewall-domain-list.ts | 73 ++++++++++++++----- .../test/firewall-domain-list.test.ts | 8 +- .../test/integ.firewall.ts | 2 +- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index 80f4ef47e298e..f49f6945569f5 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -51,7 +51,7 @@ const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { }); const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', { - domains: route53resolver.FirewallDomains.fromAsset(this, 'Asset', '/path/to/domains.txt'), + domains: route53resolver.FirewallDomains.fromAsset('/path/to/domains.txt'), }); ``` diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index 3bba7cf14a066..7cabc1b6c9dfa 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -49,22 +49,31 @@ export abstract class FirewallDomains { throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); } } - return { list }; + + return { + bind(_scope: Construct): DomainsConfig { + return { domains: list }; + }, + }; } /** - * Firewall domains created from the URI of a file stored in Amazon S3. + * Firewall domains created from the URL of a file stored in Amazon S3. * The file must be a text file and must contain a single domain per line. * The content type of the S3 object must be `plain/text`. * - * @param s3Uri S3 bucket uri (s3://bucket/prefix/objet). + * @param url S3 bucket url (s3://bucket/prefix/objet). */ - public static fromS3Uri(s3Uri: string): FirewallDomains { - if (!Token.isUnresolved(s3Uri) && !s3Uri.startsWith('s3://')) { - throw new Error(`The S3 URI must start with s3://, got ${s3Uri}`); + public static fromS3Url(url: string): FirewallDomains { + if (!Token.isUnresolved(url) && !url.startsWith('s3://')) { + throw new Error(`The S3 URI must start with s3://, got ${url}`); } - return { s3Uri }; + return { + bind(_scope: Construct): DomainsConfig { + return { domainFileUrl: url }; + }, + }; } /** @@ -76,7 +85,7 @@ export abstract class FirewallDomains { * @param key S3 key */ public static fromS3(bucket: IBucket, key: string): FirewallDomains { - return this.fromS3Uri(bucket.s3UrlForObject(key)); + return this.fromS3Url(bucket.s3UrlForObject(key)); } /** @@ -86,27 +95,50 @@ export abstract class FirewallDomains { * * @param assetPath path to the text file */ - public static fromAsset(scope: Construct, id: string, assetPath: string): FirewallDomains { + public static fromAsset(assetPath: string): FirewallDomains { // cdk-assets will correctly set the content type for the S3 object // if the file has the correct extension if (path.extname(assetPath) !== '.txt') { throw new Error(`FirewallDomains.fromAsset() expects a file with the .txt extension, got ${assetPath}`); } - const asset = new Asset(scope, id, { path: assetPath }); + return { + bind(scope: Construct): DomainsConfig { + const asset = new Asset(scope, 'Domains', { path: assetPath }); - if (!asset.isFile) { - throw new Error('FirewallDomains.fromAsset() expects a file'); - } + if (!asset.isFile) { + throw new Error('FirewallDomains.fromAsset() expects a file'); + } + + return { domainFileUrl: asset.s3ObjectUrl }; + }, + }; - return this.fromS3Uri(asset.s3ObjectUrl); } - /** S3 bucket URI of text file with domain list */ - public abstract s3Uri?: string; + /** Binds the domains to a domain list */ + public abstract bind(scope: Construct): DomainsConfig; +} + +/** + * Domains configuration + */ +export interface DomainsConfig { + /** + * The fully qualified URL or URI of the file stored in Amazon S3 that contains + * the list of domains to import. The file must be a text file and must contain + * a single domain per line. The content type of the S3 object must be `plain/text`. + * + * @default - use `domains` + */ + readonly domainFileUrl?: string; - /** List of domains */ - public abstract readonly list?: string[]; + /** + * A list of domains + * + * @default - use `domainFileUrl` + */ + readonly domains?: string[]; } /** @@ -178,10 +210,11 @@ export class FirewallDomainList extends Resource implements IFirewallDomainList constructor(scope: Construct, id: string, props: FirewallDomainListProps) { super(scope, id); + const domainsConfig = props.domains.bind(this); const domainList = new CfnFirewallDomainList(this, 'Resource', { name: props.name, - domainFileUrl: props.domains.s3Uri, - domains: props.domains.list, + domainFileUrl: domainsConfig.domainFileUrl, + domains: domainsConfig.domains, }); this.firewallDomainListId = domainList.attrId; diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index 82cf4a5592530..c00ed68f32244 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -27,7 +27,7 @@ test('domain list from strings', () => { test('domain list from S3 URI', () => { // WHEN new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromS3Uri('s3://bucket/prefix/object'), + domains: FirewallDomains.fromS3Url('s3://bucket/prefix/object'), }); // THEN @@ -51,7 +51,7 @@ test('domain list from S3', () => { test('domain list from asset', () => { // WHEN new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromAsset(stack, 'Domains', path.join(__dirname, 'domains.txt')), + domains: FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), }); // THEN @@ -105,12 +105,12 @@ test('throws with invalid domain', () => { test('throws with fromAsset and not .txt', () => { expect(() => new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromAsset(stack, 'Domains', 'image.jpg'), + domains: FirewallDomains.fromAsset('image.jpg'), })).toThrow(/expects a file with the .txt extension/); }); test('throws with invalid S3 URI', () => { expect(() => new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromS3Uri('https://invalid/bucket/uri'), + domains: FirewallDomains.fromS3Url('https://invalid/bucket/uri'), })).toThrow(/The S3 URI must start with s3:\/\//); }); diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts index 81713348b113c..7c4b2498af6f5 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts @@ -18,7 +18,7 @@ class TestStack extends Stack { }); new route53resolver.FirewallDomainList(this, 'OtherList', { - domains: route53resolver.FirewallDomains.fromAsset(this, 'Domains', path.join(__dirname, 'domains.txt')), + domains: route53resolver.FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), }); const ruleGroup = new route53resolver.FirewallRuleGroup(this, 'RuleGroup'); From 7ebe7f733f14a3aa75fd7bf888226b947bbef247 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 7 Jul 2021 09:22:40 +0200 Subject: [PATCH 09/14] Update packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts Co-authored-by: Nick Lynch --- .../@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts index 5e2a4d3c02157..4346e23e46435 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts @@ -234,7 +234,7 @@ export class FirewallRuleGroup extends Resource implements IFirewallRuleGroup { }); this.firewallRuleGroupId = ruleGroup.attrId; - this.firewallRuleGroupArn= ruleGroup.attrArn; + this.firewallRuleGroupArn = ruleGroup.attrArn; this.firewallRuleGroupCreationTime = ruleGroup.attrCreationTime; this.firewallRuleGroupCreatorRequestId = ruleGroup.attrCreatorRequestId; this.firewallRuleGroupModificationTime = ruleGroup.attrModificationTime; @@ -274,4 +274,4 @@ function renderRule(rule: FirewallRule): CfnFirewallRuleGroup.FirewallRuleProper blockOverrideTtl: rule.action.blockResponse?.blockOverrideTtl?.toSeconds(), blockResponse: rule.action.blockResponse?.blockResponse, }; -} \ No newline at end of file +} From aa5de11d1be663d0d550813d092ee63eda8c036c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 7 Jul 2021 09:23:22 +0200 Subject: [PATCH 10/14] Update packages/@aws-cdk/aws-route53resolver/README.md Co-authored-by: Nick Lynch --- packages/@aws-cdk/aws-route53resolver/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index f49f6945569f5..78c2a09718c6e 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -74,7 +74,7 @@ new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { { priority: 10, firewallDomainList: myBlockList, - action: FirewallRuleAction.block(), // defaults to NODATA + action: route53resolver.FirewallRuleAction.block(), // defaults to NODATA }, ], }); From 9452284805e9be27ccf909f69092a4ac8d403872 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 7 Jul 2021 09:27:09 +0200 Subject: [PATCH 11/14] URI -> URL --- packages/@aws-cdk/aws-route53resolver/README.md | 6 +++--- .../aws-route53resolver/lib/firewall-domain-list.ts | 2 +- .../aws-route53resolver/test/firewall-domain-list.test.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index 78c2a09718c6e..c29d893bbc899 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -47,7 +47,7 @@ const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { }); const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { - domains: route53resolver.FirewallDomains.fromS3Uri('s3://bucket/prefix/object'), + domains: route53resolver.FirewallDomains.fromS3Url('s3://bucket/prefix/object'), }); const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', { @@ -87,14 +87,14 @@ ruleGroup.addRule({ priority: 10, firewallDomainList: blockList, // block and reply with NXDOMAIN - action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), }); ruleGroup.addRule({ priority: 20, firewallDomainList: blockList, // block and override DNS response with a custom domain - action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), }); ``` diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index 7cabc1b6c9dfa..5eee4b5b8c5e4 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -66,7 +66,7 @@ export abstract class FirewallDomains { */ public static fromS3Url(url: string): FirewallDomains { if (!Token.isUnresolved(url) && !url.startsWith('s3://')) { - throw new Error(`The S3 URI must start with s3://, got ${url}`); + throw new Error(`The S3 URL must start with s3://, got ${url}`); } return { diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index c00ed68f32244..0c33b1c2360ec 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -24,7 +24,7 @@ test('domain list from strings', () => { }); }); -test('domain list from S3 URI', () => { +test('domain list from S3 URL', () => { // WHEN new FirewallDomainList(stack, 'List', { domains: FirewallDomains.fromS3Url('s3://bucket/prefix/object'), @@ -109,8 +109,8 @@ test('throws with fromAsset and not .txt', () => { })).toThrow(/expects a file with the .txt extension/); }); -test('throws with invalid S3 URI', () => { +test('throws with invalid S3 URL', () => { expect(() => new FirewallDomainList(stack, 'List', { - domains: FirewallDomains.fromS3Url('https://invalid/bucket/uri'), - })).toThrow(/The S3 URI must start with s3:\/\//); + domains: FirewallDomains.fromS3Url('https://invalid/bucket/url'), + })).toThrow(/The S3 URL must start with s3:\/\//); }); From 583acfc8a17871eca370dddd00b65c95988ded04 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 7 Jul 2021 09:28:13 +0200 Subject: [PATCH 12/14] NODATA comment --- packages/@aws-cdk/aws-route53resolver/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index c29d893bbc899..a3bf5946ead7d 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -74,7 +74,8 @@ new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { { priority: 10, firewallDomainList: myBlockList, - action: route53resolver.FirewallRuleAction.block(), // defaults to NODATA + // block and reply with NODATA + action: route53resolver.FirewallRuleAction.block(), }, ], }); From b1f03501df48fd4559e47e472baedf7f4adc19b9 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 7 Jul 2021 09:39:37 +0200 Subject: [PATCH 13/14] domain and domain list name validation --- .../aws-route53resolver/lib/firewall-domain-list.ts | 8 ++++++-- .../aws-route53resolver/test/firewall-domain-list.test.ts | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts index 5eee4b5b8c5e4..a6303b2c78114 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -45,8 +45,8 @@ export abstract class FirewallDomains { */ public static fromList(list: string[]): FirewallDomains { for (const domain of list) { - if (!/^[\w-.]{1,128}$/.test(domain)) { - throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); + if (!/^[\w-.]+$/.test(domain)) { + throw new Error(`Invalid domain: ${domain}. Valid characters: A-Z, a-z, 0-9, _, -, .`); } } @@ -210,6 +210,10 @@ export class FirewallDomainList extends Resource implements IFirewallDomainList constructor(scope: Construct, id: string, props: FirewallDomainListProps) { super(scope, id); + if (props.name && !Token.isUnresolved(props.name) && !/^[\w-.]{1,128}$/.test(props.name)) { + throw new Error(`Invalid domain list name: ${props.name}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + const domainsConfig = props.domains.bind(this); const domainList = new CfnFirewallDomainList(this, 'Resource', { name: props.name, diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index 0c33b1c2360ec..d03fee4787dfd 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -97,6 +97,13 @@ test('domain list from asset', () => { }); }); +test('throws with invalid name', () => { + expect(() => new FirewallDomainList(stack, 'List', { + name: 'Inv@lid', + domains: FirewallDomains.fromList(['domain.com']), + })).toThrow(/Invalid domain list name/); +}); + test('throws with invalid domain', () => { expect(() => new FirewallDomainList(stack, 'List', { domains: FirewallDomains.fromList(['valid.fr', 'inv@lid.com']), From 5b62359d76d55ef60d0a5207c7ee2797d9581ea2 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 30 Aug 2021 11:16:19 +0200 Subject: [PATCH 14/14] assertions --- .../test/firewall-domain-list.test.ts | 10 +++++----- .../test/firewall-rule-group.test.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts index d03fee4787dfd..3806a59c670ba 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import { FirewallDomainList, FirewallDomains } from '../lib'; @@ -16,7 +16,7 @@ test('domain list from strings', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { Domains: [ 'first-domain.com', 'second-domain.net', @@ -31,7 +31,7 @@ test('domain list from S3 URL', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { DomainFileUrl: 's3://bucket/prefix/object', }); }); @@ -43,7 +43,7 @@ test('domain list from S3', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { DomainFileUrl: 's3://bucket/prefix/object', }); }); @@ -55,7 +55,7 @@ test('domain list from asset', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallDomainList', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { DomainFileUrl: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts index aeb8b429bbb4b..f8868d2bb31d9 100644 --- a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Vpc } from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { DnsBlockResponse, FirewallDomainList, FirewallRuleAction, FirewallRuleGroup, IFirewallDomainList } from '../lib'; @@ -23,7 +23,7 @@ test('basic rule group', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { FirewallRules: [ { Action: 'BLOCK', @@ -55,7 +55,7 @@ test('use addRule to add rules', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { FirewallRules: [ { Action: 'ALLOW', @@ -83,7 +83,7 @@ test('rule with response override', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { FirewallRules: [ { Action: 'BLOCK', @@ -110,7 +110,7 @@ test('associate rule group with a vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53Resolver::FirewallRuleGroupAssociation', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroupAssociation', { FirewallRuleGroupId: { 'Fn::GetAtt': [ 'RuleGroup06BA8844',