Adding some inline documentation for godoc

This commit is contained in:
Tim Kelsey 2016-08-12 14:17:28 +01:00
parent 37205e9afa
commit 223cded656
5 changed files with 53 additions and 12 deletions

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package core holds the central scanning logic used by GAS
package core package core
import ( import (
@ -26,6 +27,9 @@ import (
"strings" "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 { type Context struct {
FileSet *token.FileSet FileSet *token.FileSet
Comments ast.CommentMap Comments ast.CommentMap
@ -35,12 +39,17 @@ type Context struct {
Config map[string]interface{} Config map[string]interface{}
} }
// The Rule interface used by all rules supported by GAS.
type Rule interface { type Rule interface {
Match(ast.Node, *Context) (*Issue, error) 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 type RuleSet map[reflect.Type][]Rule
// Metrics used when reporting information about a scanning run.
type Metrics struct { type Metrics struct {
NumFiles int `json:"files"` NumFiles int `json:"files"`
NumLines int `json:"lines"` NumLines int `json:"lines"`
@ -48,6 +57,8 @@ type Metrics struct {
NumFound int `json:"found"` 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 { type Analyzer struct {
ignoreNosec bool ignoreNosec bool
ruleset RuleSet ruleset RuleSet
@ -57,6 +68,7 @@ type Analyzer struct {
Stats Metrics `json:"metrics"` Stats Metrics `json:"metrics"`
} }
// NewAnalyzer buildas a new anaylzer.
func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer { func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer {
if logger == nil { if logger == nil {
logger = log.New(os.Stdout, "[gas]", 0) logger = log.New(os.Stdout, "[gas]", 0)
@ -104,6 +116,8 @@ func (gas *Analyzer) process(filename string, source interface{}) error {
return err 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) { func (gas *Analyzer) AddRule(r Rule, n ast.Node) {
t := reflect.TypeOf(n) t := reflect.TypeOf(n)
if val, ok := gas.ruleset[t]; ok { 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 { func (gas *Analyzer) Process(filename string) error {
err := gas.process(filename, nil) err := gas.process(filename, nil)
fun := func(f *token.File) bool { fun := func(f *token.File) bool {
@ -123,6 +139,9 @@ func (gas *Analyzer) Process(filename string) error {
return err 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 { func (gas *Analyzer) ProcessSource(filename string, source string) error {
err := gas.process(filename, source) err := gas.process(filename, source)
fun := func(f *token.File) bool { fun := func(f *token.File) bool {
@ -133,7 +152,8 @@ func (gas *Analyzer) ProcessSource(filename string, source string) error {
return err 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 { if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
for _, group := range groups { for _, group := range groups {
if strings.Contains(group.Text(), "nosec") { if strings.Contains(group.Text(), "nosec") {
@ -145,8 +165,10 @@ func (gas *Analyzer) Ignore(n ast.Node) bool {
return false 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 { 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 { 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

@ -37,6 +37,7 @@ func selectName(n ast.Node, s reflect.Type) (string, bool) {
return "", false 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 { func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr {
t := reflect.TypeOf(&ast.CallExpr{}) t := reflect.TypeOf(&ast.CallExpr{})
if name, ok := selectName(n, t); ok && r.MatchString(name) { 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 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 { func MatchCompLit(n ast.Node, r *regexp.Regexp) *ast.CompositeLit {
t := reflect.TypeOf(&ast.CompositeLit{}) t := reflect.TypeOf(&ast.CompositeLit{})
if name, ok := selectName(n, t); ok && r.MatchString(name) { 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 return nil
} }
// GetInt will read and return an integer value from an ast.BasicLit
func GetInt(n ast.Node) (int64, error) { func GetInt(n ast.Node) (int64, error) {
if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.INT { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.INT {
return strconv.ParseInt(node.Value, 0, 64) 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) 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) { func GetFloat(n ast.Node) (float64, error) {
if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.FLOAT { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.FLOAT {
return strconv.ParseFloat(node.Value, 64) 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) 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) { func GetChar(n ast.Node) (byte, error) {
if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.CHAR { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.CHAR {
return node.Value[0], nil 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) 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) { func GetString(n ast.Node) (string, error) {
if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.STRING { if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.STRING {
return strconv.Unquote(node.Value) return strconv.Unquote(node.Value)

View file

@ -20,33 +20,39 @@ import (
"os" "os"
) )
// Score type used by severity and confidence values
type Score int type Score int
const ( const (
Low Score = iota Low Score = iota // Low value
Medium Medium // Medium value
High High // High value
) )
// An Issue is returnd by a GAS rule if it discovers an issue with the scanned code.
type Issue struct { type Issue struct {
Severity Score `json:"severity"` Severity Score `json:"severity"` // issue severity (how problematic it is)
Confidence Score `json:"confidence"` Confidence Score `json:"confidence"` // issue confidence (how sure we are we found it)
What string `json:"details"` What string `json:"details"` // Human readable explanation
File string `json:"file"` File string `json:"file"` // File name we found it in
Code string `json:"code"` Code string `json:"code"` // Impacted code line
Line int `json:"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 { type MetaData struct {
Severity Score Severity Score
Confidence Score Confidence Score
What string What string
} }
// MarshalJSON is used convert a Score object into a JSON representation
func (c Score) MarshalJSON() ([]byte, error) { func (c Score) MarshalJSON() ([]byte, error) {
return json.Marshal(c.String()) return json.Marshal(c.String())
} }
// String converts a Score into a string
func (c Score) String() string { func (c Score) String() string {
switch c { switch c {
case High: case High:
@ -74,6 +80,7 @@ func codeSnippet(file *os.File, start int64, end int64, n ast.Node) (string, err
return string(buf), nil return string(buf), nil
} }
// NewIssue creates a new Issue
func NewIssue(ctx *Context, node ast.Node, desc string, severity Score, confidence Score) *Issue { func NewIssue(ctx *Context, node ast.Node, desc string, severity Score, confidence Score) *Issue {
var code string var code string
fobj := ctx.FileSet.File(node.Pos()) fobj := ctx.FileSet.File(node.Pos())

View file

@ -53,6 +53,9 @@ func resolveCallExpr(n *ast.CallExpr, c *Context) bool {
return false 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 { func TryResolve(n ast.Node, c *Context) bool {
switch node := n.(type) { switch node := n.(type) {
case *ast.BasicLit: case *ast.BasicLit:

View file

@ -20,7 +20,7 @@ import (
"reflect" "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 // 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 // the tree. The function can return true to continue traversing the tree, or
// false to end traversal here. // false to end traversal here.
@ -379,6 +379,9 @@ func Select(s Selector, n ast.Node, bits ...reflect.Type) {
depthWalk(n, 0, fun) 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 { func SimpleSelect(n ast.Node, bits ...reflect.Type) ast.Node {
var found ast.Node var found ast.Node
fun := func(n ast.Node, d int) bool { fun := func(n ast.Node, d int) bool {