Skip to content

Commit 15095a8

Browse files
committed
Merge branch 'jonmcclintock-nosec-specify-rule'
2 parents f3c8d59 + 90fe5cb commit 15095a8

28 files changed

+306
-127
lines changed

analyzer.go

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"os"
2626
"path"
2727
"reflect"
28+
"regexp"
2829
"strings"
2930

3031
"path/filepath"
@@ -43,6 +44,7 @@ type Context struct {
4344
Root *ast.File
4445
Config map[string]interface{}
4546
Imports *ImportTracker
47+
Ignores []map[string]bool
4648
}
4749

4850
// Metrics used when reporting information about a scanning run.
@@ -87,9 +89,9 @@ func NewAnalyzer(conf Config, logger *log.Logger) *Analyzer {
8789

8890
// LoadRules instantiates all the rules to be used when analyzing source
8991
// packages
90-
func (gas *Analyzer) LoadRules(ruleDefinitions ...RuleBuilder) {
91-
for _, builder := range ruleDefinitions {
92-
r, nodes := builder(gas.config)
92+
func (gas *Analyzer) LoadRules(ruleDefinitions map[string]RuleBuilder) {
93+
for id, def := range ruleDefinitions {
94+
r, nodes := def(id, gas.config)
9395
gas.ruleset.Register(r, nodes...)
9496
}
9597
}
@@ -147,41 +149,84 @@ func (gas *Analyzer) Process(packagePaths ...string) error {
147149
}
148150

149151
// ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
150-
func (gas *Analyzer) ignore(n ast.Node) bool {
152+
func (gas *Analyzer) ignore(n ast.Node) ([]string, bool) {
151153
if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
152154
for _, group := range groups {
153155
if strings.Contains(group.Text(), "#nosec") {
154156
gas.stats.NumNosec++
155-
return true
157+
158+
// Pull out the specific rules that are listed to be ignored.
159+
re := regexp.MustCompile("(G\\d{3})")
160+
matches := re.FindAllStringSubmatch(group.Text(), -1)
161+
162+
// If no specific rules were given, ignore everything.
163+
if matches == nil || len(matches) == 0 {
164+
return nil, true
165+
}
166+
167+
// Find the rule IDs to ignore.
168+
var ignores []string
169+
for _, v := range matches {
170+
ignores = append(ignores, v[1])
171+
}
172+
return ignores, false
156173
}
157174
}
158175
}
159-
return false
176+
return nil, false
160177
}
161178

162179
// Visit runs the GAS visitor logic over an AST created by parsing go code.
163180
// Rule methods added with AddRule will be invoked as necessary.
164181
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
165-
if !gas.ignore(n) {
182+
// If we've reached the end of this branch, pop off the ignores stack.
183+
if n == nil {
184+
if len(gas.context.Ignores) > 0 {
185+
gas.context.Ignores = gas.context.Ignores[1:]
186+
}
187+
return gas
188+
}
166189

167-
// Track aliased and initialization imports
168-
gas.context.Imports.TrackImport(n)
190+
// Get any new rule exclusions.
191+
ignoredRules, ignoreAll := gas.ignore(n)
192+
if ignoreAll {
193+
return nil
194+
}
169195

170-
for _, rule := range gas.ruleset.RegisteredFor(n) {
171-
issue, err := rule.Match(n, gas.context)
172-
if err != nil {
173-
file, line := GetLocation(n, gas.context)
174-
file = path.Base(file)
175-
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
176-
}
177-
if issue != nil {
178-
gas.issues = append(gas.issues, issue)
179-
gas.stats.NumFound++
180-
}
196+
// Now create the union of exclusions.
197+
ignores := make(map[string]bool, 0)
198+
if len(gas.context.Ignores) > 0 {
199+
for k, v := range gas.context.Ignores[0] {
200+
ignores[k] = v
181201
}
182-
return gas
183202
}
184-
return nil
203+
204+
for _, v := range ignoredRules {
205+
ignores[v] = true
206+
}
207+
208+
// Push the new set onto the stack.
209+
gas.context.Ignores = append([]map[string]bool{ignores}, gas.context.Ignores...)
210+
211+
// Track aliased and initialization imports
212+
gas.context.Imports.TrackImport(n)
213+
214+
for _, rule := range gas.ruleset.RegisteredFor(n) {
215+
if _, ok := ignores[rule.ID()]; ok {
216+
continue
217+
}
218+
issue, err := rule.Match(n, gas.context)
219+
if err != nil {
220+
file, line := GetLocation(n, gas.context)
221+
file = path.Base(file)
222+
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
223+
}
224+
if issue != nil {
225+
gas.issues = append(gas.issues, issue)
226+
gas.stats.NumFound++
227+
}
228+
}
229+
return gas
185230
}
186231

187232
// Report returns the current issues discovered and the metrics about the scan

analyzer_test.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var _ = Describe("Analyzer", func() {
2828
Context("when processing a package", func() {
2929

3030
It("should return an error if the package contains no Go files", func() {
31-
analyzer.LoadRules(rules.Generate().Builders()...)
31+
analyzer.LoadRules(rules.Generate().Builders())
3232
dir, err := ioutil.TempDir("", "empty")
3333
defer os.RemoveAll(dir)
3434
Expect(err).ShouldNot(HaveOccurred())
@@ -38,7 +38,7 @@ var _ = Describe("Analyzer", func() {
3838
})
3939

4040
It("should return an error if the package fails to build", func() {
41-
analyzer.LoadRules(rules.Generate().Builders()...)
41+
analyzer.LoadRules(rules.Generate().Builders())
4242
pkg := testutils.NewTestPackage()
4343
defer pkg.Close()
4444
pkg.AddFile("wonky.go", `func main(){ println("forgot the package")}`)
@@ -51,7 +51,7 @@ var _ = Describe("Analyzer", func() {
5151
})
5252

5353
It("should be able to analyze mulitple Go files", func() {
54-
analyzer.LoadRules(rules.Generate().Builders()...)
54+
analyzer.LoadRules(rules.Generate().Builders())
5555
pkg := testutils.NewTestPackage()
5656
defer pkg.Close()
5757
pkg.AddFile("foo.go", `
@@ -72,7 +72,7 @@ var _ = Describe("Analyzer", func() {
7272
})
7373

7474
It("should be able to analyze mulitple Go packages", func() {
75-
analyzer.LoadRules(rules.Generate().Builders()...)
75+
analyzer.LoadRules(rules.Generate().Builders())
7676
pkg1 := testutils.NewTestPackage()
7777
pkg2 := testutils.NewTestPackage()
7878
defer pkg1.Close()
@@ -98,7 +98,7 @@ var _ = Describe("Analyzer", func() {
9898
// Rule for MD5 weak crypto usage
9999
sample := testutils.SampleCodeG401[0]
100100
source := sample.Code
101-
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
101+
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
102102

103103
controlPackage := testutils.NewTestPackage()
104104
defer controlPackage.Close()
@@ -114,7 +114,7 @@ var _ = Describe("Analyzer", func() {
114114
// Rule for MD5 weak crypto usage
115115
sample := testutils.SampleCodeG401[0]
116116
source := sample.Code
117-
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
117+
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
118118

119119
nosecPackage := testutils.NewTestPackage()
120120
defer nosecPackage.Close()
@@ -126,6 +126,57 @@ var _ = Describe("Analyzer", func() {
126126
nosecIssues, _ := analyzer.Report()
127127
Expect(nosecIssues).Should(BeEmpty())
128128
})
129+
130+
It("should not report errors when an exclude comment is present for the correct rule", func() {
131+
// Rule for MD5 weak crypto usage
132+
sample := testutils.SampleCodeG401[0]
133+
source := sample.Code
134+
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
135+
136+
nosecPackage := testutils.NewTestPackage()
137+
defer nosecPackage.Close()
138+
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec G401", 1)
139+
nosecPackage.AddFile("md5.go", nosecSource)
140+
nosecPackage.Build()
141+
142+
analyzer.Process(nosecPackage.Path)
143+
nosecIssues, _ := analyzer.Report()
144+
Expect(nosecIssues).Should(BeEmpty())
145+
})
146+
147+
It("should report errors when an exclude comment is present for a different rule", func() {
148+
// Rule for MD5 weak crypto usage
149+
sample := testutils.SampleCodeG401[0]
150+
source := sample.Code
151+
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
152+
153+
nosecPackage := testutils.NewTestPackage()
154+
defer nosecPackage.Close()
155+
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec G301", 1)
156+
nosecPackage.AddFile("md5.go", nosecSource)
157+
nosecPackage.Build()
158+
159+
analyzer.Process(nosecPackage.Path)
160+
nosecIssues, _ := analyzer.Report()
161+
Expect(nosecIssues).Should(HaveLen(sample.Errors))
162+
})
163+
164+
It("should not report errors when an exclude comment is present for multiple rules, including the correct rule", func() {
165+
// Rule for MD5 weak crypto usage
166+
sample := testutils.SampleCodeG401[0]
167+
source := sample.Code
168+
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
169+
170+
nosecPackage := testutils.NewTestPackage()
171+
defer nosecPackage.Close()
172+
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec G301 G401", 1)
173+
nosecPackage.AddFile("md5.go", nosecSource)
174+
nosecPackage.Build()
175+
176+
analyzer.Process(nosecPackage.Path)
177+
nosecIssues, _ := analyzer.Report()
178+
Expect(nosecIssues).Should(BeEmpty())
179+
})
129180
})
130181

131182
It("should be possible to overwrite nosec comments, and report issues", func() {
@@ -138,7 +189,7 @@ var _ = Describe("Analyzer", func() {
138189
nosecIgnoreConfig := gas.NewConfig()
139190
nosecIgnoreConfig.SetGlobal("nosec", "true")
140191
customAnalyzer := gas.NewAnalyzer(nosecIgnoreConfig, logger)
141-
customAnalyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
192+
customAnalyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders())
142193

143194
nosecPackage := testutils.NewTestPackage()
144195
defer nosecPackage.Close()

cmd/gas/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func main() {
207207

208208
// Create the analyzer
209209
analyzer := gas.NewAnalyzer(config, logger)
210-
analyzer.LoadRules(ruleDefinitions.Builders()...)
210+
analyzer.LoadRules(ruleDefinitions.Builders())
211211

212212
vendor := regexp.MustCompile(`[\\/]vendor([\\/]|$)`)
213213

import_tracker_test.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

issue.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Issue struct {
4747
// MetaData is embedded in all GAS rules. The Severity, Confidence and What message
4848
// will be passed tbhrough to reported issues.
4949
type MetaData struct {
50+
ID string
5051
Severity Score
5152
Confidence Score
5253
What string

issue_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ var _ = Describe("Issue", func() {
7878

7979
// Use SQL rule to check binary expr
8080
cfg := gas.NewConfig()
81-
rule, _ := rules.NewSQLStrConcat(cfg)
81+
rule, _ := rules.NewSQLStrConcat("TEST", cfg)
8282
issue, err := rule.Match(target, ctx)
8383
Expect(err).ShouldNot(HaveOccurred())
8484
Expect(issue).ShouldNot(BeNil())

rule.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import (
1919

2020
// The Rule interface used by all rules supported by GAS.
2121
type Rule interface {
22+
ID() string
2223
Match(ast.Node, *Context) (*Issue, error)
2324
}
2425

2526
// RuleBuilder is used to register a rule definition with the analyzer
26-
type RuleBuilder func(c Config) (Rule, []ast.Node)
27+
type RuleBuilder func(id string, c Config) (Rule, []ast.Node)
2728

2829
// A RuleSet maps lists of rules to the type of AST node they should be run on.
2930
// The anaylzer will only invoke rules contained in the list associated with the

rule_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ type mockrule struct {
1515
callback func(n ast.Node, ctx *gas.Context) bool
1616
}
1717

18+
func (m *mockrule) ID() string {
19+
return "MOCK"
20+
}
21+
1822
func (m *mockrule) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
1923
if m.callback(n, ctx) {
2024
return m.issue, nil

rules/big.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type usingBigExp struct {
2626
calls []string
2727
}
2828

29+
func (r *usingBigExp) ID() string {
30+
return r.MetaData.ID
31+
}
32+
2933
func (r *usingBigExp) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
3034
if _, matched := gas.MatchCallByType(n, c, r.pkg, r.calls...); matched {
3135
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
@@ -34,11 +38,12 @@ func (r *usingBigExp) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err erro
3438
}
3539

3640
// NewUsingBigExp detects issues with modulus == 0 for Bignum
37-
func NewUsingBigExp(conf gas.Config) (gas.Rule, []ast.Node) {
41+
func NewUsingBigExp(id string, conf gas.Config) (gas.Rule, []ast.Node) {
3842
return &usingBigExp{
3943
pkg: "*math/big.Int",
4044
calls: []string{"Exp"},
4145
MetaData: gas.MetaData{
46+
ID: id,
4247
What: "Use of math/big.Int.Exp function should be audited for modulus == 0",
4348
Severity: gas.Low,
4449
Confidence: gas.High,

rules/bind.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ type bindsToAllNetworkInterfaces struct {
2828
pattern *regexp.Regexp
2929
}
3030

31+
func (r *bindsToAllNetworkInterfaces) ID() string {
32+
return r.MetaData.ID
33+
}
34+
3135
func (r *bindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
3236
callExpr := r.calls.ContainsCallExpr(n, c)
3337
if callExpr == nil {
@@ -43,14 +47,15 @@ func (r *bindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (*gas.Is
4347

4448
// NewBindsToAllNetworkInterfaces detects socket connections that are setup to
4549
// listen on all network interfaces.
46-
func NewBindsToAllNetworkInterfaces(conf gas.Config) (gas.Rule, []ast.Node) {
50+
func NewBindsToAllNetworkInterfaces(id string, conf gas.Config) (gas.Rule, []ast.Node) {
4751
calls := gas.NewCallList()
4852
calls.Add("net", "Listen")
4953
calls.Add("crypto/tls", "Listen")
5054
return &bindsToAllNetworkInterfaces{
5155
calls: calls,
5256
pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`),
5357
MetaData: gas.MetaData{
58+
ID: id,
5459
Severity: gas.Medium,
5560
Confidence: gas.High,
5661
What: "Binds to all network interfaces",

0 commit comments

Comments
 (0)