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  }