Skip to content

Commit

Permalink
Merge pull request #95 from padok-team/betterwhitelist
Browse files Browse the repository at this point in the history
feat(whitelist): on module
  • Loading branch information
Laudenlaruto authored Jan 7, 2025
2 parents 5967c87 + 7ab1ab7 commit 69cde76
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 108 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ tmp
guacamole
# Ignore modifications on examples
example/
.vscode
.vscode/
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ Three modes currently exist :
guacamole static -p /path/to/your/codebase
```

- By default, it will launch [module](#static-module-check) and [layer](#static-layer-check) checks
- To launch [layer](#static-layer-check) check use `guacamole static layer`
- To launch [module](#static-module-check) check use `guacamole static module`
- By default, it will launch [module](#static-module-check-for-terraform) and [layer](#static-layer-check-for-terragrunt) checks
- To launch [layer](#static-layer-check-for-terragrunt) check use `guacamole static layer`
- To launch [module](#static-module-check-for-terraform) check use `guacamole static module`

- [EXPERIMENTAL] State mode : runs quality checks based on your layers' state

Expand All @@ -63,14 +63,16 @@ Three modes currently exist :

A verbose mode (`-v`) exists to add more information to the output.

**Skipping individual checks**
### Skipping individual checks

You can use inline code comments to skip individual checks for a particular resource.

⚠️ Currently only supports static checks on modules for Terraform ⚠️

To skip a check on a given Terraform definition block resource, apply the following comment pattern inside its scope: `# guacamole-ignore:<check_id> <suppression_comment>`

<check_id> is one of the available check scanners.
<suppression_comment> is an optional suppression reason.
- <check_id> is one of the available check scanners.
- <suppression_comment> is an optional suppression reason.

Example:

