From ceb25e24a3d93e72c6c60af1ff164a3606f89c9c Mon Sep 17 00:00:00 2001 From: Rachel Aurand Date: Thu, 17 Mar 2022 11:10:48 +0000 Subject: [PATCH] Add AWS Wafv2 Support (#1254) * Add basic support for wafv2, missing aws_wafv2_web_acl_association and aws_wafv2_web_acl_logging_configuration * add logging config and associatations * add loop for each resource type * scope cloudfront to us-east-1 * move up dep * add docs --- cmd/provider_cmd_aws.go | 33 ++++-- docs/aws.md | 13 +++ go.mod | 1 + go.sum | 2 + providers/aws/aws_provider.go | 9 +- providers/aws/wafv2.go | 207 ++++++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 providers/aws/wafv2.go diff --git a/cmd/provider_cmd_aws.go b/cmd/provider_cmd_aws.go index 7a00c74315..cd05fc7383 100644 --- a/cmd/provider_cmd_aws.go +++ b/cmd/provider_cmd_aws.go @@ -33,7 +33,7 @@ func newCmdAwsImporter(options ImportOptions) *cobra.Command { if len(options.Regions) > 0 { shouldSpecifyPathRegion := len(options.Regions) > 1 - globalResources := parseGlobalResources(originalResources) + globalResources, eastOnlyResources, regionalResources := parseAndGroupResources(originalResources) options.Resources = globalResources options.Regions = []string{awsterraformer.GlobalRegion} e := importGlobalResources(options) @@ -41,7 +41,14 @@ func newCmdAwsImporter(options ImportOptions) *cobra.Command { return e } - options.Resources = parseRegionalResources(originalResources) + options.Resources = eastOnlyResources + options.Regions = []string{awsterraformer.MainRegionPublicPartition} + e = importEastOnlyResources(options) + if e != nil { + return e + } + + options.Resources = regionalResources options.Regions = originalRegions if len(options.Resources) > 0 { // don't import anything and potentially override global resources if len(globalResources) > 0 { @@ -71,14 +78,19 @@ func newCmdAwsImporter(options ImportOptions) *cobra.Command { return cmd } -func parseGlobalResources(allResources []string) []string { - var globalResources []string +// returns global, east-only, regional resources +func parseAndGroupResources(allResources []string) ([]string, []string, []string) { + var globalResources, eastOnlyResources, regionalResources []string for _, resourceName := range allResources { if contains(awsterraformer.SupportedGlobalResources, resourceName) { globalResources = append(globalResources, resourceName) + } else if contains(awsterraformer.SupportedEastOnlyResources, resourceName) { + eastOnlyResources = append(eastOnlyResources, resourceName) + } else { + regionalResources = append(regionalResources, resourceName) } } - return globalResources + return globalResources, eastOnlyResources, regionalResources } func importGlobalResources(options ImportOptions) error { @@ -88,14 +100,11 @@ func importGlobalResources(options ImportOptions) error { return nil } -func parseRegionalResources(allResources []string) []string { - var localResources []string - for _, resourceName := range allResources { - if !contains(awsterraformer.SupportedGlobalResources, resourceName) { - localResources = append(localResources, resourceName) - } +func importEastOnlyResources(options ImportOptions) error { + if len(options.Resources) > 0 { + return importRegionResources(options, options.PathPattern, awsterraformer.MainRegionPublicPartition, false) } - return localResources + return nil } func importRegionResources(options ImportOptions, originalPathPattern string, region string, shouldSpecifyPathRegion bool) error { diff --git a/docs/aws.md b/docs/aws.md index a0fca77751..2c5a2a97a1 100644 --- a/docs/aws.md +++ b/docs/aws.md @@ -307,6 +307,19 @@ terraformer import aws --resources=sg --regions=us-east-1 * `aws_wafregional_sql_injection_match_set` * `aws_wafregional_web_acl` * `aws_wafregional_xss_match_set` +* `wafv2_cloudfront` + * `aws_wafv2_ip_set` + * `aws_wafv2_regex_pattern_set` + * `aws_wafv2_rule_group` + * `aws_wafv2_web_acl` + * `aws_wafv2_web_acl_logging_configuration` +* `wafv2_regional` + * `aws_wafv2_ip_set` + * `aws_wafv2_regex_pattern_set` + * `aws_wafv2_rule_group` + * `aws_wafv2_web_acl` + * `aws_wafv2_web_acl_association` + * `aws_wafv2_web_acl_logging_configuration` * `vpc` * `aws_vpc` * `vpc_peering` diff --git a/go.mod b/go.mod index 6e9fcd312d..869c29f127 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/swf v1.10.0 github.com/aws/aws-sdk-go-v2/service/waf v1.1.4 github.com/aws/aws-sdk-go-v2/service/wafregional v1.2.1 + github.com/aws/aws-sdk-go-v2/service/wafv2 v1.18.0 github.com/aws/aws-sdk-go-v2/service/workspaces v1.2.1 github.com/aws/aws-sdk-go-v2/service/xray v1.2.1 github.com/cenkalti/backoff v2.2.1+incompatible // indirect diff --git a/go.sum b/go.sum index 06d8057fba..c66f3ef34f 100644 --- a/go.sum +++ b/go.sum @@ -373,6 +373,8 @@ github.com/aws/aws-sdk-go-v2/service/waf v1.1.4 h1:Wh0YMwF394A9eBC8Pv8Uxk6CBx9KT github.com/aws/aws-sdk-go-v2/service/waf v1.1.4/go.mod h1:Ujifac0yyu5RLw3cPCGua50uHnsjiJo9LAhXEJZ+tVk= github.com/aws/aws-sdk-go-v2/service/wafregional v1.2.1 h1:Re8Uwffnndo9t8V46kT3aruMvVXHXUmuYKhIF1U9CBc= github.com/aws/aws-sdk-go-v2/service/wafregional v1.2.1/go.mod h1:WvRSWmA7SJw0TbU0XKCo+oC/qsD/OR/tFRzTnq8FBpk= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.18.0 h1:d42YwLtFO0CgubEAPUxaW8rw/i8eFiFgP9dCnYuE/Ok= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.18.0/go.mod h1:mJGumgpCFbImG07kvjOS75rsmHlF4uY8fm31h1fvumc= github.com/aws/aws-sdk-go-v2/service/workspaces v1.2.1 h1:hSzpp50D6D37eIELSSpYwJ75e8XL9bvTzjspZgt4+5A= github.com/aws/aws-sdk-go-v2/service/workspaces v1.2.1/go.mod h1:8k9EEz8LMNPUDENPlW0laaQkAZC2TbEYF+XNUu1lFLk= github.com/aws/aws-sdk-go-v2/service/xray v1.2.1 h1:RiTWbH90tIuJNnZZys9HeqBfpT3oSPulh7fM7anPQiY= diff --git a/providers/aws/aws_provider.go b/providers/aws/aws_provider.go index 8279ac4b04..847bc32e02 100644 --- a/providers/aws/aws_provider.go +++ b/providers/aws/aws_provider.go @@ -35,8 +35,6 @@ const NoRegion = "" // SupportedGlobalResources should be bound to a default region. AWS doesn't specify in which region default services are // placed (see https://docs.aws.amazon.com/general/latest/gr/rande.html), so we shouldn't assume any region as well -// -// AWS WAF V2 if added, should not be included in this list since it is a composition of regional and global resources. var SupportedGlobalResources = []string{ "budgets", "cloudfront", @@ -47,6 +45,11 @@ var SupportedGlobalResources = []string{ "waf", } +// SupportedEastOnlyResources should be bound to us-east-1 region only, and does not work in any other region. +var SupportedEastOnlyResources = []string{ + "wafv2_cloudfront", +} + func (p AWSProvider) GetResourceConnections() map[string]map[string][]string { return map[string]map[string][]string{ "alb": { @@ -304,6 +307,8 @@ func (p *AWSProvider) GetSupportedService() map[string]terraformutils.ServiceGen "transit_gateway": &AwsFacade{service: &TransitGatewayGenerator{}}, "waf": &AwsFacade{service: &WafGenerator{}}, "waf_regional": &AwsFacade{service: &WafRegionalGenerator{}}, + "wafv2_cloudfront": &AwsFacade{service: NewWafv2CloudfrontGenerator()}, + "wafv2_regional": &AwsFacade{service: NewWafv2RegionalGenerator()}, "vpc": &AwsFacade{service: &VpcGenerator{}}, "vpc_peering": &AwsFacade{service: &VpcPeeringConnectionGenerator{}}, "vpn_connection": &AwsFacade{service: &VpnConnectionGenerator{}}, diff --git a/providers/aws/wafv2.go b/providers/aws/wafv2.go new file mode 100644 index 0000000000..808b7d5d6b --- /dev/null +++ b/providers/aws/wafv2.go @@ -0,0 +1,207 @@ +// Copyright 2020 The Terraformer Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aws + +import ( + "context" + + "github.com/GoogleCloudPlatform/terraformer/terraformutils" + "github.com/aws/aws-sdk-go-v2/service/wafv2" + "github.com/aws/aws-sdk-go-v2/service/wafv2/types" +) + +var wafv2AllowEmptyValues = []string{"tags."} + +type Wafv2Generator struct { + AWSService + scope types.Scope +} + +func NewWafv2CloudfrontGenerator() *Wafv2Generator { + return &Wafv2Generator{scope: types.ScopeCloudfront} +} + +func NewWafv2RegionalGenerator() *Wafv2Generator { + return &Wafv2Generator{scope: types.ScopeRegional} +} + +func (g *Wafv2Generator) InitResources() error { + config, e := g.generateConfig() + if e != nil { + return e + } + svc := wafv2.NewFromConfig(config) + + if err := g.loadWebACL(svc); err != nil { + return err + } + if err := g.loadIPSet(svc); err != nil { + return err + } + if err := g.loadRegexPatternSets(svc); err != nil { + return err + } + if err := g.loadWafRuleGroups(svc); err != nil { + return err + } + if err := g.loadWebACLLoggingConfiguration(svc); err != nil { + return err + } + + return nil +} + +func (g *Wafv2Generator) loadWebACL(svc *wafv2.Client) error { + output, err := svc.ListWebACLs(context.TODO(), &wafv2.ListWebACLsInput{Scope: g.scope}) + if err != nil { + return err + } + for _, acl := range output.WebACLs { + g.Resources = append(g.Resources, terraformutils.NewResource( + *acl.Id, + *acl.Name+"_"+(*acl.Id)[0:8], + "aws_wafv2_web_acl", + "aws", + map[string]string{ + "name": *acl.Name, + "scope": string(g.scope), + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + if g.scope == types.ScopeRegional { + // cloudfront associations are not listed here since they should to defined in + // aws_cloudfront_distribution resource instead + err = g.loadWebACLAssociations(svc, acl.ARN) + if err != nil { + return err + } + } + } + return nil +} + +func (g *Wafv2Generator) loadWebACLAssociations(svc *wafv2.Client, webACLArn *string) error { + for _, resourceType := range types.ResourceTypeApplicationLoadBalancer.Values() { + output, err := svc.ListResourcesForWebACL(context.TODO(), + &wafv2.ListResourcesForWebACLInput{WebACLArn: webACLArn, ResourceType: resourceType}) + if err != nil { + return err + } + for _, resource := range output.ResourceArns { + g.Resources = append(g.Resources, terraformutils.NewResource( + resource, + resource, + "aws_wafv2_web_acl_association", + "aws", + map[string]string{ + "resource_arn": resource, + "web_acl_arn": *webACLArn, + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + } + } + return nil +} + +func (g *Wafv2Generator) loadIPSet(svc *wafv2.Client) error { + output, err := svc.ListIPSets(context.TODO(), &wafv2.ListIPSetsInput{Scope: g.scope}) + if err != nil { + return err + } + for _, IPSet := range output.IPSets { + g.Resources = append(g.Resources, terraformutils.NewResource( + *IPSet.Id, + *IPSet.Name+"_"+(*IPSet.Id)[0:8], + "aws_wafv2_ip_set", + "aws", + map[string]string{ + "name": *IPSet.Name, + "scope": string(g.scope), + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + } + return nil +} + +func (g *Wafv2Generator) loadRegexPatternSets(svc *wafv2.Client) error { + output, err := svc.ListRegexPatternSets(context.TODO(), &wafv2.ListRegexPatternSetsInput{Scope: g.scope}) + if err != nil { + return err + } + for _, regexPatternSet := range output.RegexPatternSets { + g.Resources = append(g.Resources, terraformutils.NewResource( + *regexPatternSet.Id, + *regexPatternSet.Name+"_"+(*regexPatternSet.Id)[0:8], + "aws_wafv2_regex_pattern_set", + "aws", + map[string]string{ + "name": *regexPatternSet.Name, + "scope": string(g.scope), + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + } + return nil +} + +func (g *Wafv2Generator) loadWafRuleGroups(svc *wafv2.Client) error { + output, err := svc.ListRuleGroups(context.TODO(), &wafv2.ListRuleGroupsInput{Scope: g.scope}) + if err != nil { + return err + } + for _, ruleGroup := range output.RuleGroups { + g.Resources = append(g.Resources, terraformutils.NewResource( + *ruleGroup.Id, + *ruleGroup.Name+"_"+(*ruleGroup.Id)[0:8], + "aws_wafv2_rule_group", + "aws", + map[string]string{ + "arn": *ruleGroup.ARN, + "name": *ruleGroup.Name, + "scope": string(g.scope), + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + } + return nil +} + +func (g *Wafv2Generator) loadWebACLLoggingConfiguration(svc *wafv2.Client) error { + output, err := svc.ListLoggingConfigurations(context.TODO(), &wafv2.ListLoggingConfigurationsInput{Scope: g.scope}) + if err != nil { + return err + } + for _, logConfig := range output.LoggingConfigurations { + g.Resources = append(g.Resources, terraformutils.NewResource( + *logConfig.ResourceArn, + *logConfig.ResourceArn, + "aws_wafv2_web_acl_logging_configuration", + "aws", + map[string]string{ + "resource_arn": *logConfig.ResourceArn, + }, + wafv2AllowEmptyValues, + map[string]interface{}{}, + )) + } + return nil +}