From 161b3f8ff68df4697eded0ca6172f90c157af319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=9E=AC=EB=AA=A9=28LGUPLUS=29?= <105696566+jaemokhong@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:50:32 +0900 Subject: [PATCH] Add support for tagging WAFV2 WebACLs (#224) --- README.MD | 34 +++++++++ cmd/wafv2.go | 107 +++++++++++++++++++++++++++ pkg/wafv2.go | 110 +++++++++++++++++++++++++++ pkg/wafv2_test.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 436 insertions(+) create mode 100644 cmd/wafv2.go create mode 100644 pkg/wafv2.go create mode 100644 pkg/wafv2_test.go diff --git a/README.MD b/README.MD index 60a6767..0107683 100644 --- a/README.MD +++ b/README.MD @@ -30,6 +30,8 @@ Tags are critical to managing AWS resources at scale. Awstaghelper provides a co * [ECR](#ecr) * [AutoScaling groups](#autoscaling-groups) * [EBS Volumes](#ebs-volumes) + * [WAFV2 CloudFront WebACL](#wafv2-cloudfront-webacl) + * [WAFV2 Regional WebACL](#wafv2-regional-webacl) * [Global parameters](#global-parameters) * [Contributing](#contributing) * [License](#license) @@ -340,6 +342,38 @@ Read csv and tag EBS volumes - `awstaghelper ebs tag-ebs` Example: `awstaghelper ebs tag-ebs --filename ebsTags.csv --profile main` +### WAFV2 CloudFront WebACL + +#### Get wafv2 cloudfront webacl tags + +Get list of wafv2 cloudfront webacl with required tags - `awstaghelper wafv2 get-cfwebacl-tags` + +Example: +`awstaghelper wafv2 get-cfwebacl-tags --filename cfwebaclTags.csv --tags Name,Owner --profile main` + +#### Tag wafv2 cloudfront webacl + +Read csv and tag wafv2 cloudfront webacl - `awstaghelper wafv2 tag-cfwebacl` + +Example: +`awstaghelper wafv2 tag-cfwebacl --filename cfwebaclTags.csv --profile main` + +### WAFV2 Regional WebACL + +#### Get wafv2 regional webacl tags + +Get list of wafv2 regional webacl with required tags - `awstaghelper wafv2 get-webacl-tags` + +Example: +`awstaghelper wafv2 get-webacl-tags --filename webaclTags.csv --tags Name,Owner --profile main` + +#### Tag wafv2 regional webacl + +Read csv and tag wafv2 regional webacl - `awstaghelper wafv2 tag-webacl` + +Example: +`awstaghelper wafv2 tag-webacl --filename webaclTags.csv --profile main` + ## Global parameters `filename` - path where to write or read data. Supported by every option. Default `awsTags.csv` diff --git a/cmd/wafv2.go b/cmd/wafv2.go new file mode 100644 index 0000000..b174a37 --- /dev/null +++ b/cmd/wafv2.go @@ -0,0 +1,107 @@ +/* +Copyright © 2024 Jaemok Hong jaemokhong@lguplus.co.kr +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +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 cmd is the package for the CLI of awstaghelper +package cmd + +import ( + "github.com/mpostument/awstaghelper/pkg" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/wafv2" + "github.com/spf13/cobra" +) + +// wafv2Cmd represents the wafv2 command +var wafv2Cmd = &cobra.Command{ + Use: "wafv2", + Short: "Root command for interaction with AWS wafv2 services(cloudfront, regional)", + Long: `Root command for interaction with AWS wafv2 services(cloudfront, regional),`, +} + +var getWebACLCmd = &cobra.Command{ + Use: "get-webacl-tags", + Short: "Write regional webacl arn and required tags to csv", + Long: `Write to csv data with regional webacl arn and required tags to csv. +This csv can be used with tag-webacl command to tag aws environment. +Specify list of tags which should be read using tags flag: --tags Name,Env,Project. +Csv filename can be specified with flag filename.`, + Run: func(cmd *cobra.Command, args []string) { + tags, _ := cmd.Flags().GetString("tags") + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + region, _ := cmd.Flags().GetString("region") + sess := pkg.GetSession(region, profile) + client := wafv2.New(sess) + pkg.WriteCsv(pkg.ParseWebACLTags(tags, "REGIONAL", client), filename) + }, +} + +var tagWebACLCmd = &cobra.Command{ + Use: "tag-webacl", + Short: "Read csv and tag regional webacl with csv data", + Long: `Read csv generated with get-webacl-tags command and tag regional webacl with tags from csv.`, + Run: func(cmd *cobra.Command, args []string) { + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + region, _ := cmd.Flags().GetString("region") + sess := pkg.GetSession(region, profile) + client := wafv2.New(sess, aws.NewConfig().WithRegion(region)) + csvData := pkg.ReadCsv(filename) + pkg.TagWebACL(csvData, client) + }, +} + +var getCFWebACLCmd = &cobra.Command{ + Use: "get-cfwebacl-tags", + Short: "Write cloudfront webacl arn and required tags to csv", + Long: `Write to csv data with cloudfront webacl arn and required tags to csv. +This csv can be used with tag-cfwebacl command to tag aws environment. +Specify list of tags which should be read using tags flag: --tags Name,Env,Project. +Csv filename can be specified with flag filename.`, + Run: func(cmd *cobra.Command, args []string) { + tags, _ := cmd.Flags().GetString("tags") + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + sess := pkg.GetSession("us-east-1", profile) + client := wafv2.New(sess) + pkg.WriteCsv(pkg.ParseWebACLTags(tags, "CLOUDFRONT", client), filename) + }, +} + +var tagCFWebACLCmd = &cobra.Command{ + Use: "tag-cfwebacl", + Short: "Read csv and tag cloudfront webacl with csv data", + Long: `Read csv generated with get-cfwebacl-tags command and tag cloudfront webacl with tags from csv.`, + Run: func(cmd *cobra.Command, args []string) { + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + region, _ := cmd.Flags().GetString("region") + sess := pkg.GetSession(region, profile) + client := wafv2.New(sess, aws.NewConfig().WithRegion("us-east-1")) + csvData := pkg.ReadCsv(filename) + pkg.TagWebACL(csvData, client) + }, +} + +func init() { + rootCmd.AddCommand(wafv2Cmd) + wafv2Cmd.AddCommand(getWebACLCmd) + wafv2Cmd.AddCommand(tagWebACLCmd) + wafv2Cmd.AddCommand(getCFWebACLCmd) + wafv2Cmd.AddCommand(tagCFWebACLCmd) +} diff --git a/pkg/wafv2.go b/pkg/wafv2.go new file mode 100644 index 0000000..2854bcb --- /dev/null +++ b/pkg/wafv2.go @@ -0,0 +1,110 @@ +/* +Copyright © 2024 Jaemok Hong jaemokhong@lguplus.co.kr +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +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 pkg + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/wafv2" + "github.com/aws/aws-sdk-go/service/wafv2/wafv2iface" +) + +// getWebACL return all webacls(regional, cloudfront) from specified region +func getWebACL(scope string, client wafv2iface.WAFV2API) *wafv2.ListWebACLsOutput { + input := &wafv2.ListWebACLsInput{ + Scope: aws.String(scope), + } + + var allWebACLs []*wafv2.WebACLSummary + var nextMarker *string + + for { + result, err := client.ListWebACLs(input) + if err != nil { + log.Fatal("Not able to get webacls ", err) + return nil + } + allWebACLs = append(allWebACLs, result.WebACLs...) + nextMarker = result.NextMarker + if nextMarker == nil || *nextMarker == "" { + break + } + input.NextMarker = nextMarker + } + return &wafv2.ListWebACLsOutput{WebACLs: allWebACLs} +} + +// ParseWebACLTags parse output from getWebACL and return webacl arn and specified tags. +func ParseWebACLTags(tagsToRead string, scope string, client wafv2iface.WAFV2API) [][]string { + wafv2Output := getWebACL(scope, client) + rows := addHeadersToCsv(tagsToRead, "Arn") + + for _, webACL := range wafv2Output.WebACLs { + var nextMarker *string + + for { + input := &wafv2.ListTagsForResourceInput{ + ResourceARN: webACL.ARN, + Limit: aws.Int64(5), + } + if nextMarker != nil { + input.NextMarker = nextMarker + } + webACLTags, err := client.ListTagsForResource(input) + if err != nil { + fmt.Println("Not able to get webACL tags ", err) + break + } + tags := map[string]string{} + for _, tag := range webACLTags.TagInfoForResource.TagList { + tags[*tag.Key] = *tag.Value + } + rows = addTagsToCsv(tagsToRead, tags, rows, *webACL.ARN) + nextMarker = webACLTags.NextMarker + if nextMarker == nil || *nextMarker == "" { + break + } + } + } + return rows +} + +// TagWebACL tag webacl. Take as input data from csv file. Where first column Arn +func TagWebACL(csvData [][]string, client wafv2iface.WAFV2API) { + for r := 1; r < len(csvData); r++ { + var tags []*wafv2.Tag + for c := 1; c < len(csvData[0]); c++ { + tags = append(tags, &wafv2.Tag{ + Key: &csvData[0][c], + Value: &csvData[r][c], + }) + } + + input := &wafv2.TagResourceInput{ + ResourceARN: aws.String(csvData[r][0]), + Tags: tags, + } + + _, err := client.TagResource(input) + if awsErrorHandle(err) { + return + } + } +} diff --git a/pkg/wafv2_test.go b/pkg/wafv2_test.go new file mode 100644 index 0000000..75c1e3b --- /dev/null +++ b/pkg/wafv2_test.go @@ -0,0 +1,185 @@ +/* +Copyright © 2024 Jaemok Hong jaemokhong@lguplus.co.kr +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +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 pkg + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/wafv2" + "github.com/aws/aws-sdk-go/service/wafv2/wafv2iface" + "github.com/stretchr/testify/assert" +) + +type mockedWebACL struct { + wafv2iface.WAFV2API + respListWebACLs wafv2.ListWebACLsOutput + respListTagsForResource wafv2.ListTagsForResourceOutput +} + +func (m *mockedWebACL) ListWebACLs(*wafv2.ListWebACLsInput) (*wafv2.ListWebACLsOutput, error) { + return &m.respListWebACLs, nil +} + +func (m *mockedWebACL) ListTagsForResource(*wafv2.ListTagsForResourceInput) (*wafv2.ListTagsForResourceOutput, error) { + return &m.respListTagsForResource, nil +} + +func TestGetWebACLs(t *testing.T) { + cases := []*mockedWebACL{ + { + respListWebACLs: listWebACLsResponse, + }, + } + + expectedResult := &listWebACLsResponse + + for _, c := range cases { + t.Run("getWebACL", func(t *testing.T) { + result := getWebACL("REGIONAL", c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + + } +} + +func TestParseWebACLTags(t *testing.T) { + cases := []*mockedWebACL{ + { + respListWebACLs: listWebACLsResponse, + respListTagsForResource: listWebACLsTags, + }, + } + + expectedResult := [][]string{ + {"Arn", "Name", "Owner"}, + {"arn:aws:wafv2:us-east-1:084888172679:regional/webacl/test-webacl/12345678-abcd-1234-abcd-12345678abcd", "test-webacl", "mpostument"}, + } + + for _, c := range cases { + t.Run("ParseWebACLTags", func(t *testing.T) { + result := ParseWebACLTags("Name,Owner", "REGIONAL", c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + + } +} + +var listWebACLsResponse = wafv2.ListWebACLsOutput{ + WebACLs: []*wafv2.WebACLSummary{ + { + ARN: aws.String("arn:aws:wafv2:us-east-1:084888172679:regional/webacl/test-webacl/12345678-abcd-1234-abcd-12345678abcd"), + }, + }, +} + +var listWebACLsTags = wafv2.ListTagsForResourceOutput{ + TagInfoForResource: &wafv2.TagInfoForResource{ + TagList: []*wafv2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("test-webacl"), + }, + { + Key: aws.String("Owner"), + Value: aws.String("mpostument"), + }, + }, + }, +} + +type mockedCFWebACL struct { + wafv2iface.WAFV2API + respListWebACLs wafv2.ListWebACLsOutput + respListTagsForResource wafv2.ListTagsForResourceOutput +} + +func (m *mockedCFWebACL) ListWebACLs(*wafv2.ListWebACLsInput) (*wafv2.ListWebACLsOutput, error) { + return &m.respListWebACLs, nil +} + +func (m *mockedCFWebACL) ListTagsForResource(*wafv2.ListTagsForResourceInput) (*wafv2.ListTagsForResourceOutput, error) { + return &m.respListTagsForResource, nil +} + +func TestGetCFWebACLs(t *testing.T) { + cases := []*mockedCFWebACL{ + { + respListWebACLs: listCFWebACLsResponse, + }, + } + + expectedResult := &listCFWebACLsResponse + + for _, c := range cases { + t.Run("getWebACLs", func(t *testing.T) { + result := getWebACL("REGIONAL", c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + + } +} + +func TestParseCFWebACLTags(t *testing.T) { + cases := []*mockedCFWebACL{ + { + respListWebACLs: listCFWebACLsResponse, + respListTagsForResource: listCFWebACLsTags, + }, + } + + expectedResult := [][]string{ + {"Arn", "Name", "Owner"}, + {"arn:aws:wafv2:us-east-1:084888172679:global/webacl/test-webacl/12345678-abcd-1234-abcd-12345678abcd", "test-webacl", "mpostument"}, + } + + for _, c := range cases { + t.Run("ParseWebACLTags", func(t *testing.T) { + result := ParseWebACLTags("Name,Owner", "CLOUDFRONT", c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + + } +} + +var listCFWebACLsResponse = wafv2.ListWebACLsOutput{ + WebACLs: []*wafv2.WebACLSummary{ + { + ARN: aws.String("arn:aws:wafv2:us-east-1:084888172679:global/webacl/test-webacl/12345678-abcd-1234-abcd-12345678abcd"), + }, + }, +} + +var listCFWebACLsTags = wafv2.ListTagsForResourceOutput{ + TagInfoForResource: &wafv2.TagInfoForResource{ + TagList: []*wafv2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("test-webacl"), + }, + { + Key: aws.String("Owner"), + Value: aws.String("mpostument"), + }, + }, + }, +}