Merge pull request #75 from GoASTScanner/experimental

Track package imports and aliases
This commit is contained in:
Grant Murphy 2016-11-07 19:40:26 -08:00 committed by GitHub
commit 9f54d257fe
5 changed files with 128 additions and 21 deletions

View file

@ -27,6 +27,13 @@ import (
"strings" "strings"
) )
// ImportInfo is used to track aliased and initialization only imports.
type ImportInfo struct {
Imported map[string]string
Aliased map[string]string
InitOnly map[string]bool
}
// The Context is populated with data parsed from the source code as it is scanned. // The Context is populated with data parsed from the source code as it is scanned.
// It is passed through to all rule functions as they are called. Rules may use // It is passed through to all rule functions as they are called. Rules may use
// this data in conjunction withe the encoutered AST node. // this data in conjunction withe the encoutered AST node.
@ -37,6 +44,7 @@ type Context struct {
Pkg *types.Package Pkg *types.Package
Root *ast.File Root *ast.File
Config map[string]interface{} Config map[string]interface{}
Imports *ImportInfo
} }
// The Rule interface used by all rules supported by GAS. // The Rule interface used by all rules supported by GAS.
@ -77,8 +85,20 @@ func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer {
ignoreNosec: conf["ignoreNosec"].(bool), ignoreNosec: conf["ignoreNosec"].(bool),
ruleset: make(RuleSet), ruleset: make(RuleSet),
Issues: make([]Issue, 0), Issues: make([]Issue, 0),
context: Context{token.NewFileSet(), nil, nil, nil, nil, nil}, context: Context{
logger: logger, token.NewFileSet(),
nil,
nil,
nil,
nil,
nil,
&ImportInfo{
make(map[string]string),
make(map[string]string),
make(map[string]bool),
},
},
logger: logger,
} }
// TODO(tkelsey): use the inc/exc lists // TODO(tkelsey): use the inc/exc lists
@ -110,6 +130,10 @@ func (gas *Analyzer) process(filename string, source interface{}) error {
return err return err
} }
for _, pkg := range gas.context.Pkg.Imports() {
gas.context.Imports.Imported[pkg.Path()] = pkg.Name()
}
ast.Walk(gas, root) ast.Walk(gas, root)
gas.Stats.NumFiles++ gas.Stats.NumFiles++
} }
@ -169,6 +193,20 @@ func (gas *Analyzer) ignore(n ast.Node) bool {
// Rule methods added with AddRule will be invoked as necessary. // Rule methods added with AddRule will be invoked as necessary.
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor { func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
if !gas.ignore(n) { if !gas.ignore(n) {
// Track aliased and initialization imports
if imported, ok := n.(*ast.ImportSpec); ok {
if imported.Name != nil {
path := strings.Trim(imported.Path.Value, `"`)
if imported.Name.Name == "_" {
// Initialization import
gas.context.Imports.InitOnly[path] = true
} else {
// Aliased import
gas.context.Imports.Aliased[imported.Name.Name] = path
}
}
}
if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok { if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok {
for _, rule := range val { for _, rule := range val {
ret, err := rule.Match(n, &gas.context) ret, err := rule.Match(n, &gas.context)

View file

@ -47,19 +47,45 @@ func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr {
return nil return nil
} }
// MatchCallByObject ses the type checker to resolve the associated object with a // MatchCallByPackage ensures that the specified package is imported,
// particular *ast.CallExpr. This object is used to determine if the // adjusts the name for any aliases and ignores cases that are
// package and identifier name matches the passed in parameters. // initialization only imports.
// //
// Usage: // Usage:
// node, obj := MatchCall(n, ctx, "math/rand", "Read") // node, matched := MatchCallByPackage(n, ctx, "math/rand", "Read")
// //
func MatchCallByObject(n ast.Node, c *Context, pkg, name string) (*ast.CallExpr, types.Object) { func MatchCallByPackage(n ast.Node, c *Context, pkg string, names ...string) (*ast.CallExpr, bool) {
call, obj := GetCallObject(n, c)
if obj != nil && obj.Pkg().Path() == pkg && obj.Name() == name { importName, imported := c.Imports.Imported[pkg]
return call, obj if !imported {
return nil, false
} }
return nil, nil
if _, initonly := c.Imports.InitOnly[pkg]; initonly {
return nil, false
}
if alias, ok := c.Imports.Aliased[pkg]; ok {
importName = alias
}
switch node := n.(type) {
case *ast.CallExpr:
switch fn := node.Fun.(type) {
case *ast.SelectorExpr:
switch expr := fn.X.(type) {
case *ast.Ident:
if expr.Name == importName {
for _, name := range names {
if fn.Sel.Name == name {
return node, true
}
}
}
}
}
}
return nil, false
} }
// MatchCompLit will match an ast.CompositeLit if its string value obays the given regex. // MatchCompLit will match an ast.CompositeLit if its string value obays the given regex.

View file

@ -27,7 +27,7 @@ type BlacklistImport struct {
func (r *BlacklistImport) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { func (r *BlacklistImport) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node, ok := n.(*ast.ImportSpec); ok { if node, ok := n.(*ast.ImportSpec); ok {
if r.Path == node.Path.Value { if r.Path == node.Path.Value && node.Name.String() != "_" {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
} }
} }

38
rules/blacklist_test.go Normal file
View file

@ -0,0 +1,38 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"testing"
)
const initOnlyImportSrc = `
package main
import (
_ "crypto/md5"
"fmt"
)
func main() {
for _, arg := range os.Args {
fmt.Println(arg)
}
}`
func TestInitOnlyImport(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_crypto_md5(config))
issues := gasTestRunner(initOnlyImportSrc, analyzer)
checkTestResults(t, issues, 0, "")
}

View file

@ -16,33 +16,38 @@ package rules
import ( import (
"go/ast" "go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core" gas "github.com/GoASTScanner/gas/core"
) )
type UsesWeakCryptography struct { type UsesWeakCryptography struct {
gas.MetaData gas.MetaData
pattern *regexp.Regexp blacklist map[string][]string
} }
func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node := gas.MatchCall(n, r.pattern); node != nil {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil for pkg, funcs := range r.blacklist {
if _, matched := gas.MatchCallByPackage(n, c, pkg, funcs...); matched {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
}
} }
return nil, nil return nil, nil
} }
// Uses des.* md5.* or rc4.* // Uses des.* md5.* or rc4.*
func NewUsesWeakCryptography(conf map[string]interface{}) (r gas.Rule, n ast.Node) { func NewUsesWeakCryptography(conf map[string]interface{}) (gas.Rule, ast.Node) {
r = &UsesWeakCryptography{ calls := make(map[string][]string)
pattern: regexp.MustCompile(`des\.NewCipher|des\.NewTripleDESCipher|md5\.New|md5\.Sum|rc4\.NewCipher`), calls["crypto/des"] = []string{"NewCipher", "NewTripleDESCipher"}
calls["crypto/md5"] = []string{"New", "Sum"}
calls["crypto/rc4"] = []string{"NewCipher"}
rule := &UsesWeakCryptography{
blacklist: calls,
MetaData: gas.MetaData{ MetaData: gas.MetaData{
Severity: gas.Medium, Severity: gas.Medium,
Confidence: gas.High, Confidence: gas.High,
What: "Use of weak cryptographic primitive", What: "Use of weak cryptographic primitive",
}, },
} }
n = (*ast.CallExpr)(nil) return rule, (*ast.CallExpr)(nil)
return
} }