Expand All @@ -82,7 +84,16 @@ resource "azurerm_resource_group" "network" {
name...
```
⚠️ The following checks can't be whitelisted : `TF_MOD_002`
You can also whitelist entire checks in **modules** by adding them to a `.guacamoleignore` file at the root of your codebase.
The format of the file should be: path of the `module - check ID` to ignore.
```bash
pathtomodule/modules/cloud-run-app TF_MOD_002
```
This is the only way to whitelist the check `TF_MOD_002`
You can specify the path of the `.guacamoleignore` file with the `-w` flag.
## List of checks
Expand Down
23 changes: 5 additions & 18 deletions checks/module_static_checks.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package checks

import (
"strings"
"sync"

"github.com/padok-team/guacamole/data"
Expand Down Expand Up @@ -44,11 +43,13 @@ func ModuleStaticChecks() []data.Check {
defer wg.Done()

check, err := checkFunction(modules)
// Apply whitelist on checks errors
// Apply ignore on Terraform code blocks checks errors
// This only cover Terraform resources that have a POS attribute
for i := len(check.Errors) - 1; i >= 0; i-- {
check, _ = applyWhitelist(check, i, modules)
check, _ = helpers.ApplyIgnoreOnCodeBlock(check, i, modules)
check, _ = helpers.ApplyIgnoreOnModule(check, i, modules)
}
// Replace the check error status with the array after whitelisting
// Replace the check error status with the array after ignoreing
if len(check.Errors) == 0 {
check.Status = "✅"
}
Expand Down Expand Up @@ -79,17 +80,3 @@ func ModuleStaticChecks() []data.Check {

return checkResults
}

func applyWhitelist(checks data.Check, indexOfCheckedcheck int, modules map[string]data.TerraformModule) (data.Check, error) {
for _, module := range modules {
for _, resource := range module.Resources {
for _, whitelist := range resource.WhitelistComments {
if strings.Contains(checks.Errors[indexOfCheckedcheck].Path, resource.FilePath) && checks.Errors[indexOfCheckedcheck].LineNumber == resource.Pos && checks.ID == whitelist.CheckID {
checks.Errors = append(checks.Errors[:indexOfCheckedcheck], checks.Errors[indexOfCheckedcheck+1:]...)
return checks, nil
}
}
}
}
return checks, nil
}
2 changes: 1 addition & 1 deletion checks/provider_in_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func ProviderInModule(modules map[string]data.TerraformModule) (data.Check, erro
if diags.HasErrors() {
return data.Check{}, diags.Err()
}
//If the module has no provider, display an error
//If the module has a provider, display an error
if len(moduleConf.ProviderConfigs) > 0 {
modulesInError = append(modulesInError, data.Error{
Path: module.FullPath,
Expand Down
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

var cfgFile string
var codebasePath string
var ignorePath string
var terraformBin string

// rootCmd represents the base command when called without any subcommands
Expand Down Expand Up @@ -47,8 +48,10 @@ func init() {
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().StringVarP(&codebasePath, "codebase-path", "p", ".", "path to your IaC codebase")
rootCmd.PersistentFlags().StringVarP(&ignorePath, "guacamoleignore-path", "w", ".guacamoleignore", "path to your ignore file")

viper.BindPFlag("codebase-path", rootCmd.PersistentFlags().Lookup("codebase-path"))
viper.BindPFlag("guacamoleignore-path", rootCmd.PersistentFlags().Lookup("guacamoleignore-path"))

rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Display verbose output")
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
Expand Down
12 changes: 12 additions & 0 deletions data/ignore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package data

type IgnoreComment struct {
CheckID string
LineNumber int
Path string
}

type IgnoreModule struct {
CheckID string
ModulePath string
}
11 changes: 6 additions & 5 deletions data/terraform_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ type TerraformModule struct {
FullPath string
ModuleConfig tfconfig.Module
Resources map[string]TerraformCodeBlock
Ignore []IgnoreModule
}

type TerraformCodeBlock struct {
Name string
ModulePath string
Pos int
FilePath string
WhitelistComments []WhitelistComment
Name string
ModulePath string
Pos int
FilePath string
IgnoreComments []IgnoreComment
}
7 changes: 0 additions & 7 deletions data/whitelist.go

This file was deleted.

97 changes: 28 additions & 69 deletions helpers/getters.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package helpers

import (
"bufio"
"fmt"
"os"
"path/filepath"
Expand All @@ -18,6 +17,7 @@ import (
func GetModules() (map[string]data.TerraformModule, error) {
codebasePath := viper.GetString("codebase-path")
modules := make(map[string]data.TerraformModule)
ignoreOnModule, _ := GetIgnoreingInFile()
//Get all subdirectories in root path
err := filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand All @@ -27,8 +27,8 @@ func GetModules() (map[string]data.TerraformModule, error) {
if !info.IsDir() && strings.HasSuffix(info.Name(), ".tf") {
// exclude the files which are in the .terragrunt-cache or .terraform directory
if !regexp.MustCompile(`\.terragrunt-cache|\.terraform`).MatchString(path) {
// Get all whitelisting comments
whitelistCommentOfFile, _ := GetWhitelistingComments(path)
// Get all ignoreing comments
ignoreCommentOfFile, _ := GetIgnoreingComments(path)
// Check if the module is already in the list
alreadyInList := false
for _, m := range modules {
Expand All @@ -39,6 +39,8 @@ func GetModules() (map[string]data.TerraformModule, error) {
// If not in list create the module
if !alreadyInList {
modules[filepath.Dir(path)], _ = LoadModule(filepath.Dir(path))
// Associate ignoreing comments on module from the .guacamoleignore file
AssociateIgnoreingCommentsOnModule(ignoreOnModule, filepath.Dir(path), modules)
}
// Create a temporary object with all resources in order
resourcesInFile := make(map[string]data.TerraformCodeBlock)
Expand All @@ -57,20 +59,8 @@ func GetModules() (map[string]data.TerraformModule, error) {
return resourcesInFile[keys[i]].Pos < resourcesInFile[keys[j]].Pos
})

// Associate the whitelisting comments to a resource (Resource, Data, Variable or Output)
for _, whitelistingComment := range whitelistCommentOfFile {
previousPos := 0
for _, i := range keys {
// Find closest code block within the file
if previousPos < whitelistingComment.LineNumber && whitelistingComment.LineNumber < resourcesInFile[i].Pos {
r := modules[filepath.Dir(path)].Resources[i]
r.WhitelistComments = append(r.WhitelistComments, whitelistingComment)
modules[filepath.Dir(path)].Resources[i] = r
break
}
previousPos = resourcesInFile[i].Pos
}
}
// Associate the ignoreing comments to a resource (Resource, Data, Variable or Output)
AssociateIgnoreingComments(ignoreCommentOfFile, keys, resourcesInFile, modules, path)
}
}
return nil
Expand Down Expand Up @@ -129,43 +119,44 @@ func LoadModule(path string) (data.TerraformModule, error) {
// Create map of code blocks (resources, data, variables and outputs) of module
for _, resource := range moduleConfig.ManagedResources {
resources[resource.Type+resource.Name] = data.TerraformCodeBlock{
Name: resource.Type + " " + resource.Name,
ModulePath: path,
Pos: resource.Pos.Line,
FilePath: resource.Pos.Filename,
WhitelistComments: []data.WhitelistComment{},
Name: resource.Type + " " + resource.Name,
ModulePath: path,
Pos: resource.Pos.Line,
FilePath: resource.Pos.Filename,
IgnoreComments: []data.IgnoreComment{},
}
}
// Load data
for _, resource := range moduleConfig.DataResources {
resources[resource.Type+resource.Name] = data.TerraformCodeBlock{
Name: resource.Type + " " + resource.Name,
ModulePath: path,
Pos: resource.Pos.Line,
WhitelistComments: []data.WhitelistComment{},
FilePath: resource.Pos.Filename,
Name: resource.Type + " " + resource.Name,
ModulePath: path,
Pos: resource.Pos.Line,
IgnoreComments: []data.IgnoreComment{},
FilePath: resource.Pos.Filename,
}
}
// Load variables
for _, variable := range moduleConfig.Variables {
resources["variable "+variable.Type+variable.Name] = data.TerraformCodeBlock{
Name: "variable " + variable.Type + " " + variable.Name,
ModulePath: path,
Pos: variable.Pos.Line,
WhitelistComments: []data.WhitelistComment{},
FilePath: variable.Pos.Filename,
Name: "variable " + variable.Type + " " + variable.Name,
ModulePath: path,
Pos: variable.Pos.Line,
IgnoreComments: []data.IgnoreComment{},
FilePath: variable.Pos.Filename,
}
}
// Load outputs
for _, output := range moduleConfig.Outputs {
resources["output"+output.Name] = data.TerraformCodeBlock{
Name: "output " + output.Name,
ModulePath: path,
Pos: output.Pos.Line,
WhitelistComments: []data.WhitelistComment{},
FilePath: output.Pos.Filename,
Name: "output " + output.Name,
ModulePath: path,
Pos: output.Pos.Line,
IgnoreComments: []data.IgnoreComment{},
FilePath: output.Pos.Filename,
}
}
// Assemble the module object
module := data.TerraformModule{
FullPath: path,
Name: filepath.Base(path),
Expand All @@ -176,36 +167,4 @@ func LoadModule(path string) (data.TerraformModule, error) {
return module, nil
}

func GetWhitelistingComments(path string) ([]data.WhitelistComment, error) {
whitelistComments := []data.WhitelistComment{}
// Parse the file to get whitelist comments
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
// Read the file and find comments containing guacamole-ignore
scanner := bufio.NewScanner(file)
i := 1 //Set cursor to the start of the file
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "guacamole-ignore") {
whitelistComment := data.WhitelistComment{}
// Regex to match the check ID in the form of TF/TG_XXX_0XX
regexp := regexp.MustCompile(`(T[F|G]_(\w+)_\d+)`)
match := regexp.FindStringSubmatch(line)
if len(match) > 0 {
whitelistComment.CheckID = match[0]
whitelistComment.LineNumber = i
whitelistComment.Path = path

// Attach comment to an object
}
whitelistComments = append(whitelistComments, whitelistComment)
}
i++
}
return whitelistComments, nil
}

// TODO: add init and plan layers function
Loading

0 comments on commit 69cde76

Please sign in to comment.