github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/rules/terraformrules/terraform_module_pinned_source.go (about) 1 package terraformrules 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "strings" 8 9 "github.com/hashicorp/terraform/configs" 10 "github.com/terraform-linters/tflint/tflint" 11 ) 12 13 // TerraformModulePinnedSourceRule checks unpinned or default version module source 14 type TerraformModulePinnedSourceRule struct { 15 attributeName string 16 } 17 18 type terraformModulePinnedSourceRuleConfig struct { 19 Style string `hcl:"style,optional"` 20 } 21 22 // NewTerraformModulePinnedSourceRule returns new rule with default attributes 23 func NewTerraformModulePinnedSourceRule() *TerraformModulePinnedSourceRule { 24 return &TerraformModulePinnedSourceRule{ 25 attributeName: "source", 26 } 27 } 28 29 // Name returns the rule name 30 func (r *TerraformModulePinnedSourceRule) Name() string { 31 return "terraform_module_pinned_source" 32 } 33 34 // Enabled returns whether the rule is enabled by default 35 func (r *TerraformModulePinnedSourceRule) Enabled() bool { 36 return true 37 } 38 39 // Severity returns the rule severity 40 func (r *TerraformModulePinnedSourceRule) Severity() string { 41 return tflint.WARNING 42 } 43 44 // Link returns the rule reference link 45 func (r *TerraformModulePinnedSourceRule) Link() string { 46 return tflint.ReferenceLink(r.Name()) 47 } 48 49 // ReGitHub matches a module source which is a GitHub repository 50 // See https://www.terraform.io/docs/modules/sources.html#github 51 var ReGitHub = regexp.MustCompile("(^github.com/(.+)/(.+)$)|(^git@github.com:(.+)/(.+)$)") 52 53 // ReBitbucket matches a module source which is a Bitbucket repository 54 // See https://www.terraform.io/docs/modules/sources.html#bitbucket 55 var ReBitbucket = regexp.MustCompile("^bitbucket.org/(.+)/(.+)$") 56 57 // ReGenericGit matches a module source which is a Git repository 58 // See https://www.terraform.io/docs/modules/sources.html#generic-git-repository 59 var ReGenericGit = regexp.MustCompile("(git://(.+)/(.+))|(git::https://(.+)/(.+))|(git::ssh://((.+)@)??(.+)/(.+)/(.+))") 60 61 var reSemverReference = regexp.MustCompile("\\?ref=v?\\d+\\.\\d+\\.\\d+$") 62 var reSemverRevision = regexp.MustCompile("\\?rev=v?\\d+\\.\\d+\\.\\d+$") 63 64 // Check checks if module source version is pinned 65 // Note that this rule is valid only for Git or Mercurial source 66 func (r *TerraformModulePinnedSourceRule) Check(runner *tflint.Runner) error { 67 if !runner.TFConfig.Path.IsRoot() { 68 // This rule does not evaluate child modules. 69 return nil 70 } 71 72 log.Printf("[TRACE] Check `%s` rule for `%s` runner", r.Name(), runner.TFConfigPath()) 73 74 config := terraformModulePinnedSourceRuleConfig{Style: "flexible"} 75 if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { 76 return err 77 } 78 79 var err error 80 for _, module := range runner.TFConfig.Module.ModuleCalls { 81 log.Printf("[DEBUG] Walk `%s` attribute", module.Name+".source") 82 83 lower := strings.ToLower(module.SourceAddr) 84 85 if ReGitHub.MatchString(lower) || ReGenericGit.MatchString(lower) { 86 err = r.checkGitSource(runner, module, config) 87 } else if ReBitbucket.MatchString(lower) { 88 err = r.checkBitbucketSource(runner, module, config) 89 } else if strings.HasPrefix(lower, "hg::") { 90 err = r.checkMercurialSource(runner, module, config) 91 } 92 93 if err != nil { 94 return err 95 } 96 } 97 98 return nil 99 } 100 101 func (r *TerraformModulePinnedSourceRule) checkGitSource(runner *tflint.Runner, module *configs.ModuleCall, config terraformModulePinnedSourceRuleConfig) error { 102 lower := strings.ToLower(module.SourceAddr) 103 104 if strings.Contains(lower, "ref=") { 105 return r.checkRefSource(runner, module, config) 106 } 107 108 runner.EmitIssue( 109 r, 110 fmt.Sprintf("Module source \"%s\" is not pinned", module.SourceAddr), 111 module.SourceAddrRange, 112 ) 113 return nil 114 } 115 116 func (r *TerraformModulePinnedSourceRule) checkMercurialSource(runner *tflint.Runner, module *configs.ModuleCall, config terraformModulePinnedSourceRuleConfig) error { 117 lower := strings.ToLower(module.SourceAddr) 118 119 if strings.Contains(lower, "rev=") { 120 return r.checkRevSource(runner, module, config) 121 } 122 123 runner.EmitIssue( 124 r, 125 fmt.Sprintf("Module source \"%s\" is not pinned", module.SourceAddr), 126 module.SourceAddrRange, 127 ) 128 return nil 129 } 130 131 // Terraform can use a Bitbucket repo as Git or Mercurial. 132 // 133 // Note: Bitbucket is dropping Mercurial support in 2020, so this can be rolled into 134 // checkGitSource after that happens. 135 func (r *TerraformModulePinnedSourceRule) checkBitbucketSource(runner *tflint.Runner, module *configs.ModuleCall, config terraformModulePinnedSourceRuleConfig) error { 136 lower := strings.ToLower(module.SourceAddr) 137 138 if strings.Contains(lower, "ref=") { 139 return r.checkRefSource(runner, module, config) 140 } else if strings.Contains(lower, "rev=") { 141 return r.checkRevSource(runner, module, config) 142 } else { 143 runner.EmitIssue( 144 r, 145 fmt.Sprintf("Module source \"%s\" is not pinned", module.SourceAddr), 146 module.SourceAddrRange, 147 ) 148 } 149 150 return nil 151 } 152 153 func (r *TerraformModulePinnedSourceRule) checkRefSource(runner *tflint.Runner, module *configs.ModuleCall, config terraformModulePinnedSourceRuleConfig) error { 154 lower := strings.ToLower(module.SourceAddr) 155 156 switch config.Style { 157 // The "flexible" style enforces to pin source, except for the default branch 158 case "flexible": 159 if strings.Contains(lower, "ref=master") { 160 runner.EmitIssue( 161 r, 162 fmt.Sprintf("Module source \"%s\" uses default ref \"master\"", module.SourceAddr), 163 module.SourceAddrRange, 164 ) 165 } 166 // The "semver" style enforces to pin source like semantic versioning 167 case "semver": 168 if !reSemverReference.MatchString(lower) { 169 runner.EmitIssue( 170 r, 171 fmt.Sprintf("Module source \"%s\" uses a ref which is not a version string", module.SourceAddr), 172 module.SourceAddrRange, 173 ) 174 } 175 default: 176 return fmt.Errorf("`%s` is invalid style", config.Style) 177 } 178 179 return nil 180 } 181 182 func (r *TerraformModulePinnedSourceRule) checkRevSource(runner *tflint.Runner, module *configs.ModuleCall, config terraformModulePinnedSourceRuleConfig) error { 183 lower := strings.ToLower(module.SourceAddr) 184 185 switch config.Style { 186 // The "flexible" style enforces to pin source, except for the default reference 187 case "flexible": 188 if strings.Contains(lower, "rev=default") { 189 runner.EmitIssue( 190 r, 191 fmt.Sprintf("Module source \"%s\" uses default rev \"default\"", module.SourceAddr), 192 module.SourceAddrRange, 193 ) 194 } 195 // The "semver" style enforces to pin source like semantic versioning 196 case "semver": 197 if !reSemverRevision.MatchString(lower) { 198 runner.EmitIssue( 199 r, 200 fmt.Sprintf("Module source \"%s\" uses a rev which is not a version string", module.SourceAddr), 201 module.SourceAddrRange, 202 ) 203 } 204 default: 205 return fmt.Errorf("`%s` is invalid style", config.Style) 206 } 207 208 return nil 209 }