Major rework of codebase

- Get rid of 'core' and move CLI to cmd/gas directory
- Migrate (most) tests to use Ginkgo and testutils framework
- GAS now expects package to reside in $GOPATH
- GAS now can resolve dependencies for better type checking (if package
  on GOPATH)
- Simplified public API
This commit is contained in:
Grant Murphy 2017-07-19 15:17:00 -06:00
parent f4b705a864
commit 6943f9e5e4
51 changed files with 1189 additions and 2695 deletions

View file

@ -18,9 +18,11 @@ package gas
import (
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"path"
"reflect"
"strings"
@ -64,10 +66,11 @@ type Analyzer struct {
// NewAnalyzer builds a new anaylzer.
func NewAnalyzer(conf Config, logger *log.Logger) *Analyzer {
ignoreNoSec := false
if val, err := conf.Get("ignoreNoSec"); err == nil {
if override, ok := val.(bool); ok {
ignoreNoSec = override
}
if setting, err := conf.GetGlobal("nosec"); err == nil {
ignoreNoSec = setting == "true" || setting == "enabled"
}
if logger == nil {
logger = log.New(os.Stderr, "[gas]", log.LstdFlags)
}
return &Analyzer{
ignoreNosec: ignoreNoSec,
@ -94,7 +97,7 @@ func (gas *Analyzer) Process(packagePath string) error {
return err
}
packageConfig := loader.Config{Build: &build.Default}
packageConfig := loader.Config{Build: &build.Default, ParserMode: parser.ParseComments}
packageFiles := make([]string, 0)
for _, filename := range basePackage.GoFiles {
packageFiles = append(packageFiles, path.Join(packagePath, filename))
@ -168,3 +171,10 @@ func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
func (gas *Analyzer) Report() ([]*Issue, *Metrics) {
return gas.issues, gas.stats
}
// Reset clears state such as context, issues and metrics from the configured analyzer
func (gas *Analyzer) Reset() {
gas.context = &Context{}
gas.issues = make([]*Issue, 0, 16)
gas.stats = &Metrics{}
}

113
analyzer_test.go Normal file
View file

@ -0,0 +1,113 @@
package gas_test
import (
"bytes"
"io/ioutil"
"log"
"os"
"strings"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/rules"
"github.com/GoASTScanner/gas/testutils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Analyzer", func() {
var (
analyzer *gas.Analyzer
logger *log.Logger
output *bytes.Buffer
)
BeforeEach(func() {
logger, output = testutils.NewLogger()
analyzer = gas.NewAnalyzer(nil, logger)
})
Context("when processing a package", func() {
It("should return an error if the package contains no Go files", func() {
analyzer.LoadRules(rules.Generate().Builders()...)
dir, err := ioutil.TempDir("", "empty")
defer os.RemoveAll(dir)
Expect(err).ShouldNot(HaveOccurred())
err = analyzer.Process(dir)
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(MatchRegexp("no buildable Go source files"))
})
It("should return an error if the package fails to build", func() {
analyzer.LoadRules(rules.Generate().Builders()...)
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("wonky.go", `func main(){ println("forgot the package")}`)
pkg.Build()
err := analyzer.Process(pkg.Path)
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(MatchRegexp(`expected 'package'`))
})
It("should find errors when nosec is not in use", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
controlPackage := testutils.NewTestPackage()
defer controlPackage.Close()
controlPackage.AddFile("md5.go", source)
controlPackage.Build()
analyzer.Process(controlPackage.Path)
controlIssues, _ := analyzer.Report()
Expect(controlIssues).Should(HaveLen(sample.Errors))
})
It("should not report errors when a nosec comment is present", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close()
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
nosecPackage.AddFile("md5.go", nosecSource)
nosecPackage.Build()
analyzer.Process(nosecPackage.Path)
nosecIssues, _ := analyzer.Report()
Expect(nosecIssues).Should(BeEmpty())
})
})
It("should be possible to overwrite nosec comments, and report issues", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
// overwrite nosec option
nosecIgnoreConfig := gas.NewConfig()
nosecIgnoreConfig.SetGlobal("nosec", "true")
customAnalyzer := gas.NewAnalyzer(nosecIgnoreConfig, logger)
customAnalyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close()
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
nosecPackage.AddFile("md5.go", nosecSource)
nosecPackage.Build()
customAnalyzer.Process(nosecPackage.Path)
nosecIssues, _ := customAnalyzer.Report()
Expect(nosecIssues).Should(HaveLen(sample.Errors))
})
})

View file

@ -55,19 +55,19 @@ func (c CallList) Contains(selector, ident string) bool {
/// ContainsCallExpr resolves the call expression name and type
/// or package and determines if it exists within the CallList
func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) bool {
func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) *ast.CallExpr {
selector, ident, err := GetCallInfo(n, ctx)
if err != nil {
return false
return nil
}
// Try direct resolution
if c.Contains(selector, ident) {
return true
return n.(*ast.CallExpr)
}
// Also support explicit path
if path, ok := GetImportPath(selector, ctx); ok {
return c.Contains(path, ident)
if path, ok := GetImportPath(selector, ctx); ok && c.Contains(path, ident) {
return n.(*ast.CallExpr)
}
return false
return nil
}

View file

@ -1,60 +1,86 @@
package gas
package gas_test
import (
"go/ast"
"testing"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/testutils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
type callListRule struct {
MetaData
callList CallList
matched int
}
func (r *callListRule) Match(n ast.Node, c *Context) (gi *Issue, err error) {
if r.callList.ContainsCallExpr(n, c) {
r.matched += 1
}
return nil, nil
}
func TestCallListContainsCallExpr(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := NewAnalyzer(config, nil)
calls := NewCallList()
calls.AddAll("bytes.Buffer", "Write", "WriteTo")
rule := &callListRule{
MetaData: MetaData{
Severity: Low,
Confidence: Low,
What: "A dummy rule",
},
callList: calls,
matched: 0,
}
analyzer.AddRule(rule, []ast.Node{(*ast.CallExpr)(nil)})
source := `
package main
import (
"bytes"
"fmt"
var _ = Describe("call list", func() {
var (
calls gas.CallList
)
func main() {
var b bytes.Buffer
b.Write([]byte("Hello "))
fmt.Fprintf(&b, "world!")
}`
BeforeEach(func() {
calls = gas.NewCallList()
})
analyzer.ProcessSource("dummy.go", source)
if rule.matched != 1 {
t.Errorf("Expected to match a bytes.Buffer.Write call")
}
}
It("should not return any matches when empty", func() {
Expect(calls.Contains("foo", "bar")).Should(BeFalse())
})
func TestCallListContains(t *testing.T) {
callList := NewCallList()
callList.Add("fmt", "Printf")
if !callList.Contains("fmt", "Printf") {
t.Errorf("Expected call list to contain fmt.Printf")
}
}
It("should be possible to add a single call", func() {
Expect(calls).Should(HaveLen(0))
calls.Add("foo", "bar")
Expect(calls).Should(HaveLen(1))
expected := make(map[string]bool)
expected["bar"] = true
actual := map[string]bool(calls["foo"])
Expect(actual).Should(Equal(expected))
})
It("should be possible to add multiple calls at once", func() {
Expect(calls).Should(HaveLen(0))
calls.AddAll("fmt", "Sprint", "Sprintf", "Printf", "Println")
expected := map[string]bool{
"Sprint": true,
"Sprintf": true,
"Printf": true,
"Println": true,
}
actual := map[string]bool(calls["fmt"])
Expect(actual).Should(Equal(expected))
})
It("should not return a match if none are present", func() {
calls.Add("ioutil", "Copy")
Expect(calls.Contains("fmt", "Println")).Should(BeFalse())
})
It("should match a call based on selector and ident", func() {
calls.Add("ioutil", "Copy")
Expect(calls.Contains("ioutil", "Copy")).Should(BeTrue())
})
It("should match a call expression", func() {
// Create file to be scanned
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("md5.go", testutils.SampleCodeG401[0].Code)
ctx := pkg.CreateContext("md5.go")
// Search for md5.New()
calls.Add("md5", "New")
// Stub out visitor and count number of matched call expr
matched := 0
v := testutils.NewMockVisitor()
v.Context = ctx
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
if _, ok := n.(*ast.CallExpr); ok && calls.ContainsCallExpr(n, ctx) != nil {
matched++
}
return true
}
ast.Walk(v, ctx.Root)
Expect(matched).Should(Equal(1))
})
})

View file

@ -1,251 +0,0 @@
package main
import (
"reflect"
"testing"
)
func Test_newFileList(t *testing.T) {
type args struct {
paths []string
}
tests := []struct {
name string
args args
want *fileList
}{
{
name: "nil paths",
args: args{paths: nil},
want: &fileList{patterns: map[string]struct{}{}},
},
{
name: "empty paths",
args: args{paths: []string{}},
want: &fileList{patterns: map[string]struct{}{}},
},
{
name: "have paths",
args: args{paths: []string{"*_test.go"}},
want: &fileList{patterns: map[string]struct{}{
"*_test.go": struct{}{},
}},
},
}
for _, tt := range tests {
if got := newFileList(tt.args.paths...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. newFileList() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_fileList_String(t *testing.T) {
type fields struct {
patterns []string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "nil patterns",
fields: fields{patterns: nil},
want: "",
},
{
name: "empty patterns",
fields: fields{patterns: []string{}},
want: "",
},
{
name: "one pattern",
fields: fields{patterns: []string{"foo"}},
want: "foo",
},
{
name: "two patterns",
fields: fields{patterns: []string{"bar", "foo"}},
want: "bar, foo",
},
}
for _, tt := range tests {
f := newFileList(tt.fields.patterns...)
if got := f.String(); got != tt.want {
t.Errorf("%q. fileList.String() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_fileList_Set(t *testing.T) {
type fields struct {
patterns []string
}
type args struct {
path string
}
tests := []struct {
name string
fields fields
args args
want map[string]struct{}
wantErr bool
}{
{
name: "add empty path",
fields: fields{patterns: nil},
args: args{path: ""},
want: map[string]struct{}{},
wantErr: false,
},
{
name: "add path to nil patterns",
fields: fields{patterns: nil},
args: args{path: "foo"},
want: map[string]struct{}{
"foo": struct{}{},
},
wantErr: false,
},
{
name: "add path to empty patterns",
fields: fields{patterns: []string{}},
args: args{path: "foo"},
want: map[string]struct{}{
"foo": struct{}{},
},
wantErr: false,
},
{
name: "add path to populated patterns",
fields: fields{patterns: []string{"foo"}},
args: args{path: "bar"},
want: map[string]struct{}{
"foo": struct{}{},
"bar": struct{}{},
},
wantErr: false,
},
}
for _, tt := range tests {
f := newFileList(tt.fields.patterns...)
if err := f.Set(tt.args.path); (err != nil) != tt.wantErr {
t.Errorf("%q. fileList.Set() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if !reflect.DeepEqual(f.patterns, tt.want) {
t.Errorf("%q. got state fileList.patterns = %v, want state %v", tt.name, f.patterns, tt.want)
}
}
}
func Test_fileList_Contains(t *testing.T) {
type fields struct {
patterns []string
}
type args struct {
path string
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "nil patterns",
fields: fields{patterns: nil},
args: args{path: "foo"},
want: false,
},
{
name: "empty patterns",
fields: fields{patterns: nil},
args: args{path: "foo"},
want: false,
},
{
name: "one pattern, no wildcard, no match",
fields: fields{patterns: []string{"foo"}},
args: args{path: "bar"},
want: false,
},
{
name: "one pattern, no wildcard, match",
fields: fields{patterns: []string{"foo"}},
args: args{path: "foo"},
want: true,
},
{
name: "one pattern, wildcard prefix, match",
fields: fields{patterns: []string{"*foo"}},
args: args{path: "foo"},
want: true,
},
{
name: "one pattern, wildcard suffix, match",
fields: fields{patterns: []string{"foo*"}},
args: args{path: "foo"},
want: true,
},
{
name: "one pattern, wildcard both ends, match",
fields: fields{patterns: []string{"*foo*"}},
args: args{path: "foo"},
want: true,
},
{
name: "default test match 1",
fields: fields{patterns: []string{"*_test.go"}},
args: args{path: "foo_test.go"},
want: true,
},
{
name: "default test match 2",
fields: fields{patterns: []string{"*_test.go"}},
args: args{path: "bar/foo_test.go"},
want: true,
},
{
name: "default test match 3",
fields: fields{patterns: []string{"*_test.go"}},
args: args{path: "/bar/foo_test.go"},
want: true,
},
{
name: "default test match 4",
fields: fields{patterns: []string{"*_test.go"}},
args: args{path: "baz/bar/foo_test.go"},
want: true,
},
{
name: "default test match 5",
fields: fields{patterns: []string{"*_test.go"}},
args: args{path: "/baz/bar/foo_test.go"},
want: true,
},
{
name: "many patterns, no match",
fields: fields{patterns: []string{"*_one.go", "*_two.go"}},
args: args{path: "/baz/bar/foo_test.go"},
want: false,
},
{
name: "many patterns, match",
fields: fields{patterns: []string{"*_one.go", "*_two.go", "*_test.go"}},
args: args{path: "/baz/bar/foo_test.go"},
want: true,
},
{
name: "sub-folder, match",
fields: fields{patterns: []string{"vendor"}},
args: args{path: "/baz/vendor/bar/foo_test.go"},
want: true,
},
}
for _, tt := range tests {
f := newFileList(tt.fields.patterns...)
if got := f.Contains(tt.args.path); got != tt.want {
t.Errorf("%q. fileList.Contains() = %v, want %v", tt.name, got, tt.want)
}
}
}

View file

@ -117,6 +117,9 @@ func loadConfig(configFile string) (gas.Config, error) {
return nil, err
}
}
if *flagIgnoreNoSec {
config.SetGlobal("nosec", "true")
}
return config, nil
}

View file

@ -1,45 +0,0 @@
package main
import "testing"
func Test_shouldInclude(t *testing.T) {
type args struct {
path string
excluded *fileList
}
tests := []struct {
name string
args args
want bool
}{
{
name: "non .go file",
args: args{
path: "thing.txt",
excluded: newFileList(),
},
want: false,
},
{
name: ".go file, not excluded",
args: args{
path: "thing.go",
excluded: newFileList(),
},
want: true,
},
{
name: ".go file, excluded",
args: args{
path: "thing.go",
excluded: newFileList("thing.go"),
},
want: false,
},
}
for _, tt := range tests {
if got := shouldInclude(tt.args.path, tt.args.excluded); got != tt.want {
t.Errorf("%q. shouldInclude() = %v, want %v", tt.name, got, tt.want)
}
}
}

View file

@ -15,7 +15,9 @@ type Config map[string]interface{}
// needs to be loaded via c.ReadFrom(strings.NewReader("config data"))
// or from a *os.File.
func NewConfig() Config {
return make(Config)
cfg := make(Config)
cfg["global"] = make(map[string]string)
return cfg
}
// ReadFrom implements the io.ReaderFrom interface. This
@ -26,7 +28,7 @@ func (c Config) ReadFrom(r io.Reader) (int64, error) {
if err != nil {
return int64(len(data)), err
}
if err = json.Unmarshal(data, c); err != nil {
if err = json.Unmarshal(data, &c); err != nil {
return int64(len(data)), err
}
return int64(len(data)), nil
@ -42,39 +44,39 @@ func (c Config) WriteTo(w io.Writer) (int64, error) {
return io.Copy(w, bytes.NewReader(data))
}
// EnableRule will change the rule to the specified enabled state
func (c Config) EnableRule(ruleID string, enabled bool) {
if data, found := c["rules"]; found {
if rules, ok := data.(map[string]bool); ok {
rules[ruleID] = enabled
}
}
}
// Enabled returns a list of rules that are enabled
func (c Config) Enabled() []string {
if data, found := c["rules"]; found {
if rules, ok := data.(map[string]bool); ok {
enabled := make([]string, len(rules))
for ruleID := range rules {
enabled = append(enabled, ruleID)
}
return enabled
}
}
return nil
}
// Get returns the configuration section for a given rule
func (c Config) Get(ruleID string) (interface{}, error) {
section, found := c[ruleID]
// Get returns the configuration section for the supplied key
func (c Config) Get(section string) (interface{}, error) {
settings, found := c[section]
if !found {
return nil, fmt.Errorf("Rule %s not in configuration", ruleID)
return nil, fmt.Errorf("Section %s not in configuration", section)
}
return section, nil
return settings, nil
}
// Set section for a given rule
func (c Config) Set(ruleID string, val interface{}) {
c[ruleID] = val
// Set section in the configuration to specified value
func (c Config) Set(section string, value interface{}) {
c[section] = value
}
// GetGlobal returns value associated with global configuration option
func (c Config) GetGlobal(option string) (string, error) {
if globals, ok := c["global"]; ok {
if settings, ok := globals.(map[string]string); ok {
if value, ok := settings[option]; ok {
return value, nil
}
return "", fmt.Errorf("global setting for %s not found", option)
}
}
return "", fmt.Errorf("no global config options found")
}
// SetGlobal associates a value with a global configuration ooption
func (c Config) SetGlobal(option, value string) {
if globals, ok := c["global"]; ok {
if settings, ok := globals.(map[string]string); ok {
settings[option] = value
}
}
}

103
config_test.go Normal file
View file

@ -0,0 +1,103 @@
package gas_test
import (
"bytes"
"github.com/GoASTScanner/gas"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Configuration", func() {
var configuration gas.Config
BeforeEach(func() {
configuration = gas.NewConfig()
})
Context("when loading from disk", func() {
It("should be possible to load configuration from a file", func() {
json := `{"G101": {}}`
buffer := bytes.NewBufferString(json)
nread, err := configuration.ReadFrom(buffer)
Expect(nread).Should(Equal(int64(len(json))))
Expect(err).ShouldNot(HaveOccurred())
})
It("should return an error if configuration file is invalid", func() {
var err error
invalidBuffer := bytes.NewBuffer([]byte{0xc0, 0xff, 0xee})
_, err = configuration.ReadFrom(invalidBuffer)
Expect(err).Should(HaveOccurred())
emptyBuffer := bytes.NewBuffer([]byte{})
_, err = configuration.ReadFrom(emptyBuffer)
Expect(err).Should(HaveOccurred())
})
})
Context("when saving to disk", func() {
It("should be possible to save an empty configuration to file", func() {
expected := `{"global":{}}`
buffer := bytes.NewBuffer([]byte{})
nbytes, err := configuration.WriteTo(buffer)
Expect(int(nbytes)).Should(Equal(len(expected)))
Expect(err).ShouldNot(HaveOccurred())
Expect(buffer.String()).Should(Equal(expected))
})
It("should be possible to save configuration to file", func() {
configuration.Set("G101", map[string]string{
"mode": "strict",
})
buffer := bytes.NewBuffer([]byte{})
nbytes, err := configuration.WriteTo(buffer)
Expect(int(nbytes)).ShouldNot(BeZero())
Expect(err).ShouldNot(HaveOccurred())
Expect(buffer.String()).Should(Equal(`{"G101":{"mode":"strict"},"global":{}}`))
})
})
Context("when configuring rules", func() {
It("should be possible to get configuration for a rule", func() {
settings := map[string]string{
"ciphers": "AES256-GCM",
}
configuration.Set("G101", settings)
retrieved, err := configuration.Get("G101")
Expect(err).ShouldNot(HaveOccurred())
Expect(retrieved).Should(HaveKeyWithValue("ciphers", "AES256-GCM"))
Expect(retrieved).ShouldNot(HaveKey("foobar"))
})
})
Context("when using global configuration options", func() {
It("should have a default global section", func() {
settings, err := configuration.Get("global")
Expect(err).Should(BeNil())
expectedType := make(map[string]string)
Expect(settings).Should(BeAssignableToTypeOf(expectedType))
})
It("should save global settings to correct section", func() {
configuration.SetGlobal("nosec", "enabled")
settings, err := configuration.Get("global")
Expect(err).Should(BeNil())
if globals, ok := settings.(map[string]string); ok {
Expect(globals["nosec"]).Should(MatchRegexp("enabled"))
} else {
Fail("globals are not defined as map")
}
setValue, err := configuration.GetGlobal("nosec")
Expect(err).Should(BeNil())
Expect(setValue).Should(MatchRegexp("enabled"))
})
})
})

13
gas_suite_test.go Normal file
View file

@ -0,0 +1,13 @@
package gas_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestGas(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gas Suite")
}

View file

@ -19,34 +19,9 @@ import (
"go/ast"
"go/token"
"go/types"
"reflect"
"regexp"
"strconv"
"strings"
)
// helpfull "canned" matching routines ----------------------------------------
func selectName(n ast.Node, s reflect.Type) (string, bool) {
t := reflect.TypeOf(&ast.SelectorExpr{})
if node, ok := SimpleSelect(n, s, t).(*ast.SelectorExpr); ok {
t = reflect.TypeOf(&ast.Ident{})
if ident, ok := SimpleSelect(node.X, t).(*ast.Ident); ok {
return strings.Join([]string{ident.Name, node.Sel.Name}, "."), ok
}
}
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) {
return n.(*ast.CallExpr)
}
return nil
}
// MatchCallByPackage ensures that the specified package is imported,
// adjusts the name for any aliases and ignores cases that are
// initialization only imports.
@ -100,11 +75,13 @@ func MatchCallByType(n ast.Node, ctx *Context, requiredType string, calls ...str
return nil, false
}
// MatchCompLit 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) {
return n.(*ast.CompositeLit)
// MatchCompLit will match an ast.CompositeLit based on the supplied type
func MatchCompLit(n ast.Node, ctx *Context, required string) *ast.CompositeLit {
if complit, ok := n.(*ast.CompositeLit); ok {
typeOf := ctx.Info.TypeOf(complit)
if typeOf.String() == required {
return complit
}
}
return nil
}
@ -117,7 +94,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
// GetFloat 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)
@ -125,7 +102,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
// GetChar 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
@ -133,7 +110,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
// GetString 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)
@ -170,12 +147,10 @@ func GetCallInfo(n ast.Node, ctx *Context) (string, string, error) {
t := ctx.Info.TypeOf(expr)
if t != nil {
return t.String(), fn.Sel.Name, nil
} else {
return "undefined", fn.Sel.Name, fmt.Errorf("missing type info")
}
} else {
return expr.Name, fn.Sel.Name, nil
return "undefined", fn.Sel.Name, fmt.Errorf("missing type info")
}
return expr.Name, fn.Sel.Name, nil
}
case *ast.Ident:
return ctx.Pkg.Name(), fn.Name, nil
@ -205,7 +180,7 @@ func GetImportedName(path string, ctx *Context) (string, bool) {
// GetImportPath resolves the full import path of an identifer based on
// the imports in the current context.
func GetImportPath(name string, ctx *Context) (string, bool) {
for path, _ := range ctx.Imports.Imported {
for path := range ctx.Imports.Imported {
if imported, ok := GetImportedName(path, ctx); ok && imported == name {
return path, true
}

View file

@ -1,71 +1,14 @@
package gas
package gas_test
import (
"go/ast"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
type dummyCallback func(ast.Node, *Context, string, ...string) (*ast.CallExpr, bool)
type dummyRule struct {
MetaData
pkgOrType string
funcsOrMethods []string
callback dummyCallback
callExpr []ast.Node
matched int
}
func (r *dummyRule) Match(n ast.Node, c *Context) (gi *Issue, err error) {
if callexpr, matched := r.callback(n, c, r.pkgOrType, r.funcsOrMethods...); matched {
r.matched += 1
r.callExpr = append(r.callExpr, callexpr)
}
return nil, nil
}
func TestMatchCallByType(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := NewAnalyzer(config, nil)
rule := &dummyRule{
MetaData: MetaData{
Severity: Low,
Confidence: Low,
What: "A dummy rule",
},
pkgOrType: "bytes.Buffer",
funcsOrMethods: []string{"Write"},
callback: MatchCallByType,
callExpr: []ast.Node{},
matched: 0,
}
analyzer.AddRule(rule, []ast.Node{(*ast.CallExpr)(nil)})
source := `
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Write([]byte("Hello "))
fmt.Fprintf(&b, "world!")
}`
analyzer.ProcessSource("dummy.go", source)
if rule.matched != 1 || len(rule.callExpr) != 1 {
t.Errorf("Expected to match a bytes.Buffer.Write call")
}
typeName, callName, err := GetCallInfo(rule.callExpr[0], analyzer.context)
if err != nil {
t.Errorf("Unable to resolve call info: %v\n", err)
}
if typeName != "bytes.Buffer" {
t.Errorf("Expected: %s, Got: %s\n", "bytes.Buffer", typeName)
}
if callName != "Write" {
t.Errorf("Expected: %s, Got: %s\n", "Write", callName)
}
}
var _ = Describe("Helpers", func() {
Context("todo", func() {
It("should fail", func() {
Expect(1).Should(Equal(2))
})
})
})

View file

@ -34,9 +34,11 @@ func NewImportTracker() *ImportTracker {
func (t *ImportTracker) TrackPackages(pkgs ...*types.Package) {
for _, pkg := range pkgs {
for _, imp := range pkg.Imports() {
t.Imported[imp.Path()] = imp.Name()
}
t.Imported[pkg.Path()] = pkg.Name()
// Transient imports
//for _, imp := range pkg.Imports() {
// t.Imported[imp.Path()] = imp.Name()
//}
}
}
@ -52,8 +54,6 @@ func (t *ImportTracker) TrackImport(n ast.Node) {
t.Aliased[path] = imported.Name.Name
}
}
// unsafe is not included in Package.Imports()
if path == "unsafe" {
t.Imported[path] = path
}

30
import_tracker_test.go Normal file
View file

@ -0,0 +1,30 @@
package gas_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("ImportTracker", func() {
var (
source string
)
BeforeEach(func() {
source = `// TODO(gm)`
})
Context("when I have a valid go package", func() {
It("should record all import specs", func() {
Expect(1).Should(Equal(1))
Fail("Not implemented")
})
It("should correctly track aliased package imports", func() {
Fail("Not implemented")
})
It("should correctly track init only packages", func() {
Fail("Not implemented")
})
})
})

37
issue_test.go Normal file
View file

@ -0,0 +1,37 @@
package gas_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Issue", func() {
Context("when creating a new issue", func() {
It("should provide a code snippet for the specified ast.Node", func() {
Expect(1).Should(Equal(2))
Fail("Not implemented")
})
It("should return an error if specific context is not able to be obtained", func() {
Fail("Not implemented")
})
It("should provide accurate line and file information", func() {
Fail("Not implemented")
})
It("should maintain the provided severity score", func() {
Fail("Not implemented")
})
It("should maintain the provided confidence score", func() {
Fail("Not implemented")
})
It("should correctly record `unsafe` import as not considered a package", func() {
Fail("Not implemented")
})
})
})

View file

@ -17,6 +17,7 @@ package gas
import "go/ast"
func resolveIdent(n *ast.Ident, c *Context) bool {
if n.Obj == nil || n.Obj.Kind != ast.Var {
return true
}

95
resolve_test.go Normal file
View file

@ -0,0 +1,95 @@
package gas_test
import (
"go/ast"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/testutils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Resolve ast node to concrete value", func() {
Context("when attempting to resolve an ast node", func() {
It("should successfully resolve basic literal", func() {
var basicLiteral *ast.BasicLit
pkg := testutils.NewTestPackage()
pkg.AddFile("foo.go", `package main; const foo = "bar"; func main(){}`)
ctx := pkg.CreateContext("foo.go")
v := testutils.NewMockVisitor()
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
if node, ok := n.(*ast.BasicLit); ok {
basicLiteral = node
return false
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(basicLiteral).ShouldNot(BeNil())
Expect(gas.TryResolve(basicLiteral, ctx)).Should(BeTrue())
})
It("should successfully resolve identifier", func() {
var ident *ast.Ident
pkg := testutils.NewTestPackage()
pkg.AddFile("foo.go", `package main; var foo string = "bar"; func main(){}`)
ctx := pkg.CreateContext("foo.go")
v := testutils.NewMockVisitor()
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
if node, ok := n.(*ast.Ident); ok {
ident = node
return false
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(ident).ShouldNot(BeNil())
Expect(gas.TryResolve(ident, ctx)).Should(BeTrue())
})
It("should successfully resolve assign statement", func() {
var assign *ast.AssignStmt
pkg := testutils.NewTestPackage()
pkg.AddFile("foo.go", `package main; const x = "bar"; func main(){ y := x; println(y) }`)
ctx := pkg.CreateContext("foo.go")
v := testutils.NewMockVisitor()
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
if node, ok := n.(*ast.AssignStmt); ok {
if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "y" {
assign = node
}
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(assign).ShouldNot(BeNil())
Expect(gas.TryResolve(assign, ctx)).Should(BeTrue())
})
It("should successfully resolve a binary statement", func() {
var target *ast.BinaryExpr
pkg := testutils.NewTestPackage()
pkg.AddFile("foo.go", `package main; const (x = "bar"; y = "baz"); func main(){ z := x + y; println(z) }`)
ctx := pkg.CreateContext("foo.go")
v := testutils.NewMockVisitor()
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
if node, ok := n.(*ast.BinaryExpr); ok {
target = node
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(target).ShouldNot(BeNil())
Expect(gas.TryResolve(target, ctx)).Should(BeTrue())
})
// TODO: It should resolve call expressions
})
})

85
rule_test.go Normal file
View file

@ -0,0 +1,85 @@
package gas_test
import (
"fmt"
"go/ast"
"github.com/GoASTScanner/gas"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
type mockrule struct {
issue *gas.Issue
err error
callback func(n ast.Node, ctx *gas.Context) bool
}
func (m *mockrule) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
if m.callback(n, ctx) {
return m.issue, nil
}
return nil, m.err
}
var _ = Describe("Rule", func() {
Context("when using a ruleset", func() {
var (
ruleset gas.RuleSet
dummyErrorRule gas.Rule
dummyIssueRule gas.Rule
)
JustBeforeEach(func() {
ruleset = gas.NewRuleSet()
dummyErrorRule = &mockrule{
issue: nil,
err: fmt.Errorf("An unexpected error occurred"),
callback: func(n ast.Node, ctx *gas.Context) bool { return false },
}
dummyIssueRule = &mockrule{
issue: &gas.Issue{
Severity: gas.High,
Confidence: gas.High,
What: `Some explanation of the thing`,
File: "main.go",
Code: `#include <stdio.h> int main(){ puts("hello world"); }`,
Line: 42,
},
err: nil,
callback: func(n ast.Node, ctx *gas.Context) bool { return true },
}
})
It("should be possible to register a rule for multiple ast.Node", func() {
registeredNodeA := (*ast.CallExpr)(nil)
registeredNodeB := (*ast.AssignStmt)(nil)
unregisteredNode := (*ast.BinaryExpr)(nil)
ruleset.Register(dummyIssueRule, registeredNodeA, registeredNodeB)
Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty())
Expect(ruleset.RegisteredFor(registeredNodeA)).Should(ContainElement(dummyIssueRule))
Expect(ruleset.RegisteredFor(registeredNodeB)).Should(ContainElement(dummyIssueRule))
})
It("should not register a rule when no ast.Nodes are specified", func() {
ruleset.Register(dummyErrorRule)
Expect(ruleset).Should(BeEmpty())
})
It("should be possible to retrieve a list of rules for a given node type", func() {
registeredNode := (*ast.CallExpr)(nil)
unregisteredNode := (*ast.AssignStmt)(nil)
ruleset.Register(dummyErrorRule, registeredNode)
ruleset.Register(dummyIssueRule, registeredNode)
Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty())
Expect(ruleset.RegisteredFor(registeredNode)).Should(HaveLen(2))
Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyErrorRule))
Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyIssueRule))
})
})
})

View file

@ -1,49 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestBigExp(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewUsingBigExp(config))
issues := gasTestRunner(`
package main
import (
"math/big"
)
func main() {
z := new(big.Int)
x := new(big.Int)
x = x.SetUint64(2)
y := new(big.Int)
y = y.SetUint64(4)
m := new(big.Int)
m = m.SetUint64(0)
z = z.Exp(x, y, m)
}
`, analyzer)
checkTestResults(t, issues, 1, "Use of math/big.Int.Exp function")
}

View file

@ -24,24 +24,29 @@ import (
// Looks for net.Listen("0.0.0.0") or net.Listen(":8080")
type BindsToAllNetworkInterfaces struct {
gas.MetaData
call *regexp.Regexp
calls gas.CallList
pattern *regexp.Regexp
}
func (r *BindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCall(n, r.call); node != nil {
if arg, err := gas.GetString(node.Args[1]); err == nil {
if r.pattern.MatchString(arg) {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
}
func (r *BindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
callExpr := r.calls.ContainsCallExpr(n, c)
if callExpr == nil {
return nil, nil
}
if arg, err := gas.GetString(callExpr.Args[1]); err == nil {
if r.pattern.MatchString(arg) {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
}
}
return
return nil, nil
}
func NewBindsToAllNetworkInterfaces(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("net", "Listen")
calls.Add("tls", "Listen")
return &BindsToAllNetworkInterfaces{
call: regexp.MustCompile(`^(net|tls)\.Listen$`),
calls: calls,
pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`),
MetaData: gas.MetaData{
Severity: gas.Medium,

View file

@ -1,65 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestBind0000(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
issues := gasTestRunner(`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", "0.0.0.0:2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, analyzer)
checkTestResults(t, issues, 1, "Binds to all network interfaces")
}
func TestBindEmptyHost(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
issues := gasTestRunner(`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", ":2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, analyzer)
checkTestResults(t, issues, 1, "Binds to all network interfaces")
}

View file

@ -1,40 +0,0 @@
// 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 (
"testing"
"github.com/GoASTScanner/gas"
)
const initOnlyImportSrc = `
package main
import (
_ "crypto/md5"
"fmt"
"os"
)
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

@ -49,7 +49,7 @@ func (r *NoErrorCheck) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
switch stmt := n.(type) {
case *ast.AssignStmt:
for _, expr := range stmt.Rhs {
if callExpr, ok := expr.(*ast.CallExpr); ok && !r.whitelist.ContainsCallExpr(callExpr, ctx) {
if callExpr, ok := expr.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(expr, ctx) == nil {
pos := returnsError(callExpr, ctx)
if pos < 0 || pos >= len(stmt.Lhs) {
return nil, nil
@ -60,7 +60,7 @@ func (r *NoErrorCheck) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
}
}
case *ast.ExprStmt:
if callExpr, ok := stmt.X.(*ast.CallExpr); ok && !r.whitelist.ContainsCallExpr(callExpr, ctx) {
if callExpr, ok := stmt.X.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(stmt.X, ctx) == nil {
pos := returnsError(callExpr, ctx)
if pos >= 0 {
return gas.NewIssue(ctx, n, r.What, r.Severity, r.Confidence), nil

View file

@ -1,144 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestErrorsMulti(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func test() (int,error) {
return 0, nil
}
func main() {
v, _ := test()
fmt.Println(v)
}`, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
}
func TestErrorsSingle(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func a() error {
return fmt.Errorf("This is an error")
}
func b() {
fmt.Println("b")
}
func c() string {
return fmt.Sprintf("This isn't anything")
}
func main() {
_ = a()
a()
b()
_ = c()
c()
}`, analyzer)
checkTestResults(t, issues, 2, "Errors unhandled")
}
func TestErrorsGood(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func test() err error {
return 0, nil
}
func main() {
e := test()
}`, analyzer)
checkTestResults(t, issues, 0, "")
}
func TestErrorsWhitelisted(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G104": map[string][]string{
"compress/zlib": []string{"NewReader"},
"io": []string{"Copy"},
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
source := `package main
import (
"io"
"os"
"fmt"
"bytes"
"compress/zlib"
)
func a() error {
return fmt.Errorf("This is an error ok")
}
func main() {
// Expect at least one failure
_ = a()
var b bytes.Buffer
// Default whitelist
nbytes, _ := b.Write([]byte("Hello "))
if nbytes <= 0 {
os.Exit(1)
}
// Whitelisted via configuration
r, _ := zlib.NewReader(&b)
io.Copy(os.Stdout, r)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
}

View file

@ -1,56 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestChmod(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewFilePerms(config))
issues := gasTestRunner(`
package main
import "os"
func main() {
os.Chmod("/tmp/somefile", 0777)
os.Chmod("/tmp/someotherfile", 0600)
os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0666)
os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600)
}`, analyzer)
checkTestResults(t, issues, 2, "Expect file permissions")
}
func TestMkdir(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewMkdirPerms(config))
issues := gasTestRunner(`
package main
import "os"
func main() {
os.Mkdir("/tmp/mydir", 0777)
os.Mkdir("/tmp/mydir", 0600)
os.MkdirAll("/tmp/mydir/mysubidr", 0775)
}`, analyzer)
checkTestResults(t, issues, 2, "Expect directory permissions")
}

View file

@ -1,194 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestHardcoded(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedWithEntropy(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "secret"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
}
func TestHardcodedIgnoreEntropy(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G101": map[string]string{
"ignore_entropy": "true",
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "admin"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
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 = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
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 = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedConstantMulti(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 (
username = "user"
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardecodedVarsNotAssigned(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
var password string
func init() {
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedConstInteger(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
const (
ATNStateSomethingElse = 1
ATNStateTokenStart = 42
)
func main() {
println(ATNStateTokenStart)
}`, analyzer)
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
}
func TestHardcodedConstString(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
const (
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
println(ATNStateTokenStart)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}

View file

@ -1,39 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestHttpoxy(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_net_http_cgi(config))
issues := gasTestRunner(`
package main
import (
"net/http/cgi"
"net/http"
)
func main() {
cgi.Serve(http.FileServer(http.Dir("/usr/share/doc")))
}`, analyzer)
checkTestResults(t, issues, 1, "Go versions < 1.6.3 are vulnerable to Httpoxy")
}

View file

@ -1,85 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestNosec(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", os.Getenv("BLAH")) // #nosec
cmd.Run()
}`, analyzer)
checkTestResults(t, issues, 0, "None")
}
func TestNosecBlock(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
// #nosec
if true {
cmd := exec.Command("sh", "-c", os.Getenv("BLAH"))
cmd.Run()
}
}`, analyzer)
checkTestResults(t, issues, 0, "None")
}
func TestNosecIgnore(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": true}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", os.Args[1]) // #nosec
cmd.Run()
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with variable.")
}

View file

@ -1,85 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestRandOk(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import "crypto/rand"
func main() {
good, _ := rand.Read(nil)
println(good)
}`, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestRandBad(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import "math/rand"
func main() {
bad := rand.Int()
println(bad)
}`, analyzer)
checkTestResults(t, issues, 1, "Use of weak random number generator (math/rand instead of crypto/rand)")
}
func TestRandRenamed(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import (
"crypto/rand"
mrand "math/rand"
)
func main() {
good, _ := rand.Read(nil)
println(good)
i := mrand.Int31()
println(i)
}`, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}

View file

@ -17,20 +17,19 @@ package rules
import (
"fmt"
"go/ast"
"regexp"
"github.com/GoASTScanner/gas"
)
type WeakKeyStrength struct {
gas.MetaData
pattern *regexp.Regexp
bits int
calls gas.CallList
bits int
}
func (w *WeakKeyStrength) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node := gas.MatchCall(n, w.pattern); node != nil {
if bits, err := gas.GetInt(node.Args[1]); err == nil && bits < (int64)(w.bits) {
if callExpr := w.calls.ContainsCallExpr(n, c); callExpr != nil {
if bits, err := gas.GetInt(callExpr.Args[1]); err == nil && bits < (int64)(w.bits) {
return gas.NewIssue(c, n, w.What, w.Severity, w.Confidence), nil
}
}
@ -38,10 +37,12 @@ func (w *WeakKeyStrength) Match(n ast.Node, c *gas.Context) (*gas.Issue, error)
}
func NewWeakKeyStrength(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("rsa", "GenerateKey")
bits := 2048
return &WeakKeyStrength{
pattern: regexp.MustCompile(`^rsa\.GenerateKey$`),
bits: bits,
calls: calls,
bits: bits,
MetaData: gas.MetaData{
Severity: gas.Medium,
Confidence: gas.High,

View file

@ -1,50 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestRSAKeys(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakKeyStrength(config))
issues := gasTestRunner(
`package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
)
func main() {
//Generate Private Key
pvk, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
fmt.Println(err)
}
fmt.Println(pvk)
}`, analyzer)
checkTestResults(t, issues, 1, "RSA keys should")
}

13
rules/rules_suite_test.go Normal file
View file

@ -0,0 +1,13 @@
package rules_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestRules(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Rules Suite")
}

67
rules/rules_test.go Normal file
View file

@ -0,0 +1,67 @@
package rules_test
import (
"bytes"
"fmt"
"log"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/rules"
"github.com/GoASTScanner/gas/testutils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("gas rules", func() {
var (
logger *log.Logger
output *bytes.Buffer
config gas.Config
analyzer *gas.Analyzer
runner func(string, []testutils.CodeSample)
)
BeforeEach(func() {
logger, output = testutils.NewLogger()
config = gas.NewConfig()
analyzer = gas.NewAnalyzer(config, logger)
runner = func(rule string, samples []testutils.CodeSample) {
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, rule)).Builders()...)
for n, sample := range samples {
analyzer.Reset()
pkg := testutils.NewTestPackage()
pkg.AddFile(fmt.Sprintf("sample_%d.go", n), sample.Code)
pkg.Build()
e := analyzer.Process(pkg.Path)
Expect(e).ShouldNot(HaveOccurred())
issues, _ := analyzer.Report()
if len(issues) != sample.Errors {
fmt.Println(sample.Code)
}
Expect(issues).Should(HaveLen(sample.Errors))
}
}
})
Context("report correct errors for all samples", func() {
It("should work for G101 samples", func() {
runner("G101", testutils.SampleCodeG101)
})
It("should work for G102 samples", func() {
runner("G102", testutils.SampleCodeG102)
})
It("should work for G103 samples", func() {
runner("G103", testutils.SampleCodeG103)
})
It("should work for G104 samples", func() {
runner("G104", testutils.SampleCodeG104)
})
})
})

View file

@ -71,12 +71,14 @@ func NewSqlStrConcat(conf gas.Config) (gas.Rule, []ast.Node) {
type SqlStrFormat struct {
SqlStatement
call *regexp.Regexp
calls gas.CallList
}
// Looks for "fmt.Sprintf("SELECT * FROM foo where '%s', userInput)"
func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCall(n, s.call); node != nil {
func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
// TODO(gm) improve confidence if database/sql is being used
if node := s.calls.ContainsCallExpr(n, c); node != nil {
if arg, e := gas.GetString(node.Args[0]); s.pattern.MatchString(arg) && e == nil {
return gas.NewIssue(c, n, s.What, s.Severity, s.Confidence), nil
}
@ -85,8 +87,8 @@ func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err err
}
func NewSqlStrFormat(conf gas.Config) (gas.Rule, []ast.Node) {
return &SqlStrFormat{
call: regexp.MustCompile(`^fmt\.Sprintf$`),
rule := &SqlStrFormat{
calls: gas.NewCallList(),
SqlStatement: SqlStatement{
pattern: regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "),
MetaData: gas.MetaData{
@ -95,5 +97,7 @@ func NewSqlStrFormat(conf gas.Config) (gas.Rule, []ast.Node) {
What: "SQL string formatting",
},
},
}, []ast.Node{(*ast.CallExpr)(nil)}
}
rule.calls.AddAll("fmt", "Sprint", "Sprintf", "Sprintln")
return rule, []ast.Node{(*ast.CallExpr)(nil)}
}

View file

@ -1,216 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestSQLInjectionViaConcatenation(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
"os"
)
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query("SELECT * FROM foo WHERE name = " + os.Args[1])
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "SQL string concatenation")
}
func TestSQLInjectionViaIntepolation(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
"fmt"
"os"
//_ "github.com/mattn/go-sqlite3"
)
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", os.Args[1])
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "SQL string formatting")
}
func TestSQLInjectionFalsePositiveA(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < 32"
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveB(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < 32"
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveC(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < "
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery + "32")
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveD(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
const age = "32"
var staticQuery = "SELECT * FROM foo WHERE age < "
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery + age)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}

View file

@ -16,41 +16,42 @@ package rules
import (
"go/ast"
"regexp"
"strings"
"go/types"
"github.com/GoASTScanner/gas"
)
type Subprocess struct {
pattern *regexp.Regexp
gas.CallList
}
// TODO(gm) The only real potential for command injection with a Go project
// is something like this:
//
// syscall.Exec("/bin/sh", []string{"-c", tainted})
//
// E.g. Input is correctly escaped but the execution context being used
// is unsafe. For example:
//
// syscall.Exec("echo", "foobar" + tainted)
func (r *Subprocess) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node := gas.MatchCall(n, r.pattern); node != nil {
if node := r.ContainsCallExpr(n, c); node != nil {
for _, arg := range node.Args {
if !gas.TryResolve(arg, c) {
what := "Subprocess launching with variable."
return gas.NewIssue(c, n, what, gas.High, gas.High), nil
if ident, ok := arg.(*ast.Ident); ok {
obj := c.Info.ObjectOf(ident)
if _, ok := obj.(*types.Var); ok && !gas.TryResolve(ident, c) {
return gas.NewIssue(c, n, "Subprocess launched with variable", gas.Medium, gas.High), nil
}
}
}
// call with partially qualified command
if str, err := gas.GetString(node.Args[0]); err == nil {
if !strings.HasPrefix(str, "/") {
what := "Subprocess launching with partial path."
return gas.NewIssue(c, n, what, gas.Medium, gas.High), nil
}
}
what := "Subprocess launching should be audited."
return gas.NewIssue(c, n, what, gas.Low, gas.High), nil
return gas.NewIssue(c, n, "Subprocess launching should be audited", gas.Low, gas.High), nil
}
return nil, nil
}
func NewSubproc(conf gas.Config) (gas.Rule, []ast.Node) {
return &Subprocess{
pattern: regexp.MustCompile(`^exec\.Command|syscall\.Exec$`),
}, []ast.Node{(*ast.CallExpr)(nil)}
rule := &Subprocess{gas.NewCallList()}
rule.Add("exec", "Command")
rule.Add("syscall", "Exec")
return rule, []ast.Node{(*ast.CallExpr)(nil)}
}

View file

@ -1,124 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestSubprocess(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os/exec"
)
func main() {
val := "/bin/" + "sleep"
cmd := exec.Command(val, "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching should be audited.")
}
func TestSubprocessVar(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os"
"os/exec"
)
func main() {
run := "sleep" + os.Getenv("SOMETHING")
cmd := exec.Command(run, "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with variable.")
}
func TestSubprocessPath(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with partial path.")
}
func TestSubprocessSyscall(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"syscall"
)
func main() {
syscall.Exec("/bin/cat", []string{ "/etc/passwd" }, nil)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching should be audited.")
}

View file

@ -23,12 +23,12 @@ import (
type BadTempFile struct {
gas.MetaData
args *regexp.Regexp
call *regexp.Regexp
calls gas.CallList
args *regexp.Regexp
}
func (t *BadTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCall(n, t.call); node != nil {
if node := t.calls.ContainsCallExpr(n, c); node != nil {
if arg, e := gas.GetString(node.Args[0]); t.args.MatchString(arg) && e == nil {
return gas.NewIssue(c, n, t.What, t.Severity, t.Confidence), nil
}
@ -37,9 +37,12 @@ func (t *BadTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err erro
}
func NewBadTempFile(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("ioutil", "WriteFile")
calls.Add("os", "Create")
return &BadTempFile{
call: regexp.MustCompile(`ioutil\.WriteFile|os\.Create`),
args: regexp.MustCompile(`^/tmp/.*$|^/var/tmp/.*$`),
calls: calls,
args: regexp.MustCompile(`^/tmp/.*$|^/var/tmp/.*$`),
MetaData: gas.MetaData{
Severity: gas.Medium,
Confidence: gas.High,

View file

@ -1,47 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestTempfiles(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBadTempFile(config))
source := `
package samples
import (
"io/ioutil"
"os"
)
func main() {
file1, _ := os.Create("/tmp/demo1")
defer file1.Close()
ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644)
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 2, "shared tmp directory")
}

View file

@ -16,18 +16,17 @@ package rules
import (
"go/ast"
"regexp"
"github.com/GoASTScanner/gas"
)
type TemplateCheck struct {
gas.MetaData
call *regexp.Regexp
calls gas.CallList
}
func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCall(n, t.call); node != nil {
func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node := t.calls.ContainsCallExpr(n, c); node != nil {
for _, arg := range node.Args {
if _, ok := arg.(*ast.BasicLit); !ok { // basic lits are safe
return gas.NewIssue(c, n, t.What, t.Severity, t.Confidence), nil
@ -38,8 +37,9 @@ func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err er
}
func NewTemplateCheck(conf gas.Config) (gas.Rule, []ast.Node) {
return &TemplateCheck{
call: regexp.MustCompile(`^template\.(HTML|JS|URL)$`),
calls: gas.NewCallList(),
MetaData: gas.MetaData{
Severity: gas.Medium,
Confidence: gas.Low,

View file

@ -1,136 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestTemplateCheckSafe(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.HTML("<script>alert(1)</script>"),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadHTML(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.HTML(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadJS(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.JS(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadURL(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.URL(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}

View file

@ -17,17 +17,15 @@ package rules
import (
"fmt"
"go/ast"
"reflect"
"regexp"
"github.com/GoASTScanner/gas"
)
type InsecureConfigTLS struct {
MinVersion int16
MaxVersion int16
pattern *regexp.Regexp
goodCiphers []string
MinVersion int16
MaxVersion int16
requiredType string
goodCiphers []string
}
func stringInSlice(a string, list []string) bool {
@ -39,15 +37,24 @@ func stringInSlice(a string, list []string) bool {
return false
}
func (t *InsecureConfigTLS) processTlsCipherSuites(n ast.Node, c *gas.Context) *gas.Issue {
a := reflect.TypeOf(&ast.KeyValueExpr{})
b := reflect.TypeOf(&ast.CompositeLit{})
if node, ok := gas.SimpleSelect(n, a, b).(*ast.CompositeLit); ok {
for _, elt := range node.Elts {
if ident, ok := elt.(*ast.SelectorExpr); ok {
if !stringInSlice(ident.Sel.Name, t.goodCiphers) {
str := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name)
return gas.NewIssue(c, n, str, gas.High, gas.High)
func (t *InsecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gas.Context) *gas.Issue {
tlsConfig := gas.MatchCompLit(n, c, t.requiredType)
if tlsConfig == nil {
return nil
}
for _, expr := range tlsConfig.Elts {
if keyvalExpr, ok := expr.(*ast.KeyValueExpr); ok {
if keyname, ok := keyvalExpr.Key.(*ast.Ident); ok && keyname.Name == "CipherSuites" {
if ciphers, ok := keyvalExpr.Value.(*ast.CompositeLit); ok {
for _, cipher := range ciphers.Elts {
if ident, ok := cipher.(*ast.SelectorExpr); ok {
if !stringInSlice(ident.Sel.Name, t.goodCiphers) {
str := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name)
return gas.NewIssue(c, n, str, gas.High, gas.High)
}
}
}
}
}
}
@ -97,7 +104,7 @@ func (t *InsecureConfigTLS) processTlsConfVal(n *ast.KeyValueExpr, c *gas.Contex
}
case "CipherSuites":
if ret := t.processTlsCipherSuites(n, c); ret != nil {
if ret := t.processTLSCipherSuites(n, c); ret != nil {
return ret
}
@ -108,7 +115,7 @@ func (t *InsecureConfigTLS) processTlsConfVal(n *ast.KeyValueExpr, c *gas.Contex
}
func (t *InsecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCompLit(n, t.pattern); node != nil {
if node := gas.MatchCompLit(n, c, t.requiredType); node != nil {
for _, elt := range node.Elts {
if kve, ok := elt.(*ast.KeyValueExpr); ok {
gi = t.processTlsConfVal(kve, c)
@ -124,9 +131,9 @@ func (t *InsecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, er
func NewModernTlsCheck(conf gas.Config) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
MinVersion: 0x0303, // TLS 1.2 only
MaxVersion: 0x0303,
requiredType: "tls.Config",
MinVersion: 0x0303, // TLS 1.2 only
MaxVersion: 0x0303,
goodCiphers: []string{
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
@ -139,9 +146,9 @@ func NewModernTlsCheck(conf gas.Config) (gas.Rule, []ast.Node) {
func NewIntermediateTlsCheck(conf gas.Config) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
requiredType: "tls.Config",
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
goodCiphers: []string{
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA",
@ -165,9 +172,9 @@ func NewIntermediateTlsCheck(conf gas.Config) (gas.Rule, []ast.Node) {
func NewCompatTlsCheck(conf gas.Config) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
requiredType: "tls.Config",
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
goodCiphers: []string{
"TLS_RSA_WITH_RC4_128_SHA",
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",

View file

@ -1,169 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestInsecureSkipVerify(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS InsecureSkipVerify set true")
}
func TestInsecureMinVersion(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: 0},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS MinVersion too low")
}
func TestInsecureMaxVersion(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{MaxVersion: 0},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS MaxVersion too low")
}
func TestInsecureCipherSuite(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{CipherSuites: []uint16{
tls.TLS_RSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS Bad Cipher Suite: TLS_RSA_WITH_RC4_128_SHA")
}
func TestPreferServerCipherSuites(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{PreferServerCipherSuites: false},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS PreferServerCipherSuites set false")
}

View file

@ -1,55 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestUnsafe(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewUsingUnsafe(config))
issues := gasTestRunner(`
package main
import (
"fmt"
"unsafe"
)
type Fake struct{}
func (Fake) Good() {}
func main() {
unsafeM := Fake{}
unsafeM.Good()
intArray := [...]int{1, 2}
fmt.Printf("\nintArray: %v\n", intArray)
intPtr := &intArray[0]
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr)
addressHolder := uintptr(unsafe.Pointer(intPtr)) + unsafe.Sizeof(intArray[0])
intPtr = (*int)(unsafe.Pointer(addressHolder))
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr)
}
`, analyzer)
checkTestResults(t, issues, 3, "Use of unsafe calls")
}

View file

@ -1,40 +0,0 @@
// (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 rules
import (
"strings"
"testing"
"github.com/GoASTScanner/gas"
)
func gasTestRunner(source string, analyzer gas.Analyzer) []*gas.Issue {
analyzer.ProcessSource("dummy.go", source)
return analyzer.Issues
}
func checkTestResults(t *testing.T, issues []*gas.Issue, expected int, msg string) {
found := len(issues)
if found != expected {
t.Errorf("Found %d issues, expected %d", found, expected)
}
for _, issue := range issues {
if !strings.Contains(issue.What, msg) {
t.Errorf("Unexpected issue identified: %s", issue.What)
}
}
}

View file

@ -1,114 +0,0 @@
// (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 rules
import (
"testing"
"github.com/GoASTScanner/gas"
)
func TestMD5(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_crypto_md5(config))
analyzer.AddRule(NewUsesWeakCryptography(config))
issues := gasTestRunner(`
package main
import (
"crypto/md5"
"fmt"
"os"
)
func main() {
for _, arg := range os.Args {
fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg)
}
}
`, analyzer)
checkTestResults(t, issues, 2, "weak cryptographic")
}
func TestDES(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_crypto_des(config))
analyzer.AddRule(NewUsesWeakCryptography(config))
issues := gasTestRunner(`
package main
import (
"crypto/cipher"
"crypto/des"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)
func main() {
block, err := des.NewCipher([]byte("sekritz"))
if err != nil {
panic(err)
}
plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ")
ciphertext := make([]byte, des.BlockSize+len(plaintext))
iv := ciphertext[:des.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[des.BlockSize:], plaintext)
fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext))
}
`, analyzer)
checkTestResults(t, issues, 2, "weak cryptographic")
}
func TestRC4(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_crypto_rc4(config))
analyzer.AddRule(NewUsesWeakCryptography(config))
issues := gasTestRunner(`
package main
import (
"crypto/rc4"
"encoding/hex"
"fmt"
)
func main() {
cipher, err := rc4.NewCipher([]byte("sekritz"))
if err != nil {
panic(err)
}
plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ")
ciphertext := make([]byte, len(plaintext))
cipher.XORKeyStream(ciphertext, plaintext)
fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext))
}
`, analyzer)
checkTestResults(t, issues, 2, "weak cryptographic")
}

404
select.go
View file

@ -1,404 +0,0 @@
// (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 gas
import (
"fmt"
"go/ast"
"reflect"
)
// 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.
type SelectFunc func(ast.Node, int) bool
func walkIdentList(list []*ast.Ident, depth int, fun SelectFunc) {
for _, x := range list {
depthWalk(x, depth, fun)
}
}
func walkExprList(list []ast.Expr, depth int, fun SelectFunc) {
for _, x := range list {
depthWalk(x, depth, fun)
}
}
func walkStmtList(list []ast.Stmt, depth int, fun SelectFunc) {
for _, x := range list {
depthWalk(x, depth, fun)
}
}
func walkDeclList(list []ast.Decl, depth int, fun SelectFunc) {
for _, x := range list {
depthWalk(x, depth, fun)
}
}
func depthWalk(node ast.Node, depth int, fun SelectFunc) {
if !fun(node, depth) {
return
}
switch n := node.(type) {
// Comments and fields
case *ast.Comment:
case *ast.CommentGroup:
for _, c := range n.List {
depthWalk(c, depth+1, fun)
}
case *ast.Field:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
walkIdentList(n.Names, depth+1, fun)
depthWalk(n.Type, depth+1, fun)
if n.Tag != nil {
depthWalk(n.Tag, depth+1, fun)
}
if n.Comment != nil {
depthWalk(n.Comment, depth+1, fun)
}
case *ast.FieldList:
for _, f := range n.List {
depthWalk(f, depth+1, fun)
}
// Expressions
case *ast.BadExpr, *ast.Ident, *ast.BasicLit:
case *ast.Ellipsis:
if n.Elt != nil {
depthWalk(n.Elt, depth+1, fun)
}
case *ast.FuncLit:
depthWalk(n.Type, depth+1, fun)
depthWalk(n.Body, depth+1, fun)
case *ast.CompositeLit:
if n.Type != nil {
depthWalk(n.Type, depth+1, fun)
}
walkExprList(n.Elts, depth+1, fun)
case *ast.ParenExpr:
depthWalk(n.X, depth+1, fun)
case *ast.SelectorExpr:
depthWalk(n.X, depth+1, fun)
depthWalk(n.Sel, depth+1, fun)
case *ast.IndexExpr:
depthWalk(n.X, depth+1, fun)
depthWalk(n.Index, depth+1, fun)
case *ast.SliceExpr:
depthWalk(n.X, depth+1, fun)
if n.Low != nil {
depthWalk(n.Low, depth+1, fun)
}
if n.High != nil {
depthWalk(n.High, depth+1, fun)
}
if n.Max != nil {
depthWalk(n.Max, depth+1, fun)
}
case *ast.TypeAssertExpr:
depthWalk(n.X, depth+1, fun)
if n.Type != nil {
depthWalk(n.Type, depth+1, fun)
}
case *ast.CallExpr:
depthWalk(n.Fun, depth+1, fun)
walkExprList(n.Args, depth+1, fun)
case *ast.StarExpr:
depthWalk(n.X, depth+1, fun)
case *ast.UnaryExpr:
depthWalk(n.X, depth+1, fun)
case *ast.BinaryExpr:
depthWalk(n.X, depth+1, fun)
depthWalk(n.Y, depth+1, fun)
case *ast.KeyValueExpr:
depthWalk(n.Key, depth+1, fun)
depthWalk(n.Value, depth+1, fun)
// Types
case *ast.ArrayType:
if n.Len != nil {
depthWalk(n.Len, depth+1, fun)
}
depthWalk(n.Elt, depth+1, fun)
case *ast.StructType:
depthWalk(n.Fields, depth+1, fun)
case *ast.FuncType:
if n.Params != nil {
depthWalk(n.Params, depth+1, fun)
}
if n.Results != nil {
depthWalk(n.Results, depth+1, fun)
}
case *ast.InterfaceType:
depthWalk(n.Methods, depth+1, fun)
case *ast.MapType:
depthWalk(n.Key, depth+1, fun)
depthWalk(n.Value, depth+1, fun)
case *ast.ChanType:
depthWalk(n.Value, depth+1, fun)
// Statements
case *ast.BadStmt:
case *ast.DeclStmt:
depthWalk(n.Decl, depth+1, fun)
case *ast.EmptyStmt:
case *ast.LabeledStmt:
depthWalk(n.Label, depth+1, fun)
depthWalk(n.Stmt, depth+1, fun)
case *ast.ExprStmt:
depthWalk(n.X, depth+1, fun)
case *ast.SendStmt:
depthWalk(n.Chan, depth+1, fun)
depthWalk(n.Value, depth+1, fun)
case *ast.IncDecStmt:
depthWalk(n.X, depth+1, fun)
case *ast.AssignStmt:
walkExprList(n.Lhs, depth+1, fun)
walkExprList(n.Rhs, depth+1, fun)
case *ast.GoStmt:
depthWalk(n.Call, depth+1, fun)
case *ast.DeferStmt:
depthWalk(n.Call, depth+1, fun)
case *ast.ReturnStmt:
walkExprList(n.Results, depth+1, fun)
case *ast.BranchStmt:
if n.Label != nil {
depthWalk(n.Label, depth+1, fun)
}
case *ast.BlockStmt:
walkStmtList(n.List, depth+1, fun)
case *ast.IfStmt:
if n.Init != nil {
depthWalk(n.Init, depth+1, fun)
}
depthWalk(n.Cond, depth+1, fun)
depthWalk(n.Body, depth+1, fun)
if n.Else != nil {
depthWalk(n.Else, depth+1, fun)
}
case *ast.CaseClause:
walkExprList(n.List, depth+1, fun)
walkStmtList(n.Body, depth+1, fun)
case *ast.SwitchStmt:
if n.Init != nil {
depthWalk(n.Init, depth+1, fun)
}
if n.Tag != nil {
depthWalk(n.Tag, depth+1, fun)
}
depthWalk(n.Body, depth+1, fun)
case *ast.TypeSwitchStmt:
if n.Init != nil {
depthWalk(n.Init, depth+1, fun)
}
depthWalk(n.Assign, depth+1, fun)
depthWalk(n.Body, depth+1, fun)
case *ast.CommClause:
if n.Comm != nil {
depthWalk(n.Comm, depth+1, fun)
}
walkStmtList(n.Body, depth+1, fun)
case *ast.SelectStmt:
depthWalk(n.Body, depth+1, fun)
case *ast.ForStmt:
if n.Init != nil {
depthWalk(n.Init, depth+1, fun)
}
if n.Cond != nil {
depthWalk(n.Cond, depth+1, fun)
}
if n.Post != nil {
depthWalk(n.Post, depth+1, fun)
}
depthWalk(n.Body, depth+1, fun)
case *ast.RangeStmt:
if n.Key != nil {
depthWalk(n.Key, depth+1, fun)
}
if n.Value != nil {
depthWalk(n.Value, depth+1, fun)
}
depthWalk(n.X, depth+1, fun)
depthWalk(n.Body, depth+1, fun)
// Declarations
case *ast.ImportSpec:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
if n.Name != nil {
depthWalk(n.Name, depth+1, fun)
}
depthWalk(n.Path, depth+1, fun)
if n.Comment != nil {
depthWalk(n.Comment, depth+1, fun)
}
case *ast.ValueSpec:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
walkIdentList(n.Names, depth+1, fun)
if n.Type != nil {
depthWalk(n.Type, depth+1, fun)
}
walkExprList(n.Values, depth+1, fun)
if n.Comment != nil {
depthWalk(n.Comment, depth+1, fun)
}
case *ast.TypeSpec:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
depthWalk(n.Name, depth+1, fun)
depthWalk(n.Type, depth+1, fun)
if n.Comment != nil {
depthWalk(n.Comment, depth+1, fun)
}
case *ast.BadDecl:
case *ast.GenDecl:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
for _, s := range n.Specs {
depthWalk(s, depth+1, fun)
}
case *ast.FuncDecl:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
if n.Recv != nil {
depthWalk(n.Recv, depth+1, fun)
}
depthWalk(n.Name, depth+1, fun)
depthWalk(n.Type, depth+1, fun)
if n.Body != nil {
depthWalk(n.Body, depth+1, fun)
}
// Files and packages
case *ast.File:
if n.Doc != nil {
depthWalk(n.Doc, depth+1, fun)
}
depthWalk(n.Name, depth+1, fun)
walkDeclList(n.Decls, depth+1, fun)
// don't walk n.Comments - they have been
// visited already through the individual
// nodes
case *ast.Package:
for _, f := range n.Files {
depthWalk(f, depth+1, fun)
}
default:
panic(fmt.Sprintf("gas.depthWalk: unexpected node type %T", n))
}
}
type Selector interface {
Final(ast.Node)
Partial(ast.Node) bool
}
func Select(s Selector, n ast.Node, bits ...reflect.Type) {
fun := func(n ast.Node, d int) bool {
if d < len(bits) && reflect.TypeOf(n) == bits[d] {
if d == len(bits)-1 {
s.Final(n)
return false
} else if s.Partial(n) {
return true
}
}
return false
}
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 {
if found != nil {
return false // short cut logic if we have found a match
}
if d < len(bits) && reflect.TypeOf(n) == bits[d] {
if d == len(bits)-1 {
found = n
return false
}
return true
}
return false
}
depthWalk(n, 0, fun)
return found
}

12
testutils/log.go Normal file
View file

@ -0,0 +1,12 @@
package testutils
import (
"bytes"
"log"
)
// NewLogger returns a logger and the buffer that it will be written to
func NewLogger() (*log.Logger, *bytes.Buffer) {
var buf bytes.Buffer
return log.New(&buf, "", log.Lshortfile), &buf
}

132
testutils/pkg.go Normal file
View file

@ -0,0 +1,132 @@
package testutils
import (
"fmt"
"go/build"
"go/parser"
"io/ioutil"
"log"
"os"
"path"
"strings"
"github.com/GoASTScanner/gas"
"golang.org/x/tools/go/loader"
)
type buildObj struct {
pkg *build.Package
config loader.Config
program *loader.Program
}
type TestPackage struct {
Path string
Files map[string]string
ondisk bool
build *buildObj
}
// NewPackage will create a new and empty package. Must call Close() to cleanup
// auxilary files
func NewTestPackage() *TestPackage {
// Files must exist in $GOPATH
sourceDir := path.Join(os.Getenv("GOPATH"), "src")
workingDir, err := ioutil.TempDir(sourceDir, "gas_test")
if err != nil {
return nil
}
return &TestPackage{
Path: workingDir,
Files: make(map[string]string),
ondisk: false,
build: nil,
}
}
// AddFile inserts the filename and contents into the package contents
func (p *TestPackage) AddFile(filename, content string) {
p.Files[path.Join(p.Path, filename)] = content
}
func (p *TestPackage) write() error {
if p.ondisk {
return nil
}
for filename, content := range p.Files {
if e := ioutil.WriteFile(filename, []byte(content), 0644); e != nil {
return e
}
}
p.ondisk = true
return nil
}
// Build ensures all files are persisted to disk and built
func (p *TestPackage) Build() error {
if p.build != nil {
return nil
}
if err := p.write(); err != nil {
return err
}
basePackage, err := build.Default.ImportDir(p.Path, build.ImportComment)
if err != nil {
return err
}
packageConfig := loader.Config{Build: &build.Default, ParserMode: parser.ParseComments}
packageFiles := make([]string, 0)
for _, filename := range basePackage.GoFiles {
packageFiles = append(packageFiles, path.Join(p.Path, filename))
}
packageConfig.CreateFromFilenames(basePackage.Name, packageFiles...)
program, err := packageConfig.Load()
if err != nil {
return err
}
p.build = &buildObj{
pkg: basePackage,
config: packageConfig,
program: program,
}
return nil
}
// CreateContext builds a context out of supplied package context
func (p *TestPackage) CreateContext(filename string) *gas.Context {
if err := p.Build(); err != nil {
log.Fatal(err)
return nil
}
for _, pkg := range p.build.program.Created {
for _, file := range pkg.Files {
pkgFile := p.build.program.Fset.File(file.Pos()).Name()
strip := fmt.Sprintf("%s%c", p.Path, os.PathSeparator)
pkgFile = strings.TrimPrefix(pkgFile, strip)
if pkgFile == filename {
ctx := &gas.Context{
FileSet: p.build.program.Fset,
Root: file,
Config: gas.NewConfig(),
Info: &pkg.Info,
Pkg: pkg.Pkg,
Imports: gas.NewImportTracker(),
}
ctx.Imports.TrackPackages(ctx.Pkg.Imports()...)
return ctx
}
}
}
return nil
}
// Close will delete the package and all files in that directory
func (p *TestPackage) Close() {
if p.ondisk {
os.RemoveAll(p.Path)
}
}

193
testutils/source.go Normal file
View file

@ -0,0 +1,193 @@
package testutils
// CodeSample encapsulates a snippet of source code that compiles, and how many errors should be detected
type CodeSample struct {
Code string
Errors int
}
var (
// SampleCodeG101 code snippets for hardcoded credentials
SampleCodeG101 = []CodeSample{{`
package main
import "fmt"
func main() {
username := "admin"
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
fmt.Println("Doing something with: ", username, password)
}`, 1}, {`
// Entropy check should not report this error by default
package main
import "fmt"
func main() {
username := "admin"
password := "secret"
fmt.Println("Doing something with: ", username, password)
}`, 0}, {`
package main
import "fmt"
var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
fmt.Println("Doing something with: ", username, password)
}`, 1}, {`
package main
import "fmt"
const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
fmt.Println("Doing something with: ", username, password)
}`, 1}, {`
package main
import "fmt"
const (
username = "user"
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
fmt.Println("Doing something with: ", username, password)
}`, 1}, {`
package main
var password string
func init() {
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
}`, 1}, {`
package main
const (
ATNStateSomethingElse = 1
ATNStateTokenStart = 42
)
func main() {
println(ATNStateTokenStart)
}`, 0}, {`
package main
const (
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
println(ATNStateTokenStart)
}`, 1}}
// SampleCodeG102 code snippets for network binding
SampleCodeG102 = []CodeSample{
// Bind to all networks explicitly
{`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", "0.0.0.0:2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, 1},
// Bind to all networks implicitly (default if host omitted)
{`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", ":2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, 1},
}
// SampleCodeG103 find instances of unsafe blocks for auditing purposes
SampleCodeG103 = []CodeSample{
{`
package main
import (
"fmt"
"unsafe"
)
type Fake struct{}
func (Fake) Good() {}
func main() {
unsafeM := Fake{}
unsafeM.Good()
intArray := [...]int{1, 2}
fmt.Printf("\nintArray: %v\n", intArray)
intPtr := &intArray[0]
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr)
addressHolder := uintptr(unsafe.Pointer(intPtr)) + unsafe.Sizeof(intArray[0])
intPtr = (*int)(unsafe.Pointer(addressHolder))
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr)
}`, 3}}
// SampleCodeG104 finds errors that aren't being handled
SampleCodeG104 = []CodeSample{
{`
package main
import "fmt"
func test() (int,error) {
return 0, nil
}
func main() {
v, _ := test()
fmt.Println(v)
}`, 1}, {`
package main
import (
"io/ioutil"
"os"
"fmt"
)
func a() error {
return fmt.Errorf("This is an error")
}
func b() {
fmt.Println("b")
ioutil.WriteFile("foo.txt", []byte("bar"), os.ModeExclusive)
}
func c() string {
return fmt.Sprintf("This isn't anything")
}
func main() {
_ = a()
a()
b()
c()
}`, 3}, {`
package main
import "fmt"
func test() error {
return nil
}
func main() {
e := test()
fmt.Println(e)
}`, 0}}
// SampleCodeG401 - Use of weak crypto MD5
SampleCodeG401 = []CodeSample{
{`
package main
import (
"crypto/md5"
"fmt"
"io"
"log"
"os"
)
func main() {
f, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err)
}
fmt.Printf("%x", h.Sum(nil))
}`, 1}}
)

28
testutils/visitor.go Normal file
View file

@ -0,0 +1,28 @@
package testutils
import (
"go/ast"
"github.com/GoASTScanner/gas"
)
// MockVisitor is useful for stubbing out ast.Visitor with callback
// and looking for specific conditions to exist.
type MockVisitor struct {
Context *gas.Context
Callback func(n ast.Node, ctx *gas.Context) bool
}
// NewMockVisitor creates a new empty struct, the Context and
// Callback must be set manually. See call_list_test.go for an example.
func NewMockVisitor() *MockVisitor {
return &MockVisitor{}
}
// Visit satisfies the ast.Visitor interface
func (v *MockVisitor) Visit(n ast.Node) ast.Visitor {
if v.Callback(n, v.Context) {
return v
}
return nil
}