diff --git a/core/analyzer.go b/core/analyzer.go index 57f4cc2..5a274ff 100644 --- a/core/analyzer.go +++ b/core/analyzer.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package core holds the central scanning logic used by GAS package core import ( @@ -26,6 +27,9 @@ import ( "strings" ) +// 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. type Context struct { FileSet *token.FileSet Comments ast.CommentMap @@ -35,12 +39,17 @@ type Context struct { Config map[string]interface{} } +// The Rule interface used by all rules supported by GAS. type Rule interface { 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. type RuleSet map[reflect.Type][]Rule +// Metrics used when reporting information about a scanning run. type Metrics struct { NumFiles int `json:"files"` NumLines int `json:"lines"` @@ -48,6 +57,8 @@ type Metrics struct { NumFound int `json:"found"` } +// 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. type Analyzer struct { ignoreNosec bool ruleset RuleSet @@ -57,6 +68,7 @@ type Analyzer struct { Stats Metrics `json:"metrics"` } +// NewAnalyzer buildas a new anaylzer. func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer { if logger == nil { logger = log.New(os.Stdout, "[gas]", 0) @@ -104,6 +116,8 @@ func (gas *Analyzer) process(filename string, source interface{}) error { 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, n ast.Node) { t := reflect.TypeOf(n) if val, ok := gas.ruleset[t]; ok { @@ -113,6 +127,8 @@ func (gas *Analyzer) AddRule(r Rule, n ast.Node) { } } +// Process reads in a source file, convert it to an AST and traverse it. +// Rule methods added with AddRule will be invoked as necessary. func (gas *Analyzer) Process(filename string) error { err := gas.process(filename, nil) fun := func(f *token.File) bool { @@ -123,6 +139,9 @@ func (gas *Analyzer) Process(filename string) error { 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. func (gas *Analyzer) ProcessSource(filename string, source string) error { err := gas.process(filename, source) fun := func(f *token.File) bool { @@ -133,7 +152,8 @@ func (gas *Analyzer) ProcessSource(filename string, source string) error { return err } -func (gas *Analyzer) Ignore(n ast.Node) bool { +// ignore a node (and sub-tree) if it is tagged with a "nosec" comment +func (gas *Analyzer) ignore(n ast.Node) bool { if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec { for _, group := range groups { if strings.Contains(group.Text(), "nosec") { @@ -145,8 +165,10 @@ func (gas *Analyzer) Ignore(n ast.Node) bool { return false } +// Visit runs the GAS visitor logic over an AST created by parsing go code. +// Rule methods added with AddRule will be invoked as necessary. func (gas *Analyzer) Visit(n ast.Node) ast.Visitor { - if !gas.Ignore(n) { + if !gas.ignore(n) { if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok { for _, rule := range val { ret, err := rule.Match(n, &gas.context) diff --git a/core/helpers.go b/core/helpers.go index ddabe26..71e5d99 100644 --- a/core/helpers.go +++ b/core/helpers.go @@ -37,6 +37,7 @@ func selectName(n ast.Node, s reflect.Type) (string, bool) { return "", false } +// MatchCall will match an ast.CallNode if its method name obays the given regex. func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr { t := reflect.TypeOf(&ast.CallExpr{}) if name, ok := selectName(n, t); ok && r.MatchString(name) { @@ -45,6 +46,7 @@ func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr { return nil } +// MatcMatchCompLit hCall will match an ast.CompositeLit if its string value obays the given regex. func MatchCompLit(n ast.Node, r *regexp.Regexp) *ast.CompositeLit { t := reflect.TypeOf(&ast.CompositeLit{}) if name, ok := selectName(n, t); ok && r.MatchString(name) { @@ -53,6 +55,7 @@ func MatchCompLit(n ast.Node, r *regexp.Regexp) *ast.CompositeLit { return nil } +// GetInt will read and return an integer value from an ast.BasicLit func GetInt(n ast.Node) (int64, error) { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.INT { return strconv.ParseInt(node.Value, 0, 64) @@ -60,6 +63,7 @@ func GetInt(n ast.Node) (int64, error) { return 0, fmt.Errorf("Unexpected AST node type: %T", n) } +// GetInt will read and return a float value from an ast.BasicLit func GetFloat(n ast.Node) (float64, error) { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.FLOAT { return strconv.ParseFloat(node.Value, 64) @@ -67,6 +71,7 @@ func GetFloat(n ast.Node) (float64, error) { return 0.0, fmt.Errorf("Unexpected AST node type: %T", n) } +// GetInt will read and return a char value from an ast.BasicLit func GetChar(n ast.Node) (byte, error) { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.CHAR { return node.Value[0], nil @@ -74,6 +79,7 @@ func GetChar(n ast.Node) (byte, error) { return 0, fmt.Errorf("Unexpected AST node type: %T", n) } +// GetInt will read and return a string value from an ast.BasicLit func GetString(n ast.Node) (string, error) { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.STRING { return strconv.Unquote(node.Value) diff --git a/core/issue.go b/core/issue.go index 261e840..22c5044 100644 --- a/core/issue.go +++ b/core/issue.go @@ -20,33 +20,39 @@ import ( "os" ) +// Score type used by severity and confidence values type Score int const ( - Low Score = iota - Medium - High + Low Score = iota // Low value + Medium // Medium value + High // High value ) +// An Issue is returnd by a GAS rule if it discovers an issue with the scanned code. type Issue struct { - Severity Score `json:"severity"` - Confidence Score `json:"confidence"` - What string `json:"details"` - File string `json:"file"` - Code string `json:"code"` - Line int `json:"line"` + Severity Score `json:"severity"` // issue severity (how problematic it is) + Confidence Score `json:"confidence"` // issue confidence (how sure we are we found it) + What string `json:"details"` // Human readable explanation + File string `json:"file"` // File name we found it in + Code string `json:"code"` // Impacted code line + Line int `json:"line"` // Line number in file } +// MetaData is embedded in all GAS rules. The Severity, Confidence and What message +// will be passed tbhrough to reported issues. type MetaData struct { Severity Score Confidence Score What string } +// MarshalJSON is used convert a Score object into a JSON representation func (c Score) MarshalJSON() ([]byte, error) { return json.Marshal(c.String()) } +// String converts a Score into a string func (c Score) String() string { switch c { case High: @@ -74,6 +80,7 @@ func codeSnippet(file *os.File, start int64, end int64, n ast.Node) (string, err return string(buf), nil } +// NewIssue creates a new Issue func NewIssue(ctx *Context, node ast.Node, desc string, severity Score, confidence Score) *Issue { var code string fobj := ctx.FileSet.File(node.Pos()) diff --git a/core/resolve.go b/core/resolve.go index 6c5070c..a2c6273 100644 --- a/core/resolve.go +++ b/core/resolve.go @@ -53,6 +53,9 @@ func resolveCallExpr(n *ast.CallExpr, c *Context) bool { return false } +// TryResolve will attempt, given a subtree starting at some ATS node, to resolve +// all values contained within to a known constant. It is used to check for any +// unkown values in compound expressions. func TryResolve(n ast.Node, c *Context) bool { switch node := n.(type) { case *ast.BasicLit: diff --git a/core/select.go b/core/select.go index 24f2316..e11c946 100644 --- a/core/select.go +++ b/core/select.go @@ -20,7 +20,7 @@ import ( "reflect" ) -// A selector function. This is like a visitor, but has a richer interface. It +// SelectFunc is like an AST visitor, but has a richer interface. It // is called with the current ast.Node being visitied and that nodes depth in // the tree. The function can return true to continue traversing the tree, or // false to end traversal here. @@ -379,6 +379,9 @@ func Select(s Selector, n ast.Node, bits ...reflect.Type) { depthWalk(n, 0, fun) } +// SimpleSelect will try to match a path through a sub-tree starting at a given AST node. +// The type of each node in the path at a given depth must match its entry in list of +// node types given. func SimpleSelect(n ast.Node, bits ...reflect.Type) ast.Node { var found ast.Node fun := func(n ast.Node, d int) bool {