diff --git a/core/analyzer.go b/core/analyzer.go index 41904aa..0fa8a48 100644 --- a/core/analyzer.go +++ b/core/analyzer.go @@ -142,12 +142,14 @@ func (gas *Analyzer) process(filename string, source interface{}) error { // AddRule adds a rule into a rule set list mapped to the given AST node's type. // The node is only needed for its type and is not otherwise used. -func (gas *Analyzer) AddRule(r Rule, n ast.Node) { - t := reflect.TypeOf(n) - if val, ok := gas.ruleset[t]; ok { - gas.ruleset[t] = append(val, r) - } else { - gas.ruleset[t] = []Rule{r} +func (gas *Analyzer) AddRule(r Rule, nodes []ast.Node) { + for _, n := range nodes { + t := reflect.TypeOf(n) + if val, ok := gas.ruleset[t]; ok { + gas.ruleset[t] = append(val, r) + } else { + gas.ruleset[t] = []Rule{r} + } } } diff --git a/rulelist.go b/rulelist.go index 61ea56f..0c207af 100644 --- a/rulelist.go +++ b/rulelist.go @@ -23,7 +23,7 @@ import ( type RuleInfo struct { description string - build func(map[string]interface{}) (gas.Rule, ast.Node) + build func(map[string]interface{}) (gas.Rule, []ast.Node) } // GetFullRuleList get the full list of all rules available to GAS diff --git a/rules/bind.go b/rules/bind.go index 4363684..c2fa781 100644 --- a/rules/bind.go +++ b/rules/bind.go @@ -39,8 +39,8 @@ func (r *BindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (gi *gas return } -func NewBindsToAllNetworkInterfaces(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BindsToAllNetworkInterfaces{ +func NewBindsToAllNetworkInterfaces(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BindsToAllNetworkInterfaces{ call: regexp.MustCompile(`^net\.Listen$`), pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`), MetaData: gas.MetaData{ @@ -48,7 +48,5 @@ func NewBindsToAllNetworkInterfaces(conf map[string]interface{}) (r gas.Rule, n Confidence: gas.High, What: "Binds to all network interfaces", }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/blacklist.go b/rules/blacklist.go index f3ceb39..747eb4b 100644 --- a/rules/blacklist.go +++ b/rules/blacklist.go @@ -34,54 +34,46 @@ func (r *BlacklistImport) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err return nil, nil } -func NewBlacklist_crypto_md5(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BlacklistImport{ +func NewBlacklist_crypto_md5(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BlacklistImport{ MetaData: gas.MetaData{ Severity: gas.High, Confidence: gas.High, What: "Use of weak cryptographic primitive", }, Path: `"crypto/md5"`, - } - n = (*ast.ImportSpec)(nil) - return + }, []ast.Node{(*ast.ImportSpec)(nil)} } -func NewBlacklist_crypto_des(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BlacklistImport{ +func NewBlacklist_crypto_des(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BlacklistImport{ MetaData: gas.MetaData{ Severity: gas.High, Confidence: gas.High, What: "Use of weak cryptographic primitive", }, Path: `"crypto/des"`, - } - n = (*ast.ImportSpec)(nil) - return + }, []ast.Node{(*ast.ImportSpec)(nil)} } -func NewBlacklist_crypto_rc4(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BlacklistImport{ +func NewBlacklist_crypto_rc4(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BlacklistImport{ MetaData: gas.MetaData{ Severity: gas.High, Confidence: gas.High, What: "Use of weak cryptographic primitive", }, Path: `"crypto/rc4"`, - } - n = (*ast.ImportSpec)(nil) - return + }, []ast.Node{(*ast.ImportSpec)(nil)} } -func NewBlacklist_net_http_cgi(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BlacklistImport{ +func NewBlacklist_net_http_cgi(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BlacklistImport{ MetaData: gas.MetaData{ Severity: gas.High, Confidence: gas.High, - What: "Go code running under CGI is vulnerable to Httpoxy attack. (CVE-2016-5386)", + What: "Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)", }, Path: `"net/http/cgi"`, - } - n = (*ast.ImportSpec)(nil) - return + }, []ast.Node{(*ast.ImportSpec)(nil)} } diff --git a/rules/errors.go b/rules/errors.go index 8d74634..4490312 100644 --- a/rules/errors.go +++ b/rules/errors.go @@ -50,14 +50,12 @@ func (r *NoErrorCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err err return nil, nil } -func NewNoErrorCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &NoErrorCheck{ +func NewNoErrorCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &NoErrorCheck{ MetaData: gas.MetaData{ Severity: gas.Low, Confidence: gas.High, What: "Errors unhandled.", }, - } - n = (*ast.AssignStmt)(nil) - return + }, []ast.Node{(*ast.AssignStmt)(nil)} } diff --git a/rules/fileperms.go b/rules/fileperms.go index 061876d..9a812cb 100644 --- a/rules/fileperms.go +++ b/rules/fileperms.go @@ -52,7 +52,7 @@ func (r *FilePermissions) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) return nil, nil } -func NewFilePerms(conf map[string]interface{}) (gas.Rule, ast.Node) { +func NewFilePerms(conf map[string]interface{}) (gas.Rule, []ast.Node) { mode := getConfiguredMode(conf, "G302", 0600) return &FilePermissions{ mode: mode, @@ -63,10 +63,10 @@ func NewFilePerms(conf map[string]interface{}) (gas.Rule, ast.Node) { Confidence: gas.High, What: fmt.Sprintf("Expect file permissions to be %#o or less", mode), }, - }, (*ast.CallExpr)(nil) + }, []ast.Node{(*ast.CallExpr)(nil)} } -func NewMkdirPerms(conf map[string]interface{}) (gas.Rule, ast.Node) { +func NewMkdirPerms(conf map[string]interface{}) (gas.Rule, []ast.Node) { mode := getConfiguredMode(conf, "G301", 0700) return &FilePermissions{ mode: mode, @@ -77,5 +77,5 @@ func NewMkdirPerms(conf map[string]interface{}) (gas.Rule, ast.Node) { Confidence: gas.High, What: fmt.Sprintf("Expect directory permissions to be %#o or less", mode), }, - }, (*ast.CallExpr)(nil) + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/hardcoded_credentials.go b/rules/hardcoded_credentials.go index 679749a..45a7993 100644 --- a/rules/hardcoded_credentials.go +++ b/rules/hardcoded_credentials.go @@ -15,43 +15,71 @@ package rules import ( - "go/ast" - "regexp" - gas "github.com/GoASTScanner/gas/core" + "go/ast" + "go/token" + "regexp" ) -type CredsAssign struct { +type Credentials struct { gas.MetaData pattern *regexp.Regexp } -func (r *CredsAssign) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { - if node, ok := n.(*ast.AssignStmt); ok { - for _, i := range node.Lhs { - if ident, ok := i.(*ast.Ident); ok { - if r.pattern.MatchString(ident.Name) { - for _, e := range node.Rhs { - if _, ok := e.(*ast.BasicLit); ok { - return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil - } +func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) { + switch node := n.(type) { + case *ast.AssignStmt: + return r.matchAssign(node, ctx) + case *ast.GenDecl: + return r.matchGenDecl(node, ctx) + } + return nil, nil +} + +func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*gas.Issue, error) { + for _, i := range assign.Lhs { + if ident, ok := i.(*ast.Ident); ok { + if r.pattern.MatchString(ident.Name) { + for _, e := range assign.Rhs { + if _, ok := e.(*ast.BasicLit); ok { + return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil } } } } } - return + return nil, nil } -func NewHardcodedCredentials(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &CredsAssign{ - pattern: regexp.MustCompile(`(?i)passwd|pass|password|pwd|secret|token`), +func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Issue, error) { + if decl.Tok != token.CONST && decl.Tok != token.VAR { + return nil, nil + } + for _, spec := range decl.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok { + for index, ident := range valueSpec.Names { + if r.pattern.MatchString(ident.Name) { + if _, ok := valueSpec.Values[index].(*ast.BasicLit); ok { + return gas.NewIssue(ctx, decl, r.What, r.Severity, r.Confidence), nil + } + } + } + } + } + return nil, nil +} + +func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) { + pattern := `(?i)passwd|pass|password|pwd|secret|token` + if val, ok := conf["G101"]; ok { + pattern = val.(string) + } + return &Credentials{ + pattern: regexp.MustCompile(pattern), MetaData: gas.MetaData{ What: "Potential hardcoded credentials", Confidence: gas.Low, Severity: gas.High, }, - } - n = (*ast.AssignStmt)(nil) - return + }, []ast.Node{(*ast.AssignStmt)(nil), (*ast.GenDecl)(nil)} } diff --git a/rules/hardcoded_credentials_test.go b/rules/hardcoded_credentials_test.go index c34c331..a7a2bd5 100644 --- a/rules/hardcoded_credentials_test.go +++ b/rules/hardcoded_credentials_test.go @@ -39,3 +39,43 @@ func TestHardcoded(t *testing.T) { checkTestResults(t, issues, 1, "Potential hardcoded credentials") } + +func TestHardcodedGlobalVar(t *testing.T) { + config := map[string]interface{}{"ignoreNosec": false} + analyzer := gas.NewAnalyzer(config, nil) + analyzer.AddRule(NewHardcodedCredentials(config)) + + issues := gasTestRunner(` + package samples + + import "fmt" + + var password = "admin" + + func main() { + username := "admin" + fmt.Println("Doing something with: ", username, password) + }`, analyzer) + + checkTestResults(t, issues, 1, "Potential hardcoded credentials") +} + +func TestHardcodedConstant(t *testing.T) { + config := map[string]interface{}{"ignoreNosec": false} + analyzer := gas.NewAnalyzer(config, nil) + analyzer.AddRule(NewHardcodedCredentials(config)) + + issues := gasTestRunner(` + package samples + + import "fmt" + + const password = "secret" + + func main() { + username := "admin" + fmt.Println("Doing something with: ", username, password) + }`, analyzer) + + checkTestResults(t, issues, 1, "Potential hardcoded credentials") +} diff --git a/rules/httpoxy_test.go b/rules/httpoxy_test.go index 758389f..690794e 100644 --- a/rules/httpoxy_test.go +++ b/rules/httpoxy_test.go @@ -32,5 +32,5 @@ func TestHttpoxy(t *testing.T) { ) func main() {}`, analyzer) - checkTestResults(t, issues, 1, "Go code running under CGI is vulnerable to Httpoxy attack.") + checkTestResults(t, issues, 1, "Go versions < 1.6.3 are vulnerable to Httpoxy") } diff --git a/rules/rand.go b/rules/rand.go index 54b6e6c..47ac55d 100644 --- a/rules/rand.go +++ b/rules/rand.go @@ -34,8 +34,8 @@ func (w *WeakRand) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { return nil, nil } -func NewWeakRandCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &WeakRand{ +func NewWeakRandCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &WeakRand{ funcName: "Read", packagePath: "math/rand", MetaData: gas.MetaData{ @@ -43,7 +43,5 @@ func NewWeakRandCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { Confidence: gas.Medium, What: "Use of weak random number generator (math/rand instead of crypto/rand)", }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/rsa.go b/rules/rsa.go index 5872186..510ca78 100644 --- a/rules/rsa.go +++ b/rules/rsa.go @@ -37,9 +37,9 @@ func (w *WeakKeyStrength) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) return nil, nil } -func NewWeakKeyStrength(conf map[string]interface{}) (r gas.Rule, n ast.Node) { +func NewWeakKeyStrength(conf map[string]interface{}) (gas.Rule, []ast.Node) { bits := 2048 - r = &WeakKeyStrength{ + return &WeakKeyStrength{ pattern: regexp.MustCompile(`^rsa\.GenerateKey$`), bits: bits, MetaData: gas.MetaData{ @@ -47,7 +47,5 @@ func NewWeakKeyStrength(conf map[string]interface{}) (r gas.Rule, n ast.Node) { Confidence: gas.High, What: fmt.Sprintf("RSA keys should be at least %d bits", bits), }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/sql.go b/rules/sql.go index 18483d6..10db0a2 100644 --- a/rules/sql.go +++ b/rules/sql.go @@ -56,8 +56,8 @@ func (s *SqlStrConcat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { return nil, nil } -func NewSqlStrConcat(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &SqlStrConcat{ +func NewSqlStrConcat(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &SqlStrConcat{ SqlStatement: SqlStatement{ pattern: regexp.MustCompile(`(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) `), MetaData: gas.MetaData{ @@ -66,9 +66,7 @@ func NewSqlStrConcat(conf map[string]interface{}) (r gas.Rule, n ast.Node) { What: "SQL string concatenation", }, }, - } - n = (*ast.BinaryExpr)(nil) - return + }, []ast.Node{(*ast.BinaryExpr)(nil)} } type SqlStrFormat struct { @@ -86,8 +84,8 @@ func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err err return nil, nil } -func NewSqlStrFormat(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &SqlStrFormat{ +func NewSqlStrFormat(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &SqlStrFormat{ call: regexp.MustCompile(`^fmt\.Sprintf$`), SqlStatement: SqlStatement{ pattern: regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "), @@ -97,7 +95,5 @@ func NewSqlStrFormat(conf map[string]interface{}) (r gas.Rule, n ast.Node) { What: "SQL string formatting", }, }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/subproc.go b/rules/subproc.go index 991bbe2..b5a6fa2 100644 --- a/rules/subproc.go +++ b/rules/subproc.go @@ -49,10 +49,8 @@ func (r *Subprocess) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { return nil, nil } -func NewSubproc(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &Subprocess{ +func NewSubproc(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &Subprocess{ pattern: regexp.MustCompile(`^exec\.Command|syscall\.Exec$`), - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/tempfiles.go b/rules/tempfiles.go index 7ca7570..3d2f49e 100644 --- a/rules/tempfiles.go +++ b/rules/tempfiles.go @@ -36,8 +36,8 @@ func (t *BadTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err erro return nil, nil } -func NewBadTempFile(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &BadTempFile{ +func NewBadTempFile(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &BadTempFile{ call: regexp.MustCompile(`ioutil\.WriteFile|os\.Create`), args: regexp.MustCompile(`^/tmp/.*$|^/var/tmp/.*$`), MetaData: gas.MetaData{ @@ -45,7 +45,5 @@ func NewBadTempFile(conf map[string]interface{}) (r gas.Rule, n ast.Node) { Confidence: gas.High, What: "File creation in shared tmp directory without using ioutil.Tempfile", }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/templates.go b/rules/templates.go index 6b06a63..0f1dc24 100644 --- a/rules/templates.go +++ b/rules/templates.go @@ -37,15 +37,13 @@ func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err er return nil, nil } -func NewTemplateCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &TemplateCheck{ +func NewTemplateCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &TemplateCheck{ call: regexp.MustCompile(`^template\.(HTML|JS|URL)$`), MetaData: gas.MetaData{ Severity: gas.Medium, Confidence: gas.Low, What: "this method will not auto-escape HTML. Verify data is well formed.", }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/tls.go b/rules/tls.go index 301d3d9..c95fa58 100644 --- a/rules/tls.go +++ b/rules/tls.go @@ -109,9 +109,9 @@ func (t *InsecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, er return } -func NewModernTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { +func NewModernTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { // https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility - r = &InsecureConfigTLS{ + return &InsecureConfigTLS{ pattern: regexp.MustCompile(`^tls\.Config$`), MinVersion: 0x0303, // TLS 1.2 only MaxVersion: 0x0303, @@ -121,14 +121,12 @@ func NewModernTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", }, - } - n = (*ast.CompositeLit)(nil) - return + }, []ast.Node{(*ast.CompositeLit)(nil)} } -func NewIntermediateTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { +func NewIntermediateTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { // https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 - r = &InsecureConfigTLS{ + return &InsecureConfigTLS{ pattern: regexp.MustCompile(`^tls\.Config$`), MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 MaxVersion: 0x0303, @@ -149,14 +147,12 @@ func NewIntermediateTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Nod "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", }, - } - n = (*ast.CompositeLit)(nil) - return + }, []ast.Node{(*ast.CompositeLit)(nil)} } -func NewCompatTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { +func NewCompatTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) { // https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29 - r = &InsecureConfigTLS{ + return &InsecureConfigTLS{ pattern: regexp.MustCompile(`^tls\.Config$`), MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 MaxVersion: 0x0303, @@ -179,7 +175,5 @@ func NewCompatTlsCheck(conf map[string]interface{}) (r gas.Rule, n ast.Node) { "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", }, - } - n = (*ast.CompositeLit)(nil) - return + }, []ast.Node{(*ast.CompositeLit)(nil)} } diff --git a/rules/unsafe.go b/rules/unsafe.go index 381f31f..3110727 100644 --- a/rules/unsafe.go +++ b/rules/unsafe.go @@ -15,10 +15,9 @@ package rules import ( + gas "github.com/GoASTScanner/gas/core" "go/ast" "regexp" - - gas "github.com/GoASTScanner/gas/core" ) type UsingUnsafe struct { @@ -33,15 +32,13 @@ func (r *UsingUnsafe) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err erro return nil, nil } -func NewUsingUnsafe(conf map[string]interface{}) (r gas.Rule, n ast.Node) { - r = &UsingUnsafe{ +func NewUsingUnsafe(conf map[string]interface{}) (gas.Rule, []ast.Node) { + return &UsingUnsafe{ pattern: regexp.MustCompile(`unsafe\..*`), MetaData: gas.MetaData{ What: "Use of unsafe calls should be audited", Severity: gas.Low, Confidence: gas.High, }, - } - n = (*ast.CallExpr)(nil) - return + }, []ast.Node{(*ast.CallExpr)(nil)} } diff --git a/rules/weakcrypto.go b/rules/weakcrypto.go index a10de70..1c859e9 100644 --- a/rules/weakcrypto.go +++ b/rules/weakcrypto.go @@ -36,7 +36,7 @@ func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, er } // Uses des.* md5.* or rc4.* -func NewUsesWeakCryptography(conf map[string]interface{}) (gas.Rule, ast.Node) { +func NewUsesWeakCryptography(conf map[string]interface{}) (gas.Rule, []ast.Node) { calls := make(map[string][]string) calls["crypto/des"] = []string{"NewCipher", "NewTripleDESCipher"} calls["crypto/md5"] = []string{"New", "Sum"} @@ -49,5 +49,5 @@ func NewUsesWeakCryptography(conf map[string]interface{}) (gas.Rule, ast.Node) { What: "Use of weak cryptographic primitive", }, } - return rule, (*ast.CallExpr)(nil) + return rule, []ast.Node{(*ast.CallExpr)(nil)} }