diff --git a/README.md b/README.md index 4dcae5c4d..fb4f5d861 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![Coverage](https://img.shields.io/badge/Coverage-80.7%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-81.1%25-brightgreen) [![Maintained by Bridgecrew.io](https://img.shields.io/badge/maintained%20by-bridgecrew.io-blueviolet)](https://bridgecrew.io/?utm_source=github&utm_medium=organic_oss&utm_campaign=yor) ![golangci-lint](https://github.com/bridgecrewio/yor/workflows/tests/badge.svg) [![security](https://github.com/bridgecrewio/yor/actions/workflows/security.yml/badge.svg)](https://github.com/bridgecrewio/yor/actions/workflows/security.yml) @@ -225,7 +225,55 @@ yor list-tags yor list-tags --tag-groups git ``` +## Supporting comment format +To prevent resource from being tagged, apply the following comment pattern above the resource, currently supported only in Terraform and CloudFormation files. +## Example + +# skip specific resource - #yor:skip +```sh +## for terraform files +#yor:Skip +resource "aws_instance" "example_instance" { + ami = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + subnet_id = aws_subnet.example_subnet.id } + +## for cloudformation files +#yor:skip + ExampleInt: + Type: AWS::Lambda::Function + Properties: + Description: An example template +``` + +# skip all rsources in the page - #yor:skipAll +```sh +## for terraform files +#yor:skipAll +resource "aws_vpc" "example_vpc" { + cidr_block = "10.0.0.0/16" } + +resource "aws_subnet" "example_subnet" { + vpc_id = aws_vpc.example_vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-west-1a" } + +## for cloudformation files +#yor:skipAll +Resources: + NewVolume: + Type: AWS::EC2::Volume + Properties: + Size: 100 + + NewVolume2: + Type: AWS::EC2::Volume + Tags: + - Key: MyTag + Value: TagValue + - Key: Name +``` ### What is Yor trace? yor_trace is a magical tag creating a unique identifier for an IaC resource code block. diff --git a/src/cloudformation/structure/cloudformation_parser.go b/src/cloudformation/structure/cloudformation_parser.go index 092286aed..e3b337ef8 100644 --- a/src/cloudformation/structure/cloudformation_parser.go +++ b/src/cloudformation/structure/cloudformation_parser.go @@ -29,6 +29,7 @@ import ( type CloudformationParser struct { *types.YamlParser *types.JSONParser + skippedByCommentList []string } const TagsAttributeName = "Tags" @@ -113,6 +114,7 @@ func goformationParse(file string) (*cloudformation.Template, error) { } func (p *CloudformationParser) ParseFile(filePath string) ([]structure.IBlock, error) { + var skipResourcesByComment []string goformationLock.Lock() template, err := goformationParse(filePath) goformationLock.Unlock() @@ -138,7 +140,8 @@ func (p *CloudformationParser) ParseFile(filePath string) ([]structure.IBlock, e var resourceNamesToLines map[string]*structure.Lines switch utils.GetFileFormat(filePath) { case common.YmlFileType.FileFormat, common.YamlFileType.FileFormat: - resourceNamesToLines = yaml.MapResourcesLineYAML(filePath, resourceNames, ResourcesStartToken) + resourceNamesToLines, skipResourcesByComment = yaml.MapResourcesLineYAML(filePath, resourceNames, ResourcesStartToken) + p.skippedByCommentList = append(p.skippedByCommentList, skipResourcesByComment...) case common.JSONFileType.FileFormat: var fileBracketsMapping map[int]json.BracketPair resourceNamesToLines, fileBracketsMapping = json.MapResourcesLineJSON(filePath, resourceNames) @@ -293,3 +296,6 @@ func (p *CloudformationParser) getTagsLines(filePath string, resourceLinesRange return structure.Lines{Start: -1, End: -1} } } +func (p *CloudformationParser) GetSkipResourcesByComment() []string { + return p.skippedByCommentList +} diff --git a/src/cloudformation/structure/cloudformation_parser_test.go b/src/cloudformation/structure/cloudformation_parser_test.go index 85324973e..435eb7392 100644 --- a/src/cloudformation/structure/cloudformation_parser_test.go +++ b/src/cloudformation/structure/cloudformation_parser_test.go @@ -123,7 +123,7 @@ func Test_mapResourcesLineYAML(t *testing.T) { expected := map[string]*structure.Lines{ "NewVolume": {Start: 3, End: 15}, } - actual := yaml.MapResourcesLineYAML(filePath, resourcesNames, ResourcesStartToken) + actual, _ := yaml.MapResourcesLineYAML(filePath, resourcesNames, ResourcesStartToken) compareLines(t, expected, actual) }) @@ -136,7 +136,7 @@ func Test_mapResourcesLineYAML(t *testing.T) { "EC2LaunchTemplateResource0": {Start: 18, End: 23}, "EC2LaunchTemplateResource1": {Start: 24, End: 34}, } - actual := yaml.MapResourcesLineYAML(filePath, resourcesNames, ResourcesStartToken) + actual, _ := yaml.MapResourcesLineYAML(filePath, resourcesNames, ResourcesStartToken) compareLines(t, expected, actual) }) diff --git a/src/common/color_check.go b/src/common/color_check.go index 5247adc18..0b809c605 100644 --- a/src/common/color_check.go +++ b/src/common/color_check.go @@ -1,34 +1,33 @@ package common type ColorStruct struct { - NoColor bool - Reset string - Green string - Yellow string - Blue string - Purple string + NoColor bool + Reset string + Green string + Yellow string + Blue string + Purple string } -func NoColorCheck (NoColorBool bool) *ColorStruct { - var colors ColorStruct - colors = ColorStruct{ - NoColor : true, - Reset : "", - Green : "", - Yellow : "", - Blue : "", - Purple : "", - } - if !NoColorBool { - colors = ColorStruct{ - NoColor : false, - Reset : "\033[0m", - Green : "\033[32m", - Yellow : "\033[33m", - Blue : "\033[34m", - Purple : "\033[35m", - } - } - return &colors +func NoColorCheck(noColorBool bool) *ColorStruct { + var colors ColorStruct + colors = ColorStruct{ + NoColor: true, + Reset: "", + Green: "", + Yellow: "", + Blue: "", + Purple: "", + } + if !noColorBool { + colors = ColorStruct{ + NoColor: false, + Reset: "\033[0m", + Green: "\033[32m", + Yellow: "\033[33m", + Blue: "\033[34m", + Purple: "\033[35m", + } + } + return &colors } - diff --git a/src/common/json/json_writer.go b/src/common/json/json_writer.go index 11d7babef..4d58c8fb5 100644 --- a/src/common/json/json_writer.go +++ b/src/common/json/json_writer.go @@ -91,7 +91,8 @@ func AddTagsToResourceStr(fullOriginStr string, resourceBlock structure.IBlock, firstTagStr := tagsStr[firstTagIndex : firstTagIndex+strings.Index(tagsStr[firstTagIndex+1:], "\"")] tagEntryIndent := findIndent(tagsStr, '"', strings.Index(tagsStr[1:], "{")) // find the indent of the key and value entry compact := false - if strings.Contains(firstTagStr, "\n") { + switch { + case strings.Contains(firstTagStr, "\n"): // If the tag string has a newline, it means the indent needs to be re-evaluated. Example for this use case: // "Tags": [ // { @@ -102,10 +103,10 @@ func AddTagsToResourceStr(fullOriginStr string, resourceBlock structure.IBlock, indentDiff := len(tagEntryIndent) - len(tagBlockIndent) tagBlockIndent = tagBlockIndent[0 : len(tagBlockIndent)-indentDiff] tagEntryIndent = tagEntryIndent[0 : len(tagEntryIndent)-indentDiff] - } else if len(tagsLinesList) == 1 { + case len(tagsLinesList) == 1: // multi tags in one line compact = true - } else { + default: // Otherwise, need to take the indent of the "{" character. This case handles: // "Tags": [ // { "Key": "some-key", "Value": "some-val" } diff --git a/src/common/parser.go b/src/common/parser.go index b4982e296..74abec6e2 100644 --- a/src/common/parser.go +++ b/src/common/parser.go @@ -10,5 +10,6 @@ type IParser interface { WriteFile(readFilePath string, blocks []structure.IBlock, writeFilePath string) error GetSkippedDirs() []string GetSupportedFileExtensions() []string + GetSkipResourcesByComment() []string Close() } diff --git a/src/common/reports/report_service.go b/src/common/reports/report_service.go index b87236a6a..e66f800a3 100644 --- a/src/common/reports/report_service.go +++ b/src/common/reports/report_service.go @@ -149,16 +149,16 @@ func (r *ReportService) printUpdatedResourcesToStdout(colors *common.ColorStruct fmt.Print(colors.Green, fmt.Sprintf("Updated Resource Traces (%v):\n", r.report.Summary.UpdatedResources), colors.Reset) table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"File", "Resource", "Tag Key", "Old Value", "Updated Value", "Yor ID"}) - if !colors.NoColor { - table.SetColumnColor( - tablewriter.Colors{}, - tablewriter.Colors{}, - tablewriter.Colors{tablewriter.Bold}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgRedColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, - tablewriter.Colors{}, - ) - } + if !colors.NoColor { + table.SetColumnColor( + tablewriter.Colors{}, + tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Normal, tablewriter.FgRedColor}, + tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, + tablewriter.Colors{}, + ) + } table.SetRowLine(true) table.SetRowSeparator("-") @@ -176,15 +176,15 @@ func (r *ReportService) printNewResourcesToStdout(colors *common.ColorStruct) { table.SetHeader([]string{"File", "Resource", "Tag Key", "Tag Value", "Yor ID"}) table.SetRowLine(true) table.SetRowSeparator("-") - if !colors.NoColor { - table.SetColumnColor( - tablewriter.Colors{}, - tablewriter.Colors{}, - tablewriter.Colors{tablewriter.Bold}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, - tablewriter.Colors{}, - ) - } + if !colors.NoColor { + table.SetColumnColor( + tablewriter.Colors{}, + tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, + tablewriter.Colors{}, + ) + } for _, tr := range r.report.NewResourceTags { table.Append([]string{tr.File, tr.ResourceID, tr.TagKey, tr.UpdatedValue, tr.YorTraceID}) } diff --git a/src/common/reports/results_test.go b/src/common/reports/results_test.go index 000ae4a9a..f6cd502a4 100644 --- a/src/common/reports/results_test.go +++ b/src/common/reports/results_test.go @@ -120,7 +120,7 @@ func TestResultsGeneration(t *testing.T) { t.Run("Test CLI output structure", func(t *testing.T) { ReportServiceInst.CreateReport() - colors := common.NoColorCheck(false) + colors := common.NoColorCheck(false) output := utils.CaptureOutputColors(ReportServiceInst.PrintToStdout) lines := strings.Split(output, "\n") // Verify banner diff --git a/src/common/runner/runner.go b/src/common/runner/runner.go index e86937032..92611b0f0 100644 --- a/src/common/runner/runner.go +++ b/src/common/runner/runner.go @@ -160,12 +160,18 @@ func (r *Runner) isSkippedResourceType(resourceType string) bool { return false } -func (r *Runner) isSkippedResource(resource string) bool { +func (r *Runner) isSkippedResource(resource string, skipResource []string) bool { for _, skippedResource := range r.skippedResources { if resource == skippedResource { return true } } + for _, skippedResource := range skipResource { + if resource == skippedResource { + return true + } + } + return false } @@ -186,7 +192,8 @@ func (r *Runner) TagFile(file string) { if r.isSkippedResourceType(block.GetResourceType()) { continue } - if r.isSkippedResource(block.GetResourceID()) { + skipResourcesByComment := parser.GetSkipResourcesByComment() + if r.isSkippedResource(block.GetResourceID(), skipResourcesByComment) { continue } if block.IsBlockTaggable() { diff --git a/src/common/runner/runner_test.go b/src/common/runner/runner_test.go index 05fe706b1..55fc88946 100644 --- a/src/common/runner/runner_test.go +++ b/src/common/runner/runner_test.go @@ -2,13 +2,14 @@ package runner import ( "fmt" - "github.com/bridgecrewio/yor/src/common/tagging/tags" "os" "path/filepath" "strings" "testing" "time" + "github.com/bridgecrewio/yor/src/common/tagging/tags" + cloudformationStructure "github.com/bridgecrewio/yor/src/cloudformation/structure" "github.com/bridgecrewio/yor/src/common/clioptions" "github.com/bridgecrewio/yor/src/common/gitservice" @@ -220,7 +221,6 @@ func TestRunnerInternals(t *testing.T) { }) assert.NotContains(t, output, "aws_s3_bucket.test-bucket") }) - t.Run("Test skip resource - cloudformation", func(t *testing.T) { runner := Runner{} rootDir := "../../../tests/cloudformation" @@ -286,6 +286,9 @@ func Test_YorNameTag(t *testing.T) { runner := Runner{} err := runner.Init(&options) + if err != nil { + t.Error(err) + } reportService, err := runner.TagDirectory() if err != nil { t.Error(err) diff --git a/src/common/tagging/external/tag_group.go b/src/common/tagging/external/tag_group.go index e503882dd..40adec6af 100644 --- a/src/common/tagging/external/tag_group.go +++ b/src/common/tagging/external/tag_group.go @@ -78,9 +78,9 @@ func (t Tag) SatisfyFilters(block structure.IBlock) bool { case "directory": prefixes := make([]string, 0) - switch filterValue.(type) { + switch filterValue := filterValue.(type) { case []interface{}: - for _, e := range filterValue.([]interface{}) { + for _, e := range filterValue { prefixes = append(prefixes, e.(string)) } case interface{}: @@ -244,7 +244,7 @@ func (t *TagGroup) CalculateTagValue(block structure.IBlock, tag Tag) (tags.ITag } } if len(gitModifiersCounts) == 1 { - for k, _ := range gitModifiersCounts { + for k := range gitModifiersCounts { retTag.Value = evaluateTemplateVariable(k) break } diff --git a/src/common/utils/utils_test.go b/src/common/utils/utils_test.go index e2a4de2c0..dade664b4 100644 --- a/src/common/utils/utils_test.go +++ b/src/common/utils/utils_test.go @@ -109,11 +109,11 @@ func TestInSlice(t *testing.T) { args: args[int]{slice: []int{1, 2, 3, 4}, elem: 5}, want: false, }, - //{ // not supported for generics + // { // not supported for generics // name: "different kinds", // args: args[int]{slice: []int{1, 2, 3, 4}, elem: "bana"}, // want: false, - //}, + // }, } for _, tt := range testsStr { diff --git a/src/common/yaml/yaml_writer.go b/src/common/yaml/yaml_writer.go index eac890360..17fee1db6 100644 --- a/src/common/yaml/yaml_writer.go +++ b/src/common/yaml/yaml_writer.go @@ -266,8 +266,9 @@ func FindTagsLinesYAML(textLines []string, tagsAttributeName string) (structure. return tagsLines, tagsExist } -func MapResourcesLineYAML(filePath string, resourceNames []string, resourcesStartToken string) map[string]*structure.Lines { +func MapResourcesLineYAML(filePath string, resourceNames []string, resourcesStartToken string) (map[string]*structure.Lines, []string) { resourceToLines := make(map[string]*structure.Lines) + skipResourcesByComment := make([]string, 0) for _, resourceName := range resourceNames { // initialize a map between resource name and its lines in file resourceToLines[resourceName] = &structure.Lines{Start: -1, End: -1} @@ -276,7 +277,7 @@ func MapResourcesLineYAML(filePath string, resourceNames []string, resourcesStar file, err := os.ReadFile(filePath) if err != nil { logger.Warning(fmt.Sprintf("failed to read file %s", filePath)) - return nil + return nil, skipResourcesByComment } readResources := false @@ -287,12 +288,22 @@ func MapResourcesLineYAML(filePath string, resourceNames []string, resourcesStar for i, line := range fileLines { cleanContent := strings.TrimSpace(line) if strings.HasPrefix(cleanContent, resourcesStartToken+":") { + if strings.ToUpper(strings.TrimSpace(fileLines[i-1])) == "#YOR:SKIPALL" { + skipResourcesByComment = append(skipResourcesByComment, resourceNames...) + } readResources = true resourcesIndent = countLeadingSpaces(line) continue } if readResources { + if i > 0 { + if strings.ToUpper(strings.TrimSpace(fileLines[i-1])) == "#YOR:SKIP" { + + skipResourcesByComment = append(skipResourcesByComment, strings.Trim(strings.TrimSpace(line), ":")) + + } + } lineIndent := countLeadingSpaces(line) if lineIndent <= resourcesIndent && strings.TrimSpace(line) != "" && !strings.Contains(line, "#") { // No longer inside resources block, get the last line of the previous resource if exists @@ -325,7 +336,7 @@ func MapResourcesLineYAML(filePath string, resourceNames []string, resourcesStar // Handle last line of resource is last line of file resourceToLines[latestResourceName].End = findLastNonEmptyLine(fileLines, len(fileLines)-1) } - return resourceToLines + return resourceToLines, skipResourcesByComment } func countLeadingSpaces(line string) int { diff --git a/src/common/yaml/yaml_writer_test.go b/src/common/yaml/yaml_writer_test.go index a83b7925c..97cf5d53e 100644 --- a/src/common/yaml/yaml_writer_test.go +++ b/src/common/yaml/yaml_writer_test.go @@ -228,16 +228,40 @@ func TestTagReplacement(t *testing.T) { }) t.Run("Test line computation with duplicate - CFN", func(t *testing.T) { - res := MapResourcesLineYAML("../../../tests/cloudformation/resources/duplicate_entries/duplicate_cfn.yaml", []string{"S3Bucket", "CloudFrontDistribution"}, "Resources") + res, _ := MapResourcesLineYAML("../../../tests/cloudformation/resources/duplicate_entries/duplicate_cfn.yaml", []string{"S3Bucket", "CloudFrontDistribution"}, "Resources") assert.Equal(t, *res["S3Bucket"], structure.Lines{Start: 14, End: 17}) assert.Equal(t, *res["CloudFrontDistribution"], structure.Lines{Start: 18, End: 60}) }) t.Run("Test line computation with duplicate - SLS", func(t *testing.T) { - res := MapResourcesLineYAML("../../../tests/cloudformation/resources/duplicate_entries/duplicate_sls.yaml", []string{"attribute", "zone", "customer", "apiVersion"}, "functions") + res, _ := MapResourcesLineYAML("../../../tests/cloudformation/resources/duplicate_entries/duplicate_sls.yaml", []string{"attribute", "zone", "customer", "apiVersion"}, "functions") assert.Equal(t, *res["apiVersion"], structure.Lines{Start: 7, End: 12}) assert.Equal(t, *res["customer"], structure.Lines{Start: 14, End: 24}) assert.Equal(t, *res["zone"], structure.Lines{Start: 26, End: 38}) assert.Equal(t, *res["attribute"], structure.Lines{Start: 40, End: 53}) }) } + +func TestYaml_SkipResourceByComment(t *testing.T) { + t.Run("Test some resources with skip comment added to skipResourcesByComment slice", func(t *testing.T) { + filePath := "../../../tests/cloudformation/resources/skipComment/skipOne.yaml" + resorseSkip := []string{"NewVolume"} + expectedResourceNames := []string{"NewVolume"} + _, skipResourceByComment := MapResourcesLineYAML(filePath, expectedResourceNames, "Resources") + assert.Equal(t, skipResourceByComment, resorseSkip) + assert.NotEqual(t, skipResourceByComment, "NewVolume2") + }) + t.Run("All resources with skip comment added to skipResourcesByComment slice", func(t *testing.T) { + filePath := "../../../tests/cloudformation/resources/skipComment/skipAll.yaml" + resorseSkip := []string{"NewVolume", "NewVolume2"} + expectedResourceNames := []string{"NewVolume", "NewVolume2"} + _, skipResourceByComment := MapResourcesLineYAML(filePath, expectedResourceNames, "Resources") + assert.Equal(t, skipResourceByComment, resorseSkip) + }) + t.Run("No resources with skip all comment in the file, skipResourcesByComment slice should be empty", func(t *testing.T) { + filePath := "../../../tests/cloudformation/resources/skipComment/noSkip.yaml" + expectedResourceNames := []string{"NewVolume"} + _, skipResourceByComment := MapResourcesLineYAML(filePath, expectedResourceNames, "Resources") + assert.Empty(t, skipResourceByComment) + }) +} diff --git a/src/serverless/structure/serverless_parser.go b/src/serverless/structure/serverless_parser.go index d8cb7537f..1006128b7 100644 --- a/src/serverless/structure/serverless_parser.go +++ b/src/serverless/structure/serverless_parser.go @@ -24,7 +24,8 @@ const FunctionsSectionName = "functions" const FunctionType = "function" type ServerlessParser struct { - YamlParser types.YamlParser + YamlParser types.YamlParser + skippedByCommentList []string } var slsParseLock sync.Mutex @@ -72,6 +73,7 @@ func (p *ServerlessParser) ValidFile(file string) bool { } func (p *ServerlessParser) ParseFile(filePath string) ([]structure.IBlock, error) { + var skipResourcesByComment []string parsedBlocks := make([]structure.IBlock, 0) fileFormat := utils.GetFileFormat(filePath) fileName := filepath.Base(filePath) @@ -101,7 +103,8 @@ func (p *ServerlessParser) ParseFile(filePath string) ([]structure.IBlock, error } switch utils.GetFileFormat(filePath) { case common.YmlFileType.FileFormat, common.YamlFileType.FileFormat: - resourceNamesToLines = yamlUtils.MapResourcesLineYAML(filePath, resourceNames, FunctionsSectionName) + resourceNamesToLines, skipResourcesByComment = yamlUtils.MapResourcesLineYAML(filePath, resourceNames, FunctionsSectionName) + p.skippedByCommentList = append(p.skippedByCommentList, skipResourcesByComment...) default: return nil, fmt.Errorf("unsupported file type %s", utils.GetFileFormat(filePath)) } @@ -143,6 +146,9 @@ func (p *ServerlessParser) ParseFile(filePath string) ([]structure.IBlock, error } return parsedBlocks, nil } +func (p *ServerlessParser) GetSkipResourcesByComment() []string { + return p.skippedByCommentList +} func (p *ServerlessParser) WriteFile(readFilePath string, blocks []structure.IBlock, writeFilePath string) error { for _, block := range blocks { diff --git a/src/terraform/structure/terraform_parser.go b/src/terraform/structure/terraform_parser.go index e5f18dde7..1700b7487 100644 --- a/src/terraform/structure/terraform_parser.go +++ b/src/terraform/structure/terraform_parser.go @@ -30,7 +30,7 @@ var unsupportedTerraformBlocks = []string{ "aws_lb_listener", // This resource does not support tags, although docs state otherwise. "aws_lb_listener_rule", // This resource does not support tags, although docs state otherwise. "aws_cloudwatch_log_destination", // This resource does not support tags, although docs state otherwise. - "google_monitoring_notification_channel", //This resource uses labels for other purposes. + "google_monitoring_notification_channel", // This resource uses labels for other purposes. "aws_secretsmanager_secret_rotation", // This resource does not support tags, although tfschema states otherwise. } @@ -48,6 +48,7 @@ type TerraformParser struct { moduleInstallDir string downloadedPaths []string tfClientLock sync.Mutex + skippedByCommentList []string } func (p *TerraformParser) Name() string { @@ -132,6 +133,7 @@ func (p *TerraformParser) ParseFile(filePath string) ([]structure.IBlock, error) if err != nil { return nil, fmt.Errorf("failed to read file %s because %s", filePath, err) } + lines := strings.Split(string(src), "\n") // parse the file into hclwrite.File and hclsyntax.File to allow getting existing tags and lines hclFile, diagnostics := hclwrite.ParseConfig(src, filePath, hcl.InitialPos) @@ -151,6 +153,7 @@ func (p *TerraformParser) ParseFile(filePath string) ([]structure.IBlock, error) syntaxBlocks := hclSyntaxFile.Body.(*hclsyntax.Body).Blocks + skipAll := false rawBlocks := hclFile.Body().Blocks() parsedBlocks := make([]structure.IBlock, 0) for i, block := range rawBlocks { @@ -174,11 +177,25 @@ func (p *TerraformParser) ParseFile(filePath string) ([]structure.IBlock, error) } terraformBlock.Init(filePath, block) terraformBlock.AddHclSyntaxBlock(syntaxBlocks[i]) + line := terraformBlock.GetLines().Start + if line > 1 && line <= len(lines) { + lineAbove := lines[line-2] + if strings.ToUpper(strings.TrimSpace(lineAbove)) == "#YOR:SKIPALL" { + skipAll = true + } + + if strings.ToUpper(strings.TrimSpace(lineAbove)) == "#YOR:SKIP" || skipAll { + p.skippedByCommentList = append(p.skippedByCommentList, terraformBlock.GetResourceID()) + } + } parsedBlocks = append(parsedBlocks, terraformBlock) } return parsedBlocks, nil } +func (p *TerraformParser) GetSkipResourcesByComment() []string { + return p.skippedByCommentList +} func (p *TerraformParser) WriteFile(readFilePath string, blocks []structure.IBlock, writeFilePath string) error { // #nosec G304 @@ -266,7 +283,7 @@ func (p *TerraformParser) modifyBlockTags(rawBlock *hclwrite.Block, parsedBlock tagsAttributeName := parsedBlock.(*TerraformBlock).TagsAttributeName tagsAttribute := rawBlock.Body().GetAttribute(tagsAttributeName) - //we don't add tags to data sources + // we don't add tags to data sources if rawBlock.Type() == "data" { return } @@ -343,7 +360,7 @@ func (p *TerraformParser) modifyBlockTags(rawBlock *hclwrite.Block, parsedBlock // => we should replace it! rawTagsTokens = newTagsTokens // checkov:skip=CKV_SECRET_6 false positive } else { - rawTagsTokens = InsertTokens(rawTagsTokens, newTagsTokens[2:len(newTagsTokens)-2]) + rawTagsTokens = InsertTokens(rawTagsTokens, newTagsTokens[2:len(newTagsTokens)-2]) // checkov:skip=CKV_SECRET_80 false positive } rawBlock.Body().SetAttributeRaw(tagsAttributeName, rawTagsTokens) return @@ -566,7 +583,7 @@ func ExtractProviderFromModuleSrc(source string) string { } if isTerraformRegistryModule(source) { matches := utils.FindSubMatchByGroup(RegistryModuleRegex, source) - val, _ := matches["PROVIDER"] + val := matches["PROVIDER"] return val } withoutRef := strings.Split(source, "//")[0] diff --git a/src/terraform/structure/terraform_parser_test.go b/src/terraform/structure/terraform_parser_test.go index 4e0545922..4c1fcf754 100644 --- a/src/terraform/structure/terraform_parser_test.go +++ b/src/terraform/structure/terraform_parser_test.go @@ -22,6 +22,49 @@ import ( "github.com/stretchr/testify/assert" ) +func TestTerraformParser_SkipResourceByComment(t *testing.T) { + t.Run("SkipAll comment added all resources to skipResourcesByComment slice", func(t *testing.T) { + // Initialize TerraformParser and parse file with all resources containing skip comment + p := &TerraformParser{} + p.Init("../../../tests/terraform/skipComment/", nil) + defer p.Close() + filePath := "../../../tests/terraform/skipComment/skipAll.tf" + _, err := p.ParseFile(filePath) + if err != nil { + t.Errorf("failed to read hcl file because %s", err) + } + exceptedSkipResources := []string{"aws_vpc.example_vpc", "aws_subnet.example_subnet", "aws_instance.example_instance"} + assert.Equal(t, exceptedSkipResources, p.GetSkipResourcesByComment()) + }) + + t.Run("No resources with skip comment in the file, skipResourcesByComment slice should be empty", func(t *testing.T) { + // Initialize TerraformParser and parse file with no skip tags + p := &TerraformParser{} + p.Init("../../../tests/terraform/skipComment/", nil) + defer p.Close() + filePath := "../../../tests/terraform/skipComment/noSkip.tf" + _, err := p.ParseFile(filePath) + if err != nil { + t.Errorf("failed to read hcl file because %s", err) + } + assert.Empty(t, p.GetSkipResourcesByComment()) + }) + + t.Run("One resource with skip comment, only that resource added to skipResourcesByComment slice", func(t *testing.T) { + // Initialize TerraformParser and parse file with one resource containing skip tag + p := &TerraformParser{} + p.Init("../../../tests/terraform/skipComment/", nil) + defer p.Close() + filePath := "../../../tests/terraform/skipComment/skipOne.tf" + _, err := p.ParseFile(filePath) + if err != nil { + t.Errorf("failed to read hcl file because %s", err) + } + exceptedSkipResources := []string{"aws_instance.example_instance"} + assert.Equal(t, exceptedSkipResources, p.GetSkipResourcesByComment()) + }) +} + func TestTerraformParser_ParseFile(t *testing.T) { t.Run("parse aws eks file", func(t *testing.T) { p := &TerraformParser{} diff --git a/tests/cloudformation/resources/skipComment/noSkip.yaml b/tests/cloudformation/resources/skipComment/noSkip.yaml new file mode 100644 index 000000000..b56b83fc4 --- /dev/null +++ b/tests/cloudformation/resources/skipComment/noSkip.yaml @@ -0,0 +1,36 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Sample EBS Volume with EC2 instance template +Resources: + NewVolume: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + + NewVolume2: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + +Outputs: + VolumeId: + Value: !Ref NewVolume + Export: + Name: NewVolumeId \ No newline at end of file diff --git a/tests/cloudformation/resources/skipComment/skipAll.yaml b/tests/cloudformation/resources/skipComment/skipAll.yaml new file mode 100644 index 000000000..4e84696b0 --- /dev/null +++ b/tests/cloudformation/resources/skipComment/skipAll.yaml @@ -0,0 +1,38 @@ + +AWSTemplateFormatVersion: '2010-09-09' +Description: Sample EBS Volume with EC2 instance template +#yor:skipAll +Resources: + NewVolume: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + + NewVolume2: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + +Outputs: + VolumeId: + Value: !Ref NewVolume + Export: + Name: NewVolumeId diff --git a/tests/cloudformation/resources/skipComment/skipOne.yaml b/tests/cloudformation/resources/skipComment/skipOne.yaml new file mode 100644 index 000000000..789df3ca2 --- /dev/null +++ b/tests/cloudformation/resources/skipComment/skipOne.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Sample EBS Volume with EC2 instance template +Resources: +#yor:skip + NewVolume: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + + NewVolume2: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + Tags: + - Key: MyTag + Value: TagValue + - Key: Name + Value: !Ref EnvironmentName + AvailabilityZone: us-west-2a + DeletionPolicy: Snapshot + + +Outputs: + VolumeId: + Value: !Ref NewVolume + Export: + Name: NewVolumeId \ No newline at end of file diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 935ee04c1..41f6d5b8d 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -254,6 +254,36 @@ func TestRunResults(t *testing.T) { } } }) + +} + +func TestSkipResourcesByComment(t *testing.T) { + t.Run("Test tagging terraform and cloudFormation files and skip resource by comment", func(t *testing.T) { + yorRunner := runner.Runner{} + err := yorRunner.Init(&clioptions.TagOptions{ + Directory: "./skipComment", + TagGroups: getTagGroups(), + Parsers: []string{"Terraform", "CloudFormation"}, + }) + tfFileBytes, _ := os.ReadFile("./skipComment/skipResource.tf") + yamlFileBytes, _ := os.ReadFile("./skipComment/skipResource.yaml") + defer func() { + _ = os.WriteFile("./skipComment/skipResource.tf", tfFileBytes, 0644) + _ = os.WriteFile("./skipComment/skipResource.yaml", yamlFileBytes, 0644) + }() + failIfErr(t, err) + reportService, err := yorRunner.TagDirectory() + failIfErr(t, err) + + reportService.CreateReport() + report := reportService.GetReport() + + newTags := report.NewResourceTags + for _, newTag := range newTags { + assert.NotEqual(t, "aws_vpc.example_vpc", newTag.ResourceID) + assert.NotEqual(t, "NewVolume1", newTag.ResourceID) + } + }) } func TestTagUncommittedResults(t *testing.T) { diff --git a/tests/integration/skipComment/skipResource.tf b/tests/integration/skipComment/skipResource.tf new file mode 100644 index 000000000..26c2f2237 --- /dev/null +++ b/tests/integration/skipComment/skipResource.tf @@ -0,0 +1,24 @@ +#yor:skip +resource "aws_vpc" "example_vpc" { + cidr_block = "10.0.0.0/16" + tags = { + } +} + +resource "aws_subnet" "example_subnet" { + vpc_id = aws_vpc.example_vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-west-1a" + tags = { + yor_name = "example_subnet" + yor_trace = "74091a97-d11a-4500-a0c3-af942a0d8e00" + } +} + +resource "aws_instance" "example_instance" { + ami = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + subnet_id = aws_subnet.example_subnet.id + tags = { + } +} \ No newline at end of file diff --git a/tests/integration/skipComment/skipResource.yaml b/tests/integration/skipComment/skipResource.yaml new file mode 100644 index 000000000..f4148e308 --- /dev/null +++ b/tests/integration/skipComment/skipResource.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Sample EBS Volume with EC2 instance template +Resources: + NewVolume: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + AvailabilityZone: us-west-2a + Tags: + - Key: yor_trace + Value: d5e1032c-34e9-428d-8b17-4dff36d05e68 + - Key: yor_name + Value: NewVolume +#yor:skip + NewVolume1: + Type: AWS::EC2::Volume + Properties: + Size: 100 + Encrypted: true + #Encrypted: false + AvailabilityZone: us-west-2a + Tags: + DeletionPolicy: Snapshot \ No newline at end of file diff --git a/tests/terraform/skipComment/noSkip.tf b/tests/terraform/skipComment/noSkip.tf new file mode 100644 index 000000000..0e999b427 --- /dev/null +++ b/tests/terraform/skipComment/noSkip.tf @@ -0,0 +1,15 @@ +resource "aws_vpc" "example_vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "example_subnet" { + vpc_id = aws_vpc.example_vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-west-1a" +} + +resource "aws_instance" "example_instance" { + ami = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + subnet_id = aws_subnet.example_subnet.id +} diff --git a/tests/terraform/skipComment/skipAll.tf b/tests/terraform/skipComment/skipAll.tf new file mode 100644 index 000000000..cf8a7f4e3 --- /dev/null +++ b/tests/terraform/skipComment/skipAll.tf @@ -0,0 +1,16 @@ +#YOR:SKIPALL +resource "aws_vpc" "example_vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "example_subnet" { + vpc_id = aws_vpc.example_vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-west-1a" +} + +resource "aws_instance" "example_instance" { + ami = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + subnet_id = aws_subnet.example_subnet.id +} \ No newline at end of file diff --git a/tests/terraform/skipComment/skipOne.tf b/tests/terraform/skipComment/skipOne.tf new file mode 100644 index 000000000..619b9ed96 --- /dev/null +++ b/tests/terraform/skipComment/skipOne.tf @@ -0,0 +1,15 @@ +resource "aws_vpc" "example_vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "example_subnet" { + vpc_id = aws_vpc.example_vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-west-1a" +} +#yor:Skip +resource "aws_instance" "example_instance" { + ami = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + subnet_id = aws_subnet.example_subnet.id +} \ No newline at end of file diff --git a/tests/utils/utils.go b/tests/utils/utils.go index 489c23c49..4ef37a5ba 100644 --- a/tests/utils/utils.go +++ b/tests/utils/utils.go @@ -6,7 +6,7 @@ import ( "log" "os" - "github.com/bridgecrewio/yor/src/common" + "github.com/bridgecrewio/yor/src/common" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) @@ -39,7 +39,7 @@ func CaptureOutput(f func()) string { } func CaptureOutputColors(f func(*common.ColorStruct)) string { - colors := common.NoColorCheck(false) + colors := common.NoColorCheck(false) reader, writer, err := os.Pipe() if err != nil { panic(err)