github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/gosec.go (about)

     1  package golinters
     2  
     3  import (
     4  	"fmt"
     5  	"go/token"
     6  	"io"
     7  	"log"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/securego/gosec/v2"
    13  	"github.com/securego/gosec/v2/issue"
    14  	"github.com/securego/gosec/v2/rules"
    15  	"golang.org/x/tools/go/analysis"
    16  	"golang.org/x/tools/go/packages"
    17  
    18  	"github.com/vanstinator/golangci-lint/pkg/config"
    19  	"github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis"
    20  	"github.com/vanstinator/golangci-lint/pkg/lint/linter"
    21  	"github.com/vanstinator/golangci-lint/pkg/result"
    22  )
    23  
    24  const gosecName = "gosec"
    25  
    26  func NewGosec(settings *config.GoSecSettings) *goanalysis.Linter {
    27  	var mu sync.Mutex
    28  	var resIssues []goanalysis.Issue
    29  
    30  	var filters []rules.RuleFilter
    31  	conf := gosec.NewConfig()
    32  	if settings != nil {
    33  		filters = gosecRuleFilters(settings.Includes, settings.Excludes)
    34  		conf = toGosecConfig(settings)
    35  	}
    36  
    37  	logger := log.New(io.Discard, "", 0)
    38  
    39  	ruleDefinitions := rules.Generate(false, filters...)
    40  
    41  	analyzer := &analysis.Analyzer{
    42  		Name: gosecName,
    43  		Doc:  goanalysis.TheOnlyanalyzerDoc,
    44  		Run:  goanalysis.DummyRun,
    45  	}
    46  
    47  	return goanalysis.NewLinter(
    48  		gosecName,
    49  		"Inspects source code for security problems",
    50  		[]*analysis.Analyzer{analyzer},
    51  		nil,
    52  	).WithContextSetter(func(lintCtx *linter.Context) {
    53  		analyzer.Run = func(pass *analysis.Pass) (any, error) {
    54  			// The `gosecAnalyzer` is here because of concurrency issue.
    55  			gosecAnalyzer := gosec.NewAnalyzer(conf, true, settings.ExcludeGenerated, false, settings.Concurrency, logger)
    56  			gosecAnalyzer.LoadRules(ruleDefinitions.RulesInfo())
    57  
    58  			issues := runGoSec(lintCtx, pass, settings, gosecAnalyzer)
    59  
    60  			mu.Lock()
    61  			resIssues = append(resIssues, issues...)
    62  			mu.Unlock()
    63  
    64  			return nil, nil
    65  		}
    66  	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
    67  		return resIssues
    68  	}).WithLoadMode(goanalysis.LoadModeTypesInfo)
    69  }
    70  
    71  func runGoSec(lintCtx *linter.Context, pass *analysis.Pass, settings *config.GoSecSettings, analyzer *gosec.Analyzer) []goanalysis.Issue {
    72  	pkg := &packages.Package{
    73  		Fset:      pass.Fset,
    74  		Syntax:    pass.Files,
    75  		Types:     pass.Pkg,
    76  		TypesInfo: pass.TypesInfo,
    77  	}
    78  
    79  	analyzer.CheckRules(pkg)
    80  
    81  	secIssues, _, _ := analyzer.Report()
    82  	if len(secIssues) == 0 {
    83  		return nil
    84  	}
    85  
    86  	severity, err := convertToScore(settings.Severity)
    87  	if err != nil {
    88  		lintCtx.Log.Warnf("The provided severity %v", err)
    89  	}
    90  
    91  	confidence, err := convertToScore(settings.Confidence)
    92  	if err != nil {
    93  		lintCtx.Log.Warnf("The provided confidence %v", err)
    94  	}
    95  
    96  	secIssues = filterIssues(secIssues, severity, confidence)
    97  
    98  	issues := make([]goanalysis.Issue, 0, len(secIssues))
    99  	for _, i := range secIssues {
   100  		text := fmt.Sprintf("%s: %s", i.RuleID, i.What) // TODO: use severity and confidence
   101  
   102  		var r *result.Range
   103  
   104  		line, err := strconv.Atoi(i.Line)
   105  		if err != nil {
   106  			r = &result.Range{}
   107  			if n, rerr := fmt.Sscanf(i.Line, "%d-%d", &r.From, &r.To); rerr != nil || n != 2 {
   108  				lintCtx.Log.Warnf("Can't convert gosec line number %q of %v to int: %s", i.Line, i, err)
   109  				continue
   110  			}
   111  			line = r.From
   112  		}
   113  
   114  		column, err := strconv.Atoi(i.Col)
   115  		if err != nil {
   116  			lintCtx.Log.Warnf("Can't convert gosec column number %q of %v to int: %s", i.Col, i, err)
   117  			continue
   118  		}
   119  
   120  		issues = append(issues, goanalysis.NewIssue(&result.Issue{
   121  			Pos: token.Position{
   122  				Filename: i.File,
   123  				Line:     line,
   124  				Column:   column,
   125  			},
   126  			Text:       text,
   127  			LineRange:  r,
   128  			FromLinter: gosecName,
   129  		}, pass))
   130  	}
   131  
   132  	return issues
   133  }
   134  
   135  func toGosecConfig(settings *config.GoSecSettings) gosec.Config {
   136  	conf := gosec.NewConfig()
   137  
   138  	for k, v := range settings.Config {
   139  		if k == gosec.Globals {
   140  			convertGosecGlobals(v, conf)
   141  			continue
   142  		}
   143  
   144  		// Uses ToUpper because the parsing of the map's key change the key to lowercase.
   145  		// The value is not impacted by that: the case is respected.
   146  		conf.Set(strings.ToUpper(k), v)
   147  	}
   148  
   149  	return conf
   150  }
   151  
   152  // based on https://github.com/securego/gosec/blob/47bfd4eb6fc7395940933388550b547538b4c946/config.go#L52-L62
   153  func convertGosecGlobals(globalOptionFromConfig any, conf gosec.Config) {
   154  	globalOptionMap, ok := globalOptionFromConfig.(map[string]any)
   155  	if !ok {
   156  		return
   157  	}
   158  
   159  	for k, v := range globalOptionMap {
   160  		conf.SetGlobal(gosec.GlobalOption(k), fmt.Sprintf("%v", v))
   161  	}
   162  }
   163  
   164  // based on https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/cmd/gosec/main.go#L170-L188
   165  func gosecRuleFilters(includes, excludes []string) []rules.RuleFilter {
   166  	var filters []rules.RuleFilter
   167  
   168  	if len(includes) > 0 {
   169  		filters = append(filters, rules.NewRuleFilter(false, includes...))
   170  	}
   171  
   172  	if len(excludes) > 0 {
   173  		filters = append(filters, rules.NewRuleFilter(true, excludes...))
   174  	}
   175  
   176  	return filters
   177  }
   178  
   179  // code borrowed from https://github.com/securego/gosec/blob/69213955dacfd560562e780f723486ef1ca6d486/cmd/gosec/main.go#L250-L262
   180  func convertToScore(str string) (issue.Score, error) {
   181  	str = strings.ToLower(str)
   182  	switch str {
   183  	case "", "low":
   184  		return issue.Low, nil
   185  	case "medium":
   186  		return issue.Medium, nil
   187  	case "high":
   188  		return issue.High, nil
   189  	default:
   190  		return issue.Low, fmt.Errorf("'%s' is invalid, use low instead. Valid options: low, medium, high", str)
   191  	}
   192  }
   193  
   194  // code borrowed from https://github.com/securego/gosec/blob/69213955dacfd560562e780f723486ef1ca6d486/cmd/gosec/main.go#L264-L276
   195  func filterIssues(issues []*issue.Issue, severity, confidence issue.Score) []*issue.Issue {
   196  	res := make([]*issue.Issue, 0)
   197  
   198  	for _, i := range issues {
   199  		if i.Severity >= severity && i.Confidence >= confidence {
   200  			res = append(res, i)
   201  		}
   202  	}
   203  
   204  	return res
   205  }