Skip to content

Commit 37cada1

Browse files
committed
Add support for #excluding specific rules
1 parent 6de76c9 commit 37cada1

37 files changed

+484
-147
lines changed

core/analyzer.go

Lines changed: 80 additions & 35 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

@@ -54,10 +55,12 @@ type Context struct {
5455
Root *ast.File
5556
Config map[string]interface{}
5657
Imports *ImportInfo
58+
Ignores []map[string]bool
5759
}
5860

5961
// The Rule interface used by all rules supported by GAS.
6062
type Rule interface {
63+
ID() string
6164
Match(ast.Node, *Context) (*Issue, error)
6265
}
6366

@@ -93,7 +96,7 @@ func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer {
9396
a := Analyzer{
9497
ignoreNosec: conf["ignoreNosec"].(bool),
9598
ruleset: make(RuleSet),
96-
context: &Context{nil, nil, nil, nil, nil, nil, nil},
99+
context: &Context{nil, nil, nil, nil, nil, nil, nil, nil},
97100
logger: logger,
98101
Issues: make([]*Issue, 0, 16),
99102
Stats: &Metrics{0, 0, 0, 0},
@@ -180,56 +183,98 @@ func (gas *Analyzer) ProcessSource(filename string, source string) error {
180183
}
181184

182185
// ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
183-
func (gas *Analyzer) ignore(n ast.Node) bool {
186+
func (gas *Analyzer) ignore(n ast.Node) ([]string, bool) {
184187
if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
185188
for _, group := range groups {
186189
if strings.Contains(group.Text(), "#nosec") {
190+
return nil, true
191+
}
192+
193+
if strings.Contains(group.Text(), "#exclude") {
187194
gas.Stats.NumNosec++
188-
return true
195+
196+
// Pull out the specific rules that are listed to be ignored.
197+
re := regexp.MustCompile("!(G\\d{3})")
198+
matches := re.FindAllStringSubmatch(group.Text(), -1)
199+
200+
// Find the rule IDs to ignore.
201+
ignores := make([]string, 0)
202+
for _, v := range matches {
203+
ignores = append(ignores, v[1])
204+
}
205+
return ignores, false
189206
}
190207
}
191208
}
192-
return false
209+
return nil, false
193210
}
194211

195212
// Visit runs the GAS visitor logic over an AST created by parsing go code.
196213
// Rule methods added with AddRule will be invoked as necessary.
197214
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
198-
if !gas.ignore(n) {
199-
200-
// Track aliased and initialization imports
201-
if imported, ok := n.(*ast.ImportSpec); ok {
202-
path := strings.Trim(imported.Path.Value, `"`)
203-
if imported.Name != nil {
204-
if imported.Name.Name == "_" {
205-
// Initialization import
206-
gas.context.Imports.InitOnly[path] = true
207-
} else {
208-
// Aliased import
209-
gas.context.Imports.Aliased[path] = imported.Name.Name
210-
}
211-
}
212-
// unsafe is not included in Package.Imports()
213-
if path == "unsafe" {
214-
gas.context.Imports.Imported[path] = path
215+
// If we've reached the end of this branch, pop off the ignores stack.
216+
if n == nil {
217+
if len(gas.context.Ignores) > 0 {
218+
gas.context.Ignores = gas.context.Ignores[1:]
219+
}
220+
return gas
221+
}
222+
223+
// Get any new rule exclusions.
224+
ignoredRules, ignoreAll := gas.ignore(n)
225+
if ignoreAll {
226+
return nil
227+
}
228+
229+
// Now create the union of exclusions.
230+
ignores := make(map[string]bool, 0)
231+
if len(gas.context.Ignores) > 0 {
232+
for k, v := range gas.context.Ignores[0] {
233+
ignores[k] = v
234+
}
235+
}
236+
237+
for _, v := range ignoredRules {
238+
ignores[v] = true
239+
}
240+
241+
// Push the new set onto the stack.
242+
gas.context.Ignores = append([]map[string]bool{ignores}, gas.context.Ignores...)
243+
244+
// Track aliased and initialization imports
245+
if imported, ok := n.(*ast.ImportSpec); ok {
246+
path := strings.Trim(imported.Path.Value, `"`)
247+
if imported.Name != nil {
248+
if imported.Name.Name == "_" {
249+
// Initialization import
250+
gas.context.Imports.InitOnly[path] = true
251+
} else {
252+
// Aliased import
253+
gas.context.Imports.Aliased[path] = imported.Name.Name
215254
}
216255
}
256+
// unsafe is not included in Package.Imports()
257+
if path == "unsafe" {
258+
gas.context.Imports.Imported[path] = path
259+
}
260+
}
217261

218-
if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok {
219-
for _, rule := range val {
220-
ret, err := rule.Match(n, gas.context)
221-
if err != nil {
222-
file, line := GetLocation(n, gas.context)
223-
file = path.Base(file)
224-
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
225-
}
226-
if ret != nil {
227-
gas.Issues = append(gas.Issues, ret)
228-
gas.Stats.NumFound++
229-
}
262+
if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok {
263+
for _, rule := range val {
264+
if _, ok := ignores[rule.ID()]; ok {
265+
continue
266+
}
267+
ret, err := rule.Match(n, gas.context)
268+
if err != nil {
269+
file, line := GetLocation(n, gas.context)
270+
file = path.Base(file)
271+
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
272+
}
273+
if ret != nil {
274+
gas.Issues = append(gas.Issues, ret)
275+
gas.Stats.NumFound++
230276
}
231277
}
232-
return gas
233278
}
234-
return nil
279+
return gas
235280
}

core/call_list_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ type callListRule struct {
1111
matched int
1212
}
1313

14+
func (r *callListRule) ID() string {
15+
return r.MetaData.ID
16+
}
17+
1418
func (r *callListRule) Match(n ast.Node, c *Context) (gi *Issue, err error) {
1519
if r.callList.ContainsCallExpr(n, c) {
1620
r.matched += 1
@@ -25,6 +29,7 @@ func TestCallListContainsCallExpr(t *testing.T) {
2529
calls.AddAll("bytes.Buffer", "Write", "WriteTo")
2630
rule := &callListRule{
2731
MetaData: MetaData{
32+
ID: "TEST",
2833
Severity: Low,
2934
Confidence: Low,
3035
What: "A dummy rule",

core/helpers_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ type dummyRule struct {
1616
matched int
1717
}
1818

19+
func (r *dummyRule) ID() string {
20+
return r.MetaData.ID
21+
}
22+
1923
func (r *dummyRule) Match(n ast.Node, c *Context) (gi *Issue, err error) {
2024
if callexpr, matched := r.callback(n, c, r.pkgOrType, r.funcsOrMethods...); matched {
2125
r.matched += 1

core/issue.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Issue struct {
4242
// MetaData is embedded in all GAS rules. The Severity, Confidence and What message
4343
// will be passed tbhrough to reported issues.
4444
type MetaData struct {
45+
ID string
4546
Severity Score
4647
Confidence Score
4748
What string

rulelist.go

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,49 @@ import (
2222
)
2323

2424
type RuleInfo struct {
25+
id string
2526
description string
26-
build func(map[string]interface{}) (gas.Rule, []ast.Node)
27+
build func(string, map[string]interface{}) (gas.Rule, []ast.Node)
2728
}
2829

2930
// GetFullRuleList get the full list of all rules available to GAS
3031
func GetFullRuleList() map[string]RuleInfo {
31-
return map[string]RuleInfo{
32+
rules := []RuleInfo{
3233
// misc
33-
"G101": RuleInfo{"Look for hardcoded credentials", rules.NewHardcodedCredentials},
34-
"G102": RuleInfo{"Bind to all interfaces", rules.NewBindsToAllNetworkInterfaces},
35-
"G103": RuleInfo{"Audit the use of unsafe block", rules.NewUsingUnsafe},
36-
"G104": RuleInfo{"Audit errors not checked", rules.NewNoErrorCheck},
37-
"G105": RuleInfo{"Audit the use of big.Exp function", rules.NewUsingBigExp},
34+
RuleInfo{"G101", "Look for hardcoded credentials", rules.NewHardcodedCredentials},
35+
RuleInfo{"G102", "Bind to all interfaces", rules.NewBindsToAllNetworkInterfaces},
36+
RuleInfo{"G103", "Audit the use of unsafe block", rules.NewUsingUnsafe},
37+
RuleInfo{"G104", "Audit errors not checked", rules.NewNoErrorCheck},
38+
RuleInfo{"G105", "Audit the use of big.Exp function", rules.NewUsingBigExp},
3839

3940
// injection
40-
"G201": RuleInfo{"SQL query construction using format string", rules.NewSqlStrFormat},
41-
"G202": RuleInfo{"SQL query construction using string concatenation", rules.NewSqlStrConcat},
42-
"G203": RuleInfo{"Use of unescaped data in HTML templates", rules.NewTemplateCheck},
43-
"G204": RuleInfo{"Audit use of command execution", rules.NewSubproc},
41+
RuleInfo{"G201", "SQL query construction using format string", rules.NewSqlStrFormat},
42+
RuleInfo{"G202", "SQL query construction using string concatenation", rules.NewSqlStrConcat},
43+
RuleInfo{"G203", "Use of unescaped data in HTML templates", rules.NewTemplateCheck},
44+
RuleInfo{"G204", "Audit use of command execution", rules.NewSubproc},
4445

4546
// filesystem
46-
"G301": RuleInfo{"Poor file permissions used when creating a directory", rules.NewMkdirPerms},
47-
"G302": RuleInfo{"Poor file permisions used when creation file or using chmod", rules.NewFilePerms},
48-
"G303": RuleInfo{"Creating tempfile using a predictable path", rules.NewBadTempFile},
47+
RuleInfo{"G301", "Poor file permissions used when creating a directory", rules.NewMkdirPerms},
48+
RuleInfo{"G302", "Poor file permisions used when creation file or using chmod", rules.NewFilePerms},
49+
RuleInfo{"G303", "Creating tempfile using a predictable path", rules.NewBadTempFile},
4950

5051
// crypto
51-
"G401": RuleInfo{"Detect the usage of DES, RC4, or MD5", rules.NewUsesWeakCryptography},
52-
"G402": RuleInfo{"Look for bad TLS connection settings", rules.NewIntermediateTlsCheck},
53-
"G403": RuleInfo{"Ensure minimum RSA key length of 2048 bits", rules.NewWeakKeyStrength},
54-
"G404": RuleInfo{"Insecure random number source (rand)", rules.NewWeakRandCheck},
52+
RuleInfo{"G401", "Detect the usage of DES, RC4, or MD5", rules.NewUsesWeakCryptography},
53+
RuleInfo{"G402", "Look for bad TLS connection settings", rules.NewIntermediateTlsCheck},
54+
RuleInfo{"G403", "Ensure minimum RSA key length of 2048 bits", rules.NewWeakKeyStrength},
55+
RuleInfo{"G404", "Insecure random number source (rand)", rules.NewWeakRandCheck},
5556

5657
// blacklist
57-
"G501": RuleInfo{"Import blacklist: crypto/md5", rules.NewBlacklist_crypto_md5},
58-
"G502": RuleInfo{"Import blacklist: crypto/des", rules.NewBlacklist_crypto_des},
59-
"G503": RuleInfo{"Import blacklist: crypto/rc4", rules.NewBlacklist_crypto_rc4},
60-
"G504": RuleInfo{"Import blacklist: net/http/cgi", rules.NewBlacklist_net_http_cgi},
58+
RuleInfo{"G501", "Import blacklist: crypto/md5", rules.NewBlacklist_crypto_md5},
59+
RuleInfo{"G502", "Import blacklist: crypto/des", rules.NewBlacklist_crypto_des},
60+
RuleInfo{"G503", "Import blacklist: crypto/rc4", rules.NewBlacklist_crypto_rc4},
61+
RuleInfo{"G504", "Import blacklist: net/http/cgi", rules.NewBlacklist_net_http_cgi},
6162
}
63+
ruleMap := make(map[string]RuleInfo)
64+
for _, v := range rules {
65+
ruleMap[v.id] = v
66+
}
67+
return ruleMap
6268
}
6369

6470
func AddRules(analyzer *gas.Analyzer, conf map[string]interface{}) {
@@ -85,7 +91,7 @@ func AddRules(analyzer *gas.Analyzer, conf map[string]interface{}) {
8591
delete(all, v)
8692
}
8793

88-
for _, v := range all {
89-
analyzer.AddRule(v.build(conf))
94+
for k, v := range all {
95+
analyzer.AddRule(v.build(k, conf))
9096
}
9197
}

rules/big.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
package rules
1616

1717
import (
18-
gas "github.com/GoASTScanner/gas/core"
1918
"go/ast"
19+
20+
gas "github.com/GoASTScanner/gas/core"
2021
)
2122

2223
type UsingBigExp struct {
@@ -25,17 +26,22 @@ type UsingBigExp struct {
2526
calls []string
2627
}
2728

29+
func (r *UsingBigExp) ID() string {
30+
return r.MetaData.ID
31+
}
32+
2833
func (r *UsingBigExp) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
2934
if _, matched := gas.MatchCallByType(n, c, r.pkg, r.calls...); matched {
3035
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
3136
}
3237
return nil, nil
3338
}
34-
func NewUsingBigExp(conf map[string]interface{}) (gas.Rule, []ast.Node) {
39+
func NewUsingBigExp(id string, conf map[string]interface{}) (gas.Rule, []ast.Node) {
3540
return &UsingBigExp{
3641
pkg: "*math/big.Int",
3742
calls: []string{"Exp"},
3843
MetaData: gas.MetaData{
44+
ID: id,
3945
What: "Use of math/big.Int.Exp function should be audited for modulus == 0",
4046
Severity: gas.Low,
4147
Confidence: gas.High,

rules/big_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
func TestBigExp(t *testing.T) {
2424
config := map[string]interface{}{"ignoreNosec": false}
2525
analyzer := gas.NewAnalyzer(config, nil)
26-
analyzer.AddRule(NewUsingBigExp(config))
26+
analyzer.AddRule(NewUsingBigExp("TEST", config))
2727

2828
issues := gasTestRunner(`
2929
package main

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) (gi *gas.Issue, err error) {
3236
if node := gas.MatchCall(n, r.call); node != nil {
3337
if arg, err := gas.GetString(node.Args[1]); err == nil {
@@ -39,11 +43,12 @@ func (r *BindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (gi *gas
3943
return
4044
}
4145

42-
func NewBindsToAllNetworkInterfaces(conf map[string]interface{}) (gas.Rule, []ast.Node) {
46+
func NewBindsToAllNetworkInterfaces(id string, conf map[string]interface{}) (gas.Rule, []ast.Node) {
4347
return &BindsToAllNetworkInterfaces{
4448
call: regexp.MustCompile(`^(net|tls)\.Listen$`),
4549
pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`),
4650
MetaData: gas.MetaData{
51+
ID: id,
4752
Severity: gas.Medium,
4853
Confidence: gas.High,
4954
What: "Binds to all network interfaces",

rules/bind_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
func TestBind0000(t *testing.T) {
2424
config := map[string]interface{}{"ignoreNosec": false}
2525
analyzer := gas.NewAnalyzer(config, nil)
26-
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
26+
analyzer.AddRule(NewBindsToAllNetworkInterfaces("TEST", config))
2727

2828
issues := gasTestRunner(`
2929
package main
@@ -45,7 +45,7 @@ func TestBind0000(t *testing.T) {
4545
func TestBindEmptyHost(t *testing.T) {
4646
config := map[string]interface{}{"ignoreNosec": false}
4747
analyzer := gas.NewAnalyzer(config, nil)
48-
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
48+
analyzer.AddRule(NewBindsToAllNetworkInterfaces("TEST", config))
4949

5050
issues := gasTestRunner(`
5151
package main

0 commit comments

Comments
 (0)