From 6943f9e5e4d2c5e7bf5af00d0e9fa90a82a8af3d Mon Sep 17 00:00:00 2001 From: Grant Murphy Date: Wed, 19 Jul 2017 15:17:00 -0600 Subject: [PATCH] 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 --- analyzer.go | 20 +- analyzer_test.go | 113 ++++++++ call_list.go | 12 +- call_list_test.go | 130 +++++---- cmd/gas/filelist_test.go | 251 ----------------- cmd/gas/main.go | 3 + cmd/gas/main_test.go | 45 ---- config.go | 68 ++--- config_test.go | 103 +++++++ gas_suite_test.go | 13 + helpers.go | 51 +--- helpers_test.go | 77 +----- import_tracker.go | 10 +- import_tracker_test.go | 30 +++ issue_test.go | 37 +++ resolve.go | 1 + resolve_test.go | 95 +++++++ rule_test.go | 85 ++++++ rules/big_test.go | 49 ---- rules/bind.go | 23 +- rules/bind_test.go | 65 ----- rules/blacklist_test.go | 40 --- rules/errors.go | 4 +- rules/errors_test.go | 144 ---------- rules/fileperms_test.go | 56 ---- rules/hardcoded_credentials_test.go | 194 ------------- rules/httpoxy_test.go | 39 --- rules/nosec_test.go | 85 ------ rules/rand_test.go | 85 ------ rules/rsa.go | 15 +- rules/rsa_test.go | 50 ---- rules/rules_suite_test.go | 13 + rules/rules_test.go | 67 +++++ rules/sql.go | 16 +- rules/sql_test.go | 216 --------------- rules/subproc.go | 43 +-- rules/subproc_test.go | 124 --------- rules/tempfiles.go | 13 +- rules/tempfiles_test.go | 47 ---- rules/templates.go | 10 +- rules/templates_test.go | 136 ---------- rules/tls.go | 59 ++-- rules/tls_test.go | 169 ------------ rules/unsafe_test.go | 55 ---- rules/utils_test.go | 40 --- rules/weakcrypto_test.go | 114 -------- select.go | 404 ---------------------------- testutils/log.go | 12 + testutils/pkg.go | 132 +++++++++ testutils/source.go | 193 +++++++++++++ testutils/visitor.go | 28 ++ 51 files changed, 1189 insertions(+), 2695 deletions(-) create mode 100644 analyzer_test.go delete mode 100644 cmd/gas/filelist_test.go delete mode 100644 cmd/gas/main_test.go create mode 100644 config_test.go create mode 100644 gas_suite_test.go create mode 100644 import_tracker_test.go create mode 100644 issue_test.go create mode 100644 resolve_test.go create mode 100644 rule_test.go delete mode 100644 rules/big_test.go delete mode 100644 rules/bind_test.go delete mode 100644 rules/blacklist_test.go delete mode 100644 rules/errors_test.go delete mode 100644 rules/fileperms_test.go delete mode 100644 rules/hardcoded_credentials_test.go delete mode 100644 rules/httpoxy_test.go delete mode 100644 rules/nosec_test.go delete mode 100644 rules/rand_test.go delete mode 100644 rules/rsa_test.go create mode 100644 rules/rules_suite_test.go create mode 100644 rules/rules_test.go delete mode 100644 rules/sql_test.go delete mode 100644 rules/subproc_test.go delete mode 100644 rules/tempfiles_test.go delete mode 100644 rules/templates_test.go delete mode 100644 rules/tls_test.go delete mode 100644 rules/unsafe_test.go delete mode 100644 rules/utils_test.go delete mode 100644 rules/weakcrypto_test.go delete mode 100644 select.go create mode 100644 testutils/log.go create mode 100644 testutils/pkg.go create mode 100644 testutils/source.go create mode 100644 testutils/visitor.go diff --git a/analyzer.go b/analyzer.go index 711cc48..c99e4d3 100644 --- a/analyzer.go +++ b/analyzer.go @@ -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{} +} diff --git a/analyzer_test.go b/analyzer_test.go new file mode 100644 index 0000000..2376d43 --- /dev/null +++ b/analyzer_test.go @@ -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)) + + }) +}) diff --git a/call_list.go b/call_list.go index 3de5b04..90dcc40 100644 --- a/call_list.go +++ b/call_list.go @@ -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 } diff --git a/call_list_test.go b/call_list_test.go index d77199f..a2ce2b9 100644 --- a/call_list_test.go +++ b/call_list_test.go @@ -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)) + + }) + +}) diff --git a/cmd/gas/filelist_test.go b/cmd/gas/filelist_test.go deleted file mode 100644 index eaa3cd6..0000000 --- a/cmd/gas/filelist_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/cmd/gas/main.go b/cmd/gas/main.go index 7a80c41..10ddc9a 100644 --- a/cmd/gas/main.go +++ b/cmd/gas/main.go @@ -117,6 +117,9 @@ func loadConfig(configFile string) (gas.Config, error) { return nil, err } } + if *flagIgnoreNoSec { + config.SetGlobal("nosec", "true") + } return config, nil } diff --git a/cmd/gas/main_test.go b/cmd/gas/main_test.go deleted file mode 100644 index b47a4b5..0000000 --- a/cmd/gas/main_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/config.go b/config.go index 2759950..ba9b9cd 100644 --- a/config.go +++ b/config.go @@ -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 + } + } } diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..a1ff0f5 --- /dev/null +++ b/config_test.go @@ -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")) + }) + }) +}) diff --git a/gas_suite_test.go b/gas_suite_test.go new file mode 100644 index 0000000..649d89a --- /dev/null +++ b/gas_suite_test.go @@ -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") +} diff --git a/helpers.go b/helpers.go index 14a0d7d..8bd1f5c 100644 --- a/helpers.go +++ b/helpers.go @@ -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 } diff --git a/helpers_test.go b/helpers_test.go index 18fa63d..3685588 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -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)) + }) + }) +}) diff --git a/import_tracker.go b/import_tracker.go index fe6cb85..02bef01 100644 --- a/import_tracker.go +++ b/import_tracker.go @@ -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 } diff --git a/import_tracker_test.go b/import_tracker_test.go new file mode 100644 index 0000000..a34d478 --- /dev/null +++ b/import_tracker_test.go @@ -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") + }) + }) +}) diff --git a/issue_test.go b/issue_test.go new file mode 100644 index 0000000..534c247 --- /dev/null +++ b/issue_test.go @@ -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") + }) + }) + +}) diff --git a/resolve.go b/resolve.go index 93680b5..d7c6dce 100644 --- a/resolve.go +++ b/resolve.go @@ -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 } diff --git a/resolve_test.go b/resolve_test.go new file mode 100644 index 0000000..7ce2f39 --- /dev/null +++ b/resolve_test.go @@ -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 + + }) + +}) diff --git a/rule_test.go b/rule_test.go new file mode 100644 index 0000000..1eb7c86 --- /dev/null +++ b/rule_test.go @@ -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 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)) + }) + + }) + +}) diff --git a/rules/big_test.go b/rules/big_test.go deleted file mode 100644 index da96ca1..0000000 --- a/rules/big_test.go +++ /dev/null @@ -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") -} diff --git a/rules/bind.go b/rules/bind.go index 0016016..c3f6f63 100644 --- a/rules/bind.go +++ b/rules/bind.go @@ -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, diff --git a/rules/bind_test.go b/rules/bind_test.go deleted file mode 100644 index 69206f0..0000000 --- a/rules/bind_test.go +++ /dev/null @@ -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") -} diff --git a/rules/blacklist_test.go b/rules/blacklist_test.go deleted file mode 100644 index a706ca8..0000000 --- a/rules/blacklist_test.go +++ /dev/null @@ -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, "") -} diff --git a/rules/errors.go b/rules/errors.go index 6ed0c82..147b16d 100644 --- a/rules/errors.go +++ b/rules/errors.go @@ -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 diff --git a/rules/errors_test.go b/rules/errors_test.go deleted file mode 100644 index 2b2fcd4..0000000 --- a/rules/errors_test.go +++ /dev/null @@ -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") -} diff --git a/rules/fileperms_test.go b/rules/fileperms_test.go deleted file mode 100644 index 31cf48a..0000000 --- a/rules/fileperms_test.go +++ /dev/null @@ -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") -} diff --git a/rules/hardcoded_credentials_test.go b/rules/hardcoded_credentials_test.go deleted file mode 100644 index 34380c5..0000000 --- a/rules/hardcoded_credentials_test.go +++ /dev/null @@ -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") -} diff --git a/rules/httpoxy_test.go b/rules/httpoxy_test.go deleted file mode 100644 index 61b4a87..0000000 --- a/rules/httpoxy_test.go +++ /dev/null @@ -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") -} diff --git a/rules/nosec_test.go b/rules/nosec_test.go deleted file mode 100644 index 45b4b3b..0000000 --- a/rules/nosec_test.go +++ /dev/null @@ -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.") -} diff --git a/rules/rand_test.go b/rules/rand_test.go deleted file mode 100644 index 1a166c0..0000000 --- a/rules/rand_test.go +++ /dev/null @@ -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") -} diff --git a/rules/rsa.go b/rules/rsa.go index ee9fec2..8dd467b 100644 --- a/rules/rsa.go +++ b/rules/rsa.go @@ -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, diff --git a/rules/rsa_test.go b/rules/rsa_test.go deleted file mode 100644 index 33aefdd..0000000 --- a/rules/rsa_test.go +++ /dev/null @@ -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") -} diff --git a/rules/rules_suite_test.go b/rules/rules_suite_test.go new file mode 100644 index 0000000..51a204e --- /dev/null +++ b/rules/rules_suite_test.go @@ -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") +} diff --git a/rules/rules_test.go b/rules/rules_test.go new file mode 100644 index 0000000..0bc921b --- /dev/null +++ b/rules/rules_test.go @@ -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) + }) + + }) + +}) diff --git a/rules/sql.go b/rules/sql.go index 0bf7a00..316fe88 100644 --- a/rules/sql.go +++ b/rules/sql.go @@ -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)} } diff --git a/rules/sql_test.go b/rules/sql_test.go deleted file mode 100644 index 4df6b1f..0000000 --- a/rules/sql_test.go +++ /dev/null @@ -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") -} diff --git a/rules/subproc.go b/rules/subproc.go index 3c86620..2453acc 100644 --- a/rules/subproc.go +++ b/rules/subproc.go @@ -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)} } diff --git a/rules/subproc_test.go b/rules/subproc_test.go deleted file mode 100644 index c333f44..0000000 --- a/rules/subproc_test.go +++ /dev/null @@ -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.") -} diff --git a/rules/tempfiles.go b/rules/tempfiles.go index 43c81bf..316c337 100644 --- a/rules/tempfiles.go +++ b/rules/tempfiles.go @@ -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, diff --git a/rules/tempfiles_test.go b/rules/tempfiles_test.go deleted file mode 100644 index 34cf2b9..0000000 --- a/rules/tempfiles_test.go +++ /dev/null @@ -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") -} diff --git a/rules/templates.go b/rules/templates.go index d185768..1f522a8 100644 --- a/rules/templates.go +++ b/rules/templates.go @@ -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, diff --git a/rules/templates_test.go b/rules/templates_test.go deleted file mode 100644 index e794625..0000000 --- a/rules/templates_test.go +++ /dev/null @@ -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 World", - "Body": template.HTML(""), - } - 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 World", - "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 World", - "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 World", - "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") -} diff --git a/rules/tls.go b/rules/tls.go index 7448ef5..8402a8a 100644 --- a/rules/tls.go +++ b/rules/tls.go @@ -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", diff --git a/rules/tls_test.go b/rules/tls_test.go deleted file mode 100644 index c5d7124..0000000 --- a/rules/tls_test.go +++ /dev/null @@ -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") -} diff --git a/rules/unsafe_test.go b/rules/unsafe_test.go deleted file mode 100644 index 920024c..0000000 --- a/rules/unsafe_test.go +++ /dev/null @@ -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") - -} diff --git a/rules/utils_test.go b/rules/utils_test.go deleted file mode 100644 index c8062cc..0000000 --- a/rules/utils_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/rules/weakcrypto_test.go b/rules/weakcrypto_test.go deleted file mode 100644 index 59d5a5f..0000000 --- a/rules/weakcrypto_test.go +++ /dev/null @@ -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") -} diff --git a/select.go b/select.go deleted file mode 100644 index b4b928c..0000000 --- a/select.go +++ /dev/null @@ -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 -} diff --git a/testutils/log.go b/testutils/log.go new file mode 100644 index 0000000..460cb71 --- /dev/null +++ b/testutils/log.go @@ -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 +} diff --git a/testutils/pkg.go b/testutils/pkg.go new file mode 100644 index 0000000..2421e9f --- /dev/null +++ b/testutils/pkg.go @@ -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) + } +} diff --git a/testutils/source.go b/testutils/source.go new file mode 100644 index 0000000..91a0837 --- /dev/null +++ b/testutils/source.go @@ -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}} +) diff --git a/testutils/visitor.go b/testutils/visitor.go new file mode 100644 index 0000000..df9275b --- /dev/null +++ b/testutils/visitor.go @@ -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 +}