Skip to content

Commit 7d539ed

Browse files
authored
feat: add concurrency option to parallelize package loading (#778)
* feat: add concurrency option to parallelize package loading * refactor: move wg.add inside the for loop * fix: gracefully stop the workers on error * test: add test for concurrent scan
1 parent 43577ce commit 7d539ed

File tree

4 files changed

+95
-16
lines changed

4 files changed

+95
-16
lines changed

analyzer.go

+57-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"regexp"
3030
"strconv"
3131
"strings"
32+
"sync"
3233

3334
"golang.org/x/tools/go/packages"
3435
)
@@ -88,6 +89,7 @@ type Analyzer struct {
8889
excludeGenerated bool
8990
showIgnored bool
9091
trackSuppressions bool
92+
concurrency int
9193
}
9294

9395
// SuppressionInfo object is to record the kind and the justification that used
@@ -98,7 +100,7 @@ type SuppressionInfo struct {
98100
}
99101

100102
// NewAnalyzer builds a new analyzer.
101-
func NewAnalyzer(conf Config, tests bool, excludeGenerated bool, trackSuppressions bool, logger *log.Logger) *Analyzer {
103+
func NewAnalyzer(conf Config, tests bool, excludeGenerated bool, trackSuppressions bool, concurrency int, logger *log.Logger) *Analyzer {
102104
ignoreNoSec := false
103105
if enabled, err := conf.IsGlobalEnabled(Nosec); err == nil {
104106
ignoreNoSec = enabled
@@ -121,6 +123,7 @@ func NewAnalyzer(conf Config, tests bool, excludeGenerated bool, trackSuppressio
121123
stats: &Metrics{},
122124
errors: make(map[string][]Error),
123125
tests: tests,
126+
concurrency: concurrency,
124127
excludeGenerated: excludeGenerated,
125128
trackSuppressions: trackSuppressions,
126129
}
@@ -153,15 +156,64 @@ func (gosec *Analyzer) Process(buildTags []string, packagePaths ...string) error
153156
Tests: gosec.tests,
154157
}
155158

159+
type result struct {
160+
pkgPath string
161+
pkgs []*packages.Package
162+
err error
163+
}
164+
165+
results := make(chan result)
166+
jobs := make(chan string, len(packagePaths))
167+
quit := make(chan struct{})
168+
169+
var wg sync.WaitGroup
170+
171+
worker := func(j chan string, r chan result, quit chan struct{}) {
172+
for {
173+
select {
174+
case s := <-j:
175+
packages, err := gosec.load(s, config)
176+
select {
177+
case r <- result{pkgPath: s, pkgs: packages, err: err}:
178+
case <-quit:
179+
// we've been told to stop, probably an error while
180+
// processing a previous result.
181+
wg.Done()
182+
return
183+
}
184+
default:
185+
// j is empty and there are no jobs left
186+
wg.Done()
187+
return
188+
}
189+
}
190+
}
191+
192+
// fill the buffer
156193
for _, pkgPath := range packagePaths {
157-
pkgs, err := gosec.load(pkgPath, config)
158-
if err != nil {
159-
gosec.AppendError(pkgPath, err)
194+
jobs <- pkgPath
195+
}
196+
197+
for i := 0; i < gosec.concurrency; i++ {
198+
wg.Add(1)
199+
go worker(jobs, results, quit)
200+
}
201+
202+
go func() {
203+
wg.Wait()
204+
close(results)
205+
}()
206+
207+
for r := range results {
208+
if r.err != nil {
209+
gosec.AppendError(r.pkgPath, r.err)
160210
}
161-
for _, pkg := range pkgs {
211+
for _, pkg := range r.pkgs {
162212
if pkg.Name != "" {
163213
err := gosec.ParseErrors(pkg)
164214
if err != nil {
215+
close(quit)
216+
wg.Wait() // wait for the goroutines to stop
165217
return fmt.Errorf("parsing errors in pkg %q: %w", pkg.Name, err)
166218
}
167219
gosec.Check(pkg)

analyzer_test.go

+32-9
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ var _ = Describe("Analyzer", func() {
2424
)
2525
BeforeEach(func() {
2626
logger, _ = testutils.NewLogger()
27-
analyzer = gosec.NewAnalyzer(nil, tests, false, false, logger)
27+
analyzer = gosec.NewAnalyzer(nil, tests, false, false, 1, logger)
2828
})
2929

3030
Context("when processing a package", func() {
@@ -77,6 +77,29 @@ var _ = Describe("Analyzer", func() {
7777
Expect(metrics.NumFiles).To(Equal(2))
7878
})
7979

80+
It("should be able to analyze multiple Go files concurrently", func() {
81+
customAnalyzer := gosec.NewAnalyzer(nil, true, true, false, 32, logger)
82+
customAnalyzer.LoadRules(rules.Generate(false).RulesInfo())
83+
pkg := testutils.NewTestPackage()
84+
defer pkg.Close()
85+
pkg.AddFile("foo.go", `
86+
package main
87+
func main(){
88+
bar()
89+
}`)
90+
pkg.AddFile("bar.go", `
91+
package main
92+
func bar(){
93+
println("package has two files!")
94+
}`)
95+
err := pkg.Build()
96+
Expect(err).ShouldNot(HaveOccurred())
97+
err = customAnalyzer.Process(buildTags, pkg.Path)
98+
Expect(err).ShouldNot(HaveOccurred())
99+
_, metrics, _ := customAnalyzer.Report()
100+
Expect(metrics.NumFiles).To(Equal(2))
101+
})
102+
80103
It("should be able to analyze multiple Go packages", func() {
81104
analyzer.LoadRules(rules.Generate(false).RulesInfo())
82105
pkg1 := testutils.NewTestPackage()
@@ -262,7 +285,7 @@ var _ = Describe("Analyzer", func() {
262285
// overwrite nosec option
263286
nosecIgnoreConfig := gosec.NewConfig()
264287
nosecIgnoreConfig.SetGlobal(gosec.Nosec, "true")
265-
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, logger)
288+
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, 1, logger)
266289
customAnalyzer.LoadRules(rules.Generate(false, rules.NewRuleFilter(false, "G401")).RulesInfo())
267290

268291
nosecPackage := testutils.NewTestPackage()
@@ -286,7 +309,7 @@ var _ = Describe("Analyzer", func() {
286309
nosecIgnoreConfig := gosec.NewConfig()
287310
nosecIgnoreConfig.SetGlobal(gosec.Nosec, "true")
288311
nosecIgnoreConfig.SetGlobal(gosec.ShowIgnored, "true")
289-
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, logger)
312+
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, 1, logger)
290313
customAnalyzer.LoadRules(rules.Generate(false, rules.NewRuleFilter(false, "G401")).RulesInfo())
291314

292315
nosecPackage := testutils.NewTestPackage()
@@ -379,7 +402,7 @@ var _ = Describe("Analyzer", func() {
379402
// overwrite nosec option
380403
nosecIgnoreConfig := gosec.NewConfig()
381404
nosecIgnoreConfig.SetGlobal(gosec.NoSecAlternative, "#falsePositive")
382-
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, logger)
405+
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, 1, logger)
383406
customAnalyzer.LoadRules(rules.Generate(false, rules.NewRuleFilter(false, "G401")).RulesInfo())
384407

385408
nosecPackage := testutils.NewTestPackage()
@@ -402,7 +425,7 @@ var _ = Describe("Analyzer", func() {
402425
// overwrite nosec option
403426
nosecIgnoreConfig := gosec.NewConfig()
404427
nosecIgnoreConfig.SetGlobal(gosec.NoSecAlternative, "#falsePositive")
405-
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, logger)
428+
customAnalyzer := gosec.NewAnalyzer(nosecIgnoreConfig, tests, false, false, 1, logger)
406429
customAnalyzer.LoadRules(rules.Generate(false, rules.NewRuleFilter(false, "G401")).RulesInfo())
407430

408431
nosecPackage := testutils.NewTestPackage()
@@ -418,7 +441,7 @@ var _ = Describe("Analyzer", func() {
418441
})
419442

420443
It("should be able to analyze Go test package", func() {
421-
customAnalyzer := gosec.NewAnalyzer(nil, true, false, false, logger)
444+
customAnalyzer := gosec.NewAnalyzer(nil, true, false, false, 1, logger)
422445
customAnalyzer.LoadRules(rules.Generate(false).RulesInfo())
423446
pkg := testutils.NewTestPackage()
424447
defer pkg.Close()
@@ -443,7 +466,7 @@ var _ = Describe("Analyzer", func() {
443466
Expect(issues).Should(HaveLen(1))
444467
})
445468
It("should be able to scan generated files if NOT excluded", func() {
446-
customAnalyzer := gosec.NewAnalyzer(nil, true, false, false, logger)
469+
customAnalyzer := gosec.NewAnalyzer(nil, true, false, false, 1, logger)
447470
customAnalyzer.LoadRules(rules.Generate(false).RulesInfo())
448471
pkg := testutils.NewTestPackage()
449472
defer pkg.Close()
@@ -464,7 +487,7 @@ var _ = Describe("Analyzer", func() {
464487
Expect(issues).Should(HaveLen(1))
465488
})
466489
It("should be able to skip generated files if excluded", func() {
467-
customAnalyzer := gosec.NewAnalyzer(nil, true, true, false, logger)
490+
customAnalyzer := gosec.NewAnalyzer(nil, true, true, false, 1, logger)
468491
customAnalyzer.LoadRules(rules.Generate(false).RulesInfo())
469492
pkg := testutils.NewTestPackage()
470493
defer pkg.Close()
@@ -671,7 +694,7 @@ var _ = Describe("Analyzer", func() {
671694

672695
Context("when tracking suppressions", func() {
673696
BeforeEach(func() {
674-
analyzer = gosec.NewAnalyzer(nil, tests, false, true, logger)
697+
analyzer = gosec.NewAnalyzer(nil, tests, false, true, 1, logger)
675698
})
676699

677700
It("should not report an error if the violation is suppressed", func() {

cmd/gosec/main.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io/ioutil"
2121
"log"
2222
"os"
23+
"runtime"
2324
"sort"
2425
"strings"
2526

@@ -114,6 +115,9 @@ var (
114115
// fail by confidence
115116
flagConfidence = flag.String("confidence", "low", "Filter out the issues with a lower confidence than the given value. Valid options are: low, medium, high")
116117

118+
// concurrency value
119+
flagConcurrency = flag.Int("concurrency", runtime.NumCPU(), "Concurrency value")
120+
117121
// do not fail
118122
flagNoFail = flag.Bool("no-fail", false, "Do not fail the scanning, even if issues were found")
119123

@@ -371,7 +375,7 @@ func main() {
371375
}
372376

373377
// Create the analyzer
374-
analyzer := gosec.NewAnalyzer(config, *flagScanTests, *flagExcludeGenerated, *flagTrackSuppressions, logger)
378+
analyzer := gosec.NewAnalyzer(config, *flagScanTests, *flagExcludeGenerated, *flagTrackSuppressions, *flagConcurrency, logger)
375379
analyzer.LoadRules(ruleList.RulesInfo())
376380

377381
excludedDirs := gosec.ExcludedDirsRegExp(flagDirsExclude)

rules/rules_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ var _ = Describe("gosec rules", func() {
2424
BeforeEach(func() {
2525
logger, _ = testutils.NewLogger()
2626
config = gosec.NewConfig()
27-
analyzer = gosec.NewAnalyzer(config, tests, false, false, logger)
27+
analyzer = gosec.NewAnalyzer(config, tests, false, false, 1, logger)
2828
runner = func(rule string, samples []testutils.CodeSample) {
2929
for n, sample := range samples {
3030
analyzer.Reset()

0 commit comments

Comments
 (0)