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 }