gosec/core/analyzer.go

281 lines
7.9 KiB
Go
Raw Normal View History

2016-07-20 11:02:01 +01:00
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// 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 core holds the central scanning logic used by GAS
2016-07-20 11:02:01 +01:00
package core
import (
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"path"
2016-07-20 11:02:01 +01:00
"reflect"
"regexp"
2016-07-20 11:02:01 +01:00
"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
}
func NewImportInfo() *ImportInfo {
return &ImportInfo{
make(map[string]string),
make(map[string]string),
make(map[string]bool),
}
}
// 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
// this data in conjunction withe the encoutered AST node.
2016-07-20 11:02:01 +01:00
type Context struct {
FileSet *token.FileSet
Comments ast.CommentMap
Info *types.Info
Pkg *types.Package
Root *ast.File
Config map[string]interface{}
Imports *ImportInfo
Ignores []map[string]bool
2016-07-20 11:02:01 +01:00
}
// The Rule interface used by all rules supported by GAS.
2016-07-20 11:02:01 +01:00
type Rule interface {
ID() string
2016-07-20 11:02:01 +01:00
Match(ast.Node, *Context) (*Issue, error)
}
// A RuleSet maps lists of rules to the type of AST node they should be run on.
// The anaylzer will only invoke rules contained in the list associated with the
// type of AST node it is currently visiting.
2016-07-20 11:02:01 +01:00
type RuleSet map[reflect.Type][]Rule
// Metrics used when reporting information about a scanning run.
2016-07-20 11:02:01 +01:00
type Metrics struct {
2016-07-26 00:39:55 +01:00
NumFiles int `json:"files"`
NumLines int `json:"lines"`
NumNosec int `json:"nosec"`
NumFound int `json:"found"`
2016-07-20 11:02:01 +01:00
}
// The Analyzer object is the main object of GAS. It has methods traverse an AST
// and invoke the correct checking rules as on each node as required.
2016-07-20 11:02:01 +01:00
type Analyzer struct {
ignoreNosec bool
2016-07-20 11:02:01 +01:00
ruleset RuleSet
context *Context
2016-07-20 11:02:01 +01:00
logger *log.Logger
Issues []*Issue `json:"issues"`
Stats *Metrics `json:"metrics"`
2016-07-20 11:02:01 +01:00
}
2016-08-28 19:22:08 +01:00
// NewAnalyzer builds a new anaylzer.
func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer {
2016-07-20 11:02:01 +01:00
if logger == nil {
logger = log.New(os.Stdout, "[gas]", 0)
}
a := Analyzer{
ignoreNosec: conf["ignoreNosec"].(bool),
2016-07-20 11:02:01 +01:00
ruleset: make(RuleSet),
context: &Context{nil, nil, nil, nil, nil, nil, nil, nil},
logger: logger,
Issues: make([]*Issue, 0, 16),
Stats: &Metrics{0, 0, 0, 0},
2016-07-20 11:02:01 +01:00
}
// TODO(tkelsey): use the inc/exc lists
return a
2016-07-20 11:02:01 +01:00
}
func (gas *Analyzer) process(filename string, source interface{}) error {
mode := parser.ParseComments
gas.context.FileSet = token.NewFileSet()
2016-07-20 11:02:01 +01:00
root, err := parser.ParseFile(gas.context.FileSet, filename, source, mode)
if err == nil {
gas.context.Comments = ast.NewCommentMap(gas.context.FileSet, root, root.Comments)
gas.context.Root = root
2016-07-20 11:02:01 +01:00
// here we get type info
gas.context.Info = &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
Implicits: make(map[ast.Node]types.Object),
2016-07-20 11:02:01 +01:00
}
conf := types.Config{Importer: importer.Default()}
gas.context.Pkg, err = conf.Check("pkg", gas.context.FileSet, []*ast.File{root}, gas.context.Info)
if err != nil {
// TODO(gm) Type checker not currently considering all files within a package
// see: issue #113
gas.logger.Printf(`Error during type checking: "%s"`, err)
err = nil
}
2016-07-20 11:02:01 +01:00
gas.context.Imports = NewImportInfo()
for _, pkg := range gas.context.Pkg.Imports() {
gas.context.Imports.Imported[pkg.Path()] = pkg.Name()
}
2016-07-20 11:02:01 +01:00
ast.Walk(gas, root)
gas.Stats.NumFiles++
}
return err
}
// 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, 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}
}
2016-07-20 11:02:01 +01:00
}
}
// Process reads in a source file, convert it to an AST and traverse it.
// Rule methods added with AddRule will be invoked as necessary.
2016-07-20 11:02:01 +01:00
func (gas *Analyzer) Process(filename string) error {
err := gas.process(filename, nil)
fun := func(f *token.File) bool {
gas.Stats.NumLines += f.LineCount()
return true
}
gas.context.FileSet.Iterate(fun)
return err
}
// ProcessSource will convert a source code string into an AST and traverse it.
// Rule methods added with AddRule will be invoked as necessary. The string is
// identified by the filename given but no file IO will be done.
2016-07-20 11:02:01 +01:00
func (gas *Analyzer) ProcessSource(filename string, source string) error {
err := gas.process(filename, source)
fun := func(f *token.File) bool {
gas.Stats.NumLines += f.LineCount()
return true
}
gas.context.FileSet.Iterate(fun)
return err
}
// ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
func (gas *Analyzer) ignore(n ast.Node) ([]string, bool) {
if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
2016-07-20 11:02:01 +01:00
for _, group := range groups {
if strings.Contains(group.Text(), "#nosec") {
return nil, true
}
if strings.Contains(group.Text(), "#exclude") {
2016-07-20 11:02:01 +01:00
gas.Stats.NumNosec++
// Pull out the specific rules that are listed to be ignored.
re := regexp.MustCompile("!(G\\d{3})")
matches := re.FindAllStringSubmatch(group.Text(), -1)
// Find the rule IDs to ignore.
ignores := make([]string, 0)
for _, v := range matches {
ignores = append(ignores, v[1])
}
return ignores, false
2016-07-20 11:02:01 +01:00
}
}
}
return nil, false
2016-07-20 11:02:01 +01:00
}
// Visit runs the GAS visitor logic over an AST created by parsing go code.
// Rule methods added with AddRule will be invoked as necessary.
2016-07-20 11:02:01 +01:00
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
// If we've reached the end of this branch, pop off the ignores stack.
if n == nil {
if len(gas.context.Ignores) > 0 {
gas.context.Ignores = gas.context.Ignores[1:]
}
return gas
}
// Get any new rule exclusions.
ignoredRules, ignoreAll := gas.ignore(n)
if ignoreAll {
return nil
}
// Now create the union of exclusions.
ignores := make(map[string]bool, 0)
if len(gas.context.Ignores) > 0 {
for k, v := range gas.context.Ignores[0] {
ignores[k] = v
}
}
for _, v := range ignoredRules {
ignores[v] = true
}
// Push the new set onto the stack.
gas.context.Ignores = append([]map[string]bool{ignores}, gas.context.Ignores...)
// Track aliased and initialization imports
if imported, ok := n.(*ast.ImportSpec); ok {
path := strings.Trim(imported.Path.Value, `"`)
if imported.Name != nil {
if imported.Name.Name == "_" {
// Initialization import
gas.context.Imports.InitOnly[path] = true
} else {
// Aliased import
gas.context.Imports.Aliased[path] = imported.Name.Name
}
}
// unsafe is not included in Package.Imports()
if path == "unsafe" {
gas.context.Imports.Imported[path] = path
}
}
if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok {
for _, rule := range val {
if _, ok := ignores[rule.ID()]; ok {
continue
}
ret, err := rule.Match(n, gas.context)
if err != nil {
file, line := GetLocation(n, gas.context)
file = path.Base(file)
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
}
if ret != nil {
gas.Issues = append(gas.Issues, ret)
gas.Stats.NumFound++
2016-07-20 11:02:01 +01:00
}
}
}
return gas
2016-07-20 11:02:01 +01:00
}