gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/github.com/securego/gosec/analyzer.go (about)

     1  // (c) Copyright 2016 Hewlett Packard Enterprise Development LP
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package gosec holds the central scanning logic used by gosec security scanner
    16  package gosec
    17  
    18  import (
    19  	"go/ast"
    20  	"go/build"
    21  	"go/parser"
    22  	"go/token"
    23  	"go/types"
    24  	"log"
    25  	"os"
    26  	"path"
    27  	"reflect"
    28  	"regexp"
    29  	"strings"
    30  
    31  	"golang.org/x/tools/go/loader"
    32  )
    33  
    34  // The Context is populated with data parsed from the source code as it is scanned.
    35  // It is passed through to all rule functions as they are called. Rules may use
    36  // this data in conjunction withe the encoutered AST node.
    37  type Context struct {
    38  	FileSet  *token.FileSet
    39  	Comments ast.CommentMap
    40  	Info     *types.Info
    41  	Pkg      *types.Package
    42  	Root     *ast.File
    43  	Config   map[string]interface{}
    44  	Imports  *ImportTracker
    45  	Ignores  []map[string]bool
    46  }
    47  
    48  // Metrics used when reporting information about a scanning run.
    49  type Metrics struct {
    50  	NumFiles int `json:"files"`
    51  	NumLines int `json:"lines"`
    52  	NumNosec int `json:"nosec"`
    53  	NumFound int `json:"found"`
    54  }
    55  
    56  // Analyzer object is the main object of gosec. It has methods traverse an AST
    57  // and invoke the correct checking rules as on each node as required.
    58  type Analyzer struct {
    59  	ignoreNosec bool
    60  	ruleset     RuleSet
    61  	context     *Context
    62  	config      Config
    63  	logger      *log.Logger
    64  	issues      []*Issue
    65  	stats       *Metrics
    66  }
    67  
    68  // NewAnalyzer builds a new anaylzer.
    69  func NewAnalyzer(conf Config, logger *log.Logger) *Analyzer {
    70  	ignoreNoSec := false
    71  	if setting, err := conf.GetGlobal("nosec"); err == nil {
    72  		ignoreNoSec = setting == "true" || setting == "enabled"
    73  	}
    74  	if logger == nil {
    75  		logger = log.New(os.Stderr, "[gosec]", log.LstdFlags)
    76  	}
    77  	return &Analyzer{
    78  		ignoreNosec: ignoreNoSec,
    79  		ruleset:     make(RuleSet),
    80  		context:     &Context{},
    81  		config:      conf,
    82  		logger:      logger,
    83  		issues:      make([]*Issue, 0, 16),
    84  		stats:       &Metrics{},
    85  	}
    86  }
    87  
    88  // LoadRules instantiates all the rules to be used when analyzing source
    89  // packages
    90  func (gosec *Analyzer) LoadRules(ruleDefinitions map[string]RuleBuilder) {
    91  	for id, def := range ruleDefinitions {
    92  		r, nodes := def(id, gosec.config)
    93  		gosec.ruleset.Register(r, nodes...)
    94  	}
    95  }
    96  
    97  // Process kicks off the analysis process for a given package
    98  func (gosec *Analyzer) Process(buildTags []string, packagePaths ...string) error {
    99  	ctx := build.Default
   100  	ctx.BuildTags = append(ctx.BuildTags, buildTags...)
   101  	packageConfig := loader.Config{
   102  		Build:       &ctx,
   103  		ParserMode:  parser.ParseComments,
   104  		AllowErrors: true,
   105  	}
   106  	for _, packagePath := range packagePaths {
   107  		abspath, err := GetPkgAbsPath(packagePath)
   108  		if err != nil {
   109  			gosec.logger.Printf("Skipping: %s. Path doesn't exist.", abspath)
   110  			continue
   111  		}
   112  		gosec.logger.Println("Searching directory:", abspath)
   113  
   114  		basePackage, err := build.Default.ImportDir(packagePath, build.ImportComment)
   115  		if err != nil {
   116  			return err
   117  		}
   118  
   119  		var packageFiles []string
   120  		for _, filename := range basePackage.GoFiles {
   121  			packageFiles = append(packageFiles, path.Join(packagePath, filename))
   122  		}
   123  
   124  		packageConfig.CreateFromFilenames(basePackage.Name, packageFiles...)
   125  	}
   126  
   127  	builtPackage, err := packageConfig.Load()
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	for _, pkg := range builtPackage.Created {
   133  		gosec.logger.Println("Checking package:", pkg.String())
   134  		for _, file := range pkg.Files {
   135  			gosec.logger.Println("Checking file:", builtPackage.Fset.File(file.Pos()).Name())
   136  			gosec.context.FileSet = builtPackage.Fset
   137  			gosec.context.Config = gosec.config
   138  			gosec.context.Comments = ast.NewCommentMap(gosec.context.FileSet, file, file.Comments)
   139  			gosec.context.Root = file
   140  			gosec.context.Info = &pkg.Info
   141  			gosec.context.Pkg = pkg.Pkg
   142  			gosec.context.Imports = NewImportTracker()
   143  			gosec.context.Imports.TrackPackages(gosec.context.Pkg.Imports()...)
   144  			ast.Walk(gosec, file)
   145  			gosec.stats.NumFiles++
   146  			gosec.stats.NumLines += builtPackage.Fset.File(file.Pos()).LineCount()
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  // ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
   153  func (gosec *Analyzer) ignore(n ast.Node) ([]string, bool) {
   154  	if groups, ok := gosec.context.Comments[n]; ok && !gosec.ignoreNosec {
   155  		for _, group := range groups {
   156  			if strings.Contains(group.Text(), "#nosec") {
   157  				gosec.stats.NumNosec++
   158  
   159  				// Pull out the specific rules that are listed to be ignored.
   160  				re := regexp.MustCompile("(G\\d{3})")
   161  				matches := re.FindAllStringSubmatch(group.Text(), -1)
   162  
   163  				// If no specific rules were given, ignore everything.
   164  				if matches == nil || len(matches) == 0 {
   165  					return nil, true
   166  				}
   167  
   168  				// Find the rule IDs to ignore.
   169  				var ignores []string
   170  				for _, v := range matches {
   171  					ignores = append(ignores, v[1])
   172  				}
   173  				return ignores, false
   174  			}
   175  		}
   176  	}
   177  	return nil, false
   178  }
   179  
   180  // Visit runs the gosec visitor logic over an AST created by parsing go code.
   181  // Rule methods added with AddRule will be invoked as necessary.
   182  func (gosec *Analyzer) Visit(n ast.Node) ast.Visitor {
   183  	// If we've reached the end of this branch, pop off the ignores stack.
   184  	if n == nil {
   185  		if len(gosec.context.Ignores) > 0 {
   186  			gosec.context.Ignores = gosec.context.Ignores[1:]
   187  		}
   188  		return gosec
   189  	}
   190  
   191  	// Get any new rule exclusions.
   192  	ignoredRules, ignoreAll := gosec.ignore(n)
   193  	if ignoreAll {
   194  		return nil
   195  	}
   196  
   197  	// Now create the union of exclusions.
   198  	ignores := make(map[string]bool, 0)
   199  	if len(gosec.context.Ignores) > 0 {
   200  		for k, v := range gosec.context.Ignores[0] {
   201  			ignores[k] = v
   202  		}
   203  	}
   204  
   205  	for _, v := range ignoredRules {
   206  		ignores[v] = true
   207  	}
   208  
   209  	// Push the new set onto the stack.
   210  	gosec.context.Ignores = append([]map[string]bool{ignores}, gosec.context.Ignores...)
   211  
   212  	// Track aliased and initialization imports
   213  	gosec.context.Imports.TrackImport(n)
   214  
   215  	for _, rule := range gosec.ruleset.RegisteredFor(n) {
   216  		if _, ok := ignores[rule.ID()]; ok {
   217  			continue
   218  		}
   219  		issue, err := rule.Match(n, gosec.context)
   220  		if err != nil {
   221  			file, line := GetLocation(n, gosec.context)
   222  			file = path.Base(file)
   223  			gosec.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
   224  		}
   225  		if issue != nil {
   226  			gosec.issues = append(gosec.issues, issue)
   227  			gosec.stats.NumFound++
   228  		}
   229  	}
   230  	return gosec
   231  }
   232  
   233  // Report returns the current issues discovered and the metrics about the scan
   234  func (gosec *Analyzer) Report() ([]*Issue, *Metrics) {
   235  	return gosec.issues, gosec.stats
   236  }
   237  
   238  // Reset clears state such as context, issues and metrics from the configured analyzer
   239  func (gosec *Analyzer) Reset() {
   240  	gosec.context = &Context{}
   241  	gosec.issues = make([]*Issue, 0, 16)
   242  	gosec.stats = &Metrics{}
   243  }