mirror of
https://github.com/securego/gosec.git
synced 2024-11-05 11:35:51 +00:00
Major rework of codebase
- Get rid of 'core' and move CLI to cmd/gas directory - Migrate (most) tests to use Ginkgo and testutils framework - GAS now expects package to reside in $GOPATH - GAS now can resolve dependencies for better type checking (if package on GOPATH) - Simplified public API
This commit is contained in:
parent
f4b705a864
commit
6943f9e5e4
51 changed files with 1189 additions and 2695 deletions
20
analyzer.go
20
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{}
|
||||
}
|
||||
|
|
113
analyzer_test.go
Normal file
113
analyzer_test.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
"github.com/GoASTScanner/gas/rules"
|
||||
|
||||
"github.com/GoASTScanner/gas/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Analyzer", func() {
|
||||
|
||||
var (
|
||||
analyzer *gas.Analyzer
|
||||
logger *log.Logger
|
||||
output *bytes.Buffer
|
||||
)
|
||||
BeforeEach(func() {
|
||||
logger, output = testutils.NewLogger()
|
||||
analyzer = gas.NewAnalyzer(nil, logger)
|
||||
})
|
||||
|
||||
Context("when processing a package", func() {
|
||||
|
||||
It("should return an error if the package contains no Go files", func() {
|
||||
analyzer.LoadRules(rules.Generate().Builders()...)
|
||||
dir, err := ioutil.TempDir("", "empty")
|
||||
defer os.RemoveAll(dir)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
err = analyzer.Process(dir)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(err.Error()).Should(MatchRegexp("no buildable Go source files"))
|
||||
})
|
||||
|
||||
It("should return an error if the package fails to build", func() {
|
||||
analyzer.LoadRules(rules.Generate().Builders()...)
|
||||
pkg := testutils.NewTestPackage()
|
||||
defer pkg.Close()
|
||||
pkg.AddFile("wonky.go", `func main(){ println("forgot the package")}`)
|
||||
pkg.Build()
|
||||
|
||||
err := analyzer.Process(pkg.Path)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(err.Error()).Should(MatchRegexp(`expected 'package'`))
|
||||
|
||||
})
|
||||
|
||||
It("should find errors when nosec is not in use", func() {
|
||||
|
||||
// Rule for MD5 weak crypto usage
|
||||
sample := testutils.SampleCodeG401[0]
|
||||
source := sample.Code
|
||||
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
|
||||
|
||||
controlPackage := testutils.NewTestPackage()
|
||||
defer controlPackage.Close()
|
||||
controlPackage.AddFile("md5.go", source)
|
||||
controlPackage.Build()
|
||||
analyzer.Process(controlPackage.Path)
|
||||
controlIssues, _ := analyzer.Report()
|
||||
Expect(controlIssues).Should(HaveLen(sample.Errors))
|
||||
|
||||
})
|
||||
|
||||
It("should not report errors when a nosec comment is present", func() {
|
||||
// Rule for MD5 weak crypto usage
|
||||
sample := testutils.SampleCodeG401[0]
|
||||
source := sample.Code
|
||||
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
|
||||
|
||||
nosecPackage := testutils.NewTestPackage()
|
||||
defer nosecPackage.Close()
|
||||
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
|
||||
nosecPackage.AddFile("md5.go", nosecSource)
|
||||
nosecPackage.Build()
|
||||
|
||||
analyzer.Process(nosecPackage.Path)
|
||||
nosecIssues, _ := analyzer.Report()
|
||||
Expect(nosecIssues).Should(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
It("should be possible to overwrite nosec comments, and report issues", func() {
|
||||
|
||||
// Rule for MD5 weak crypto usage
|
||||
sample := testutils.SampleCodeG401[0]
|
||||
source := sample.Code
|
||||
|
||||
// overwrite nosec option
|
||||
nosecIgnoreConfig := gas.NewConfig()
|
||||
nosecIgnoreConfig.SetGlobal("nosec", "true")
|
||||
customAnalyzer := gas.NewAnalyzer(nosecIgnoreConfig, logger)
|
||||
customAnalyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
|
||||
|
||||
nosecPackage := testutils.NewTestPackage()
|
||||
defer nosecPackage.Close()
|
||||
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
|
||||
nosecPackage.AddFile("md5.go", nosecSource)
|
||||
nosecPackage.Build()
|
||||
|
||||
customAnalyzer.Process(nosecPackage.Path)
|
||||
nosecIssues, _ := customAnalyzer.Report()
|
||||
Expect(nosecIssues).Should(HaveLen(sample.Errors))
|
||||
|
||||
})
|
||||
})
|
12
call_list.go
12
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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -117,6 +117,9 @@ func loadConfig(configFile string) (gas.Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
if *flagIgnoreNoSec {
|
||||
config.SetGlobal("nosec", "true")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
68
config.go
68
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
103
config_test.go
Normal file
103
config_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Configuration", func() {
|
||||
var configuration gas.Config
|
||||
BeforeEach(func() {
|
||||
configuration = gas.NewConfig()
|
||||
})
|
||||
|
||||
Context("when loading from disk", func() {
|
||||
|
||||
It("should be possible to load configuration from a file", func() {
|
||||
json := `{"G101": {}}`
|
||||
buffer := bytes.NewBufferString(json)
|
||||
nread, err := configuration.ReadFrom(buffer)
|
||||
Expect(nread).Should(Equal(int64(len(json))))
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should return an error if configuration file is invalid", func() {
|
||||
var err error
|
||||
invalidBuffer := bytes.NewBuffer([]byte{0xc0, 0xff, 0xee})
|
||||
_, err = configuration.ReadFrom(invalidBuffer)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
|
||||
emptyBuffer := bytes.NewBuffer([]byte{})
|
||||
_, err = configuration.ReadFrom(emptyBuffer)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Context("when saving to disk", func() {
|
||||
It("should be possible to save an empty configuration to file", func() {
|
||||
expected := `{"global":{}}`
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
nbytes, err := configuration.WriteTo(buffer)
|
||||
Expect(int(nbytes)).Should(Equal(len(expected)))
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(buffer.String()).Should(Equal(expected))
|
||||
})
|
||||
|
||||
It("should be possible to save configuration to file", func() {
|
||||
|
||||
configuration.Set("G101", map[string]string{
|
||||
"mode": "strict",
|
||||
})
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
nbytes, err := configuration.WriteTo(buffer)
|
||||
Expect(int(nbytes)).ShouldNot(BeZero())
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(buffer.String()).Should(Equal(`{"G101":{"mode":"strict"},"global":{}}`))
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
Context("when configuring rules", func() {
|
||||
|
||||
It("should be possible to get configuration for a rule", func() {
|
||||
settings := map[string]string{
|
||||
"ciphers": "AES256-GCM",
|
||||
}
|
||||
configuration.Set("G101", settings)
|
||||
|
||||
retrieved, err := configuration.Get("G101")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(retrieved).Should(HaveKeyWithValue("ciphers", "AES256-GCM"))
|
||||
Expect(retrieved).ShouldNot(HaveKey("foobar"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when using global configuration options", func() {
|
||||
It("should have a default global section", func() {
|
||||
settings, err := configuration.Get("global")
|
||||
Expect(err).Should(BeNil())
|
||||
expectedType := make(map[string]string)
|
||||
Expect(settings).Should(BeAssignableToTypeOf(expectedType))
|
||||
})
|
||||
|
||||
It("should save global settings to correct section", func() {
|
||||
configuration.SetGlobal("nosec", "enabled")
|
||||
settings, err := configuration.Get("global")
|
||||
Expect(err).Should(BeNil())
|
||||
if globals, ok := settings.(map[string]string); ok {
|
||||
Expect(globals["nosec"]).Should(MatchRegexp("enabled"))
|
||||
} else {
|
||||
Fail("globals are not defined as map")
|
||||
}
|
||||
|
||||
setValue, err := configuration.GetGlobal("nosec")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(setValue).Should(MatchRegexp("enabled"))
|
||||
})
|
||||
})
|
||||
})
|
13
gas_suite_test.go
Normal file
13
gas_suite_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGas(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Gas Suite")
|
||||
}
|
51
helpers.go
51
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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -34,9 +34,11 @@ func NewImportTracker() *ImportTracker {
|
|||
|
||||
func (t *ImportTracker) TrackPackages(pkgs ...*types.Package) {
|
||||
for _, pkg := range pkgs {
|
||||
for _, imp := range pkg.Imports() {
|
||||
t.Imported[imp.Path()] = imp.Name()
|
||||
}
|
||||
t.Imported[pkg.Path()] = pkg.Name()
|
||||
// Transient imports
|
||||
//for _, imp := range pkg.Imports() {
|
||||
// t.Imported[imp.Path()] = imp.Name()
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,8 +54,6 @@ func (t *ImportTracker) TrackImport(n ast.Node) {
|
|||
t.Aliased[path] = imported.Name.Name
|
||||
}
|
||||
}
|
||||
|
||||
// unsafe is not included in Package.Imports()
|
||||
if path == "unsafe" {
|
||||
t.Imported[path] = path
|
||||
}
|
||||
|
|
30
import_tracker_test.go
Normal file
30
import_tracker_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ImportTracker", func() {
|
||||
var (
|
||||
source string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
source = `// TODO(gm)`
|
||||
})
|
||||
Context("when I have a valid go package", func() {
|
||||
It("should record all import specs", func() {
|
||||
Expect(1).Should(Equal(1))
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should correctly track aliased package imports", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should correctly track init only packages", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
})
|
||||
})
|
37
issue_test.go
Normal file
37
issue_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Issue", func() {
|
||||
|
||||
Context("when creating a new issue", func() {
|
||||
It("should provide a code snippet for the specified ast.Node", func() {
|
||||
Expect(1).Should(Equal(2))
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should return an error if specific context is not able to be obtained", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should provide accurate line and file information", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should maintain the provided severity score", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should maintain the provided confidence score", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
|
||||
It("should correctly record `unsafe` import as not considered a package", func() {
|
||||
Fail("Not implemented")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -17,6 +17,7 @@ package gas
|
|||
import "go/ast"
|
||||
|
||||
func resolveIdent(n *ast.Ident, c *Context) bool {
|
||||
|
||||
if n.Obj == nil || n.Obj.Kind != ast.Var {
|
||||
return true
|
||||
}
|
||||
|
|
95
resolve_test.go
Normal file
95
resolve_test.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
"github.com/GoASTScanner/gas/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Resolve ast node to concrete value", func() {
|
||||
Context("when attempting to resolve an ast node", func() {
|
||||
It("should successfully resolve basic literal", func() {
|
||||
var basicLiteral *ast.BasicLit
|
||||
|
||||
pkg := testutils.NewTestPackage()
|
||||
pkg.AddFile("foo.go", `package main; const foo = "bar"; func main(){}`)
|
||||
ctx := pkg.CreateContext("foo.go")
|
||||
v := testutils.NewMockVisitor()
|
||||
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
|
||||
if node, ok := n.(*ast.BasicLit); ok {
|
||||
basicLiteral = node
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
v.Context = ctx
|
||||
ast.Walk(v, ctx.Root)
|
||||
Expect(basicLiteral).ShouldNot(BeNil())
|
||||
Expect(gas.TryResolve(basicLiteral, ctx)).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("should successfully resolve identifier", func() {
|
||||
var ident *ast.Ident
|
||||
pkg := testutils.NewTestPackage()
|
||||
pkg.AddFile("foo.go", `package main; var foo string = "bar"; func main(){}`)
|
||||
ctx := pkg.CreateContext("foo.go")
|
||||
v := testutils.NewMockVisitor()
|
||||
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
|
||||
if node, ok := n.(*ast.Ident); ok {
|
||||
ident = node
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
v.Context = ctx
|
||||
ast.Walk(v, ctx.Root)
|
||||
Expect(ident).ShouldNot(BeNil())
|
||||
Expect(gas.TryResolve(ident, ctx)).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("should successfully resolve assign statement", func() {
|
||||
var assign *ast.AssignStmt
|
||||
pkg := testutils.NewTestPackage()
|
||||
pkg.AddFile("foo.go", `package main; const x = "bar"; func main(){ y := x; println(y) }`)
|
||||
ctx := pkg.CreateContext("foo.go")
|
||||
v := testutils.NewMockVisitor()
|
||||
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
|
||||
if node, ok := n.(*ast.AssignStmt); ok {
|
||||
if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "y" {
|
||||
assign = node
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
v.Context = ctx
|
||||
ast.Walk(v, ctx.Root)
|
||||
Expect(assign).ShouldNot(BeNil())
|
||||
Expect(gas.TryResolve(assign, ctx)).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("should successfully resolve a binary statement", func() {
|
||||
var target *ast.BinaryExpr
|
||||
pkg := testutils.NewTestPackage()
|
||||
pkg.AddFile("foo.go", `package main; const (x = "bar"; y = "baz"); func main(){ z := x + y; println(z) }`)
|
||||
ctx := pkg.CreateContext("foo.go")
|
||||
v := testutils.NewMockVisitor()
|
||||
v.Callback = func(n ast.Node, ctx *gas.Context) bool {
|
||||
if node, ok := n.(*ast.BinaryExpr); ok {
|
||||
target = node
|
||||
}
|
||||
return true
|
||||
}
|
||||
v.Context = ctx
|
||||
ast.Walk(v, ctx.Root)
|
||||
Expect(target).ShouldNot(BeNil())
|
||||
Expect(gas.TryResolve(target, ctx)).Should(BeTrue())
|
||||
})
|
||||
|
||||
// TODO: It should resolve call expressions
|
||||
|
||||
})
|
||||
|
||||
})
|
85
rule_test.go
Normal file
85
rule_test.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package gas_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type mockrule struct {
|
||||
issue *gas.Issue
|
||||
err error
|
||||
callback func(n ast.Node, ctx *gas.Context) bool
|
||||
}
|
||||
|
||||
func (m *mockrule) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
|
||||
if m.callback(n, ctx) {
|
||||
return m.issue, nil
|
||||
}
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
var _ = Describe("Rule", func() {
|
||||
|
||||
Context("when using a ruleset", func() {
|
||||
|
||||
var (
|
||||
ruleset gas.RuleSet
|
||||
dummyErrorRule gas.Rule
|
||||
dummyIssueRule gas.Rule
|
||||
)
|
||||
|
||||
JustBeforeEach(func() {
|
||||
ruleset = gas.NewRuleSet()
|
||||
dummyErrorRule = &mockrule{
|
||||
issue: nil,
|
||||
err: fmt.Errorf("An unexpected error occurred"),
|
||||
callback: func(n ast.Node, ctx *gas.Context) bool { return false },
|
||||
}
|
||||
dummyIssueRule = &mockrule{
|
||||
issue: &gas.Issue{
|
||||
Severity: gas.High,
|
||||
Confidence: gas.High,
|
||||
What: `Some explanation of the thing`,
|
||||
File: "main.go",
|
||||
Code: `#include <stdio.h> int main(){ puts("hello world"); }`,
|
||||
Line: 42,
|
||||
},
|
||||
err: nil,
|
||||
callback: func(n ast.Node, ctx *gas.Context) bool { return true },
|
||||
}
|
||||
})
|
||||
It("should be possible to register a rule for multiple ast.Node", func() {
|
||||
registeredNodeA := (*ast.CallExpr)(nil)
|
||||
registeredNodeB := (*ast.AssignStmt)(nil)
|
||||
unregisteredNode := (*ast.BinaryExpr)(nil)
|
||||
|
||||
ruleset.Register(dummyIssueRule, registeredNodeA, registeredNodeB)
|
||||
Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty())
|
||||
Expect(ruleset.RegisteredFor(registeredNodeA)).Should(ContainElement(dummyIssueRule))
|
||||
Expect(ruleset.RegisteredFor(registeredNodeB)).Should(ContainElement(dummyIssueRule))
|
||||
|
||||
})
|
||||
|
||||
It("should not register a rule when no ast.Nodes are specified", func() {
|
||||
ruleset.Register(dummyErrorRule)
|
||||
Expect(ruleset).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("should be possible to retrieve a list of rules for a given node type", func() {
|
||||
registeredNode := (*ast.CallExpr)(nil)
|
||||
unregisteredNode := (*ast.AssignStmt)(nil)
|
||||
ruleset.Register(dummyErrorRule, registeredNode)
|
||||
ruleset.Register(dummyIssueRule, registeredNode)
|
||||
Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty())
|
||||
Expect(ruleset.RegisteredFor(registeredNode)).Should(HaveLen(2))
|
||||
Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyErrorRule))
|
||||
Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyIssueRule))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -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")
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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, "")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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.")
|
||||
}
|
|
@ -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")
|
||||
}
|
15
rules/rsa.go
15
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,
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
)
|
||||
|
||||
func TestRSAKeys(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewWeakKeyStrength(config))
|
||||
|
||||
issues := gasTestRunner(
|
||||
`package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
//Generate Private Key
|
||||
pvk, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Println(pvk)
|
||||
|
||||
}`, analyzer)
|
||||
|
||||
checkTestResults(t, issues, 1, "RSA keys should")
|
||||
}
|
13
rules/rules_suite_test.go
Normal file
13
rules/rules_suite_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package rules_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRules(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Rules Suite")
|
||||
}
|
67
rules/rules_test.go
Normal file
67
rules/rules_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package rules_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
|
||||
"github.com/GoASTScanner/gas/rules"
|
||||
"github.com/GoASTScanner/gas/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("gas rules", func() {
|
||||
|
||||
var (
|
||||
logger *log.Logger
|
||||
output *bytes.Buffer
|
||||
config gas.Config
|
||||
analyzer *gas.Analyzer
|
||||
runner func(string, []testutils.CodeSample)
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
logger, output = testutils.NewLogger()
|
||||
config = gas.NewConfig()
|
||||
analyzer = gas.NewAnalyzer(config, logger)
|
||||
runner = func(rule string, samples []testutils.CodeSample) {
|
||||
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, rule)).Builders()...)
|
||||
for n, sample := range samples {
|
||||
analyzer.Reset()
|
||||
pkg := testutils.NewTestPackage()
|
||||
pkg.AddFile(fmt.Sprintf("sample_%d.go", n), sample.Code)
|
||||
pkg.Build()
|
||||
e := analyzer.Process(pkg.Path)
|
||||
Expect(e).ShouldNot(HaveOccurred())
|
||||
issues, _ := analyzer.Report()
|
||||
if len(issues) != sample.Errors {
|
||||
fmt.Println(sample.Code)
|
||||
}
|
||||
Expect(issues).Should(HaveLen(sample.Errors))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Context("report correct errors for all samples", func() {
|
||||
It("should work for G101 samples", func() {
|
||||
runner("G101", testutils.SampleCodeG101)
|
||||
})
|
||||
|
||||
It("should work for G102 samples", func() {
|
||||
runner("G102", testutils.SampleCodeG102)
|
||||
})
|
||||
|
||||
It("should work for G103 samples", func() {
|
||||
runner("G103", testutils.SampleCodeG103)
|
||||
})
|
||||
|
||||
It("should work for G104 samples", func() {
|
||||
runner("G104", testutils.SampleCodeG104)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
16
rules/sql.go
16
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)}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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)}
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
)
|
||||
|
||||
func TestTemplateCheckSafe(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewTemplateCheck(config))
|
||||
|
||||
source := `
|
||||
package samples
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const tmpl = ""
|
||||
|
||||
func main() {
|
||||
t := template.Must(template.New("ex").Parse(tmpl))
|
||||
v := map[string]interface{}{
|
||||
"Title": "Test <b>World</b>",
|
||||
"Body": template.HTML("<script>alert(1)</script>"),
|
||||
}
|
||||
t.Execute(os.Stdout, v)
|
||||
}`
|
||||
|
||||
issues := gasTestRunner(source, analyzer)
|
||||
checkTestResults(t, issues, 0, "this method will not auto-escape HTML. Verify data is well formed")
|
||||
}
|
||||
|
||||
func TestTemplateCheckBadHTML(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewTemplateCheck(config))
|
||||
|
||||
source := `
|
||||
package samples
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const tmpl = ""
|
||||
|
||||
func main() {
|
||||
a := "something from another place"
|
||||
t := template.Must(template.New("ex").Parse(tmpl))
|
||||
v := map[string]interface{}{
|
||||
"Title": "Test <b>World</b>",
|
||||
"Body": template.HTML(a),
|
||||
}
|
||||
t.Execute(os.Stdout, v)
|
||||
}`
|
||||
|
||||
issues := gasTestRunner(source, analyzer)
|
||||
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
|
||||
}
|
||||
|
||||
func TestTemplateCheckBadJS(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewTemplateCheck(config))
|
||||
|
||||
source := `
|
||||
package samples
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const tmpl = ""
|
||||
|
||||
func main() {
|
||||
a := "something from another place"
|
||||
t := template.Must(template.New("ex").Parse(tmpl))
|
||||
v := map[string]interface{}{
|
||||
"Title": "Test <b>World</b>",
|
||||
"Body": template.JS(a),
|
||||
}
|
||||
t.Execute(os.Stdout, v)
|
||||
}`
|
||||
|
||||
issues := gasTestRunner(source, analyzer)
|
||||
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
|
||||
}
|
||||
|
||||
func TestTemplateCheckBadURL(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewTemplateCheck(config))
|
||||
|
||||
source := `
|
||||
package samples
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const tmpl = ""
|
||||
|
||||
func main() {
|
||||
a := "something from another place"
|
||||
t := template.Must(template.New("ex").Parse(tmpl))
|
||||
v := map[string]interface{}{
|
||||
"Title": "Test <b>World</b>",
|
||||
"Body": template.URL(a),
|
||||
}
|
||||
t.Execute(os.Stdout, v)
|
||||
}`
|
||||
|
||||
issues := gasTestRunner(source, analyzer)
|
||||
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
|
||||
}
|
59
rules/tls.go
59
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",
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
)
|
||||
|
||||
func TestMD5(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewBlacklist_crypto_md5(config))
|
||||
analyzer.AddRule(NewUsesWeakCryptography(config))
|
||||
|
||||
issues := gasTestRunner(`
|
||||
package main
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
func main() {
|
||||
for _, arg := range os.Args {
|
||||
fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg)
|
||||
}
|
||||
}
|
||||
`, analyzer)
|
||||
checkTestResults(t, issues, 2, "weak cryptographic")
|
||||
}
|
||||
|
||||
func TestDES(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewBlacklist_crypto_des(config))
|
||||
analyzer.AddRule(NewUsesWeakCryptography(config))
|
||||
|
||||
issues := gasTestRunner(`
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func main() {
|
||||
block, err := des.NewCipher([]byte("sekritz"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ")
|
||||
ciphertext := make([]byte, des.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:des.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[des.BlockSize:], plaintext)
|
||||
fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext))
|
||||
}
|
||||
`, analyzer)
|
||||
|
||||
checkTestResults(t, issues, 2, "weak cryptographic")
|
||||
}
|
||||
|
||||
func TestRC4(t *testing.T) {
|
||||
config := map[string]interface{}{"ignoreNosec": false}
|
||||
analyzer := gas.NewAnalyzer(config, nil)
|
||||
analyzer.AddRule(NewBlacklist_crypto_rc4(config))
|
||||
analyzer.AddRule(NewUsesWeakCryptography(config))
|
||||
|
||||
issues := gasTestRunner(`
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rc4"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cipher, err := rc4.NewCipher([]byte("sekritz"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ")
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
cipher.XORKeyStream(ciphertext, plaintext)
|
||||
fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext))
|
||||
}
|
||||
`, analyzer)
|
||||
|
||||
checkTestResults(t, issues, 2, "weak cryptographic")
|
||||
}
|
404
select.go
404
select.go
|
@ -1,404 +0,0 @@
|
|||
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// SelectFunc is like an AST visitor, but has a richer interface. It
|
||||
// is called with the current ast.Node being visitied and that nodes depth in
|
||||
// the tree. The function can return true to continue traversing the tree, or
|
||||
// false to end traversal here.
|
||||
type SelectFunc func(ast.Node, int) bool
|
||||
|
||||
func walkIdentList(list []*ast.Ident, depth int, fun SelectFunc) {
|
||||
for _, x := range list {
|
||||
depthWalk(x, depth, fun)
|
||||
}
|
||||
}
|
||||
|
||||
func walkExprList(list []ast.Expr, depth int, fun SelectFunc) {
|
||||
for _, x := range list {
|
||||
depthWalk(x, depth, fun)
|
||||
}
|
||||
}
|
||||
|
||||
func walkStmtList(list []ast.Stmt, depth int, fun SelectFunc) {
|
||||
for _, x := range list {
|
||||
depthWalk(x, depth, fun)
|
||||
}
|
||||
}
|
||||
|
||||
func walkDeclList(list []ast.Decl, depth int, fun SelectFunc) {
|
||||
for _, x := range list {
|
||||
depthWalk(x, depth, fun)
|
||||
}
|
||||
}
|
||||
|
||||
func depthWalk(node ast.Node, depth int, fun SelectFunc) {
|
||||
if !fun(node, depth) {
|
||||
return
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
// Comments and fields
|
||||
case *ast.Comment:
|
||||
|
||||
case *ast.CommentGroup:
|
||||
for _, c := range n.List {
|
||||
depthWalk(c, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.Field:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
walkIdentList(n.Names, depth+1, fun)
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
if n.Tag != nil {
|
||||
depthWalk(n.Tag, depth+1, fun)
|
||||
}
|
||||
if n.Comment != nil {
|
||||
depthWalk(n.Comment, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.FieldList:
|
||||
for _, f := range n.List {
|
||||
depthWalk(f, depth+1, fun)
|
||||
}
|
||||
|
||||
// Expressions
|
||||
case *ast.BadExpr, *ast.Ident, *ast.BasicLit:
|
||||
|
||||
case *ast.Ellipsis:
|
||||
if n.Elt != nil {
|
||||
depthWalk(n.Elt, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.FuncLit:
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.CompositeLit:
|
||||
if n.Type != nil {
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
}
|
||||
walkExprList(n.Elts, depth+1, fun)
|
||||
|
||||
case *ast.ParenExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
|
||||
case *ast.SelectorExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
depthWalk(n.Sel, depth+1, fun)
|
||||
|
||||
case *ast.IndexExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
depthWalk(n.Index, depth+1, fun)
|
||||
|
||||
case *ast.SliceExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
if n.Low != nil {
|
||||
depthWalk(n.Low, depth+1, fun)
|
||||
}
|
||||
if n.High != nil {
|
||||
depthWalk(n.High, depth+1, fun)
|
||||
}
|
||||
if n.Max != nil {
|
||||
depthWalk(n.Max, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.TypeAssertExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
if n.Type != nil {
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.CallExpr:
|
||||
depthWalk(n.Fun, depth+1, fun)
|
||||
walkExprList(n.Args, depth+1, fun)
|
||||
|
||||
case *ast.StarExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
|
||||
case *ast.UnaryExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
|
||||
case *ast.BinaryExpr:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
depthWalk(n.Y, depth+1, fun)
|
||||
|
||||
case *ast.KeyValueExpr:
|
||||
depthWalk(n.Key, depth+1, fun)
|
||||
depthWalk(n.Value, depth+1, fun)
|
||||
|
||||
// Types
|
||||
case *ast.ArrayType:
|
||||
if n.Len != nil {
|
||||
depthWalk(n.Len, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Elt, depth+1, fun)
|
||||
|
||||
case *ast.StructType:
|
||||
depthWalk(n.Fields, depth+1, fun)
|
||||
|
||||
case *ast.FuncType:
|
||||
if n.Params != nil {
|
||||
depthWalk(n.Params, depth+1, fun)
|
||||
}
|
||||
if n.Results != nil {
|
||||
depthWalk(n.Results, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.InterfaceType:
|
||||
depthWalk(n.Methods, depth+1, fun)
|
||||
|
||||
case *ast.MapType:
|
||||
depthWalk(n.Key, depth+1, fun)
|
||||
depthWalk(n.Value, depth+1, fun)
|
||||
|
||||
case *ast.ChanType:
|
||||
depthWalk(n.Value, depth+1, fun)
|
||||
|
||||
// Statements
|
||||
case *ast.BadStmt:
|
||||
|
||||
case *ast.DeclStmt:
|
||||
depthWalk(n.Decl, depth+1, fun)
|
||||
|
||||
case *ast.EmptyStmt:
|
||||
|
||||
case *ast.LabeledStmt:
|
||||
depthWalk(n.Label, depth+1, fun)
|
||||
depthWalk(n.Stmt, depth+1, fun)
|
||||
|
||||
case *ast.ExprStmt:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
|
||||
case *ast.SendStmt:
|
||||
depthWalk(n.Chan, depth+1, fun)
|
||||
depthWalk(n.Value, depth+1, fun)
|
||||
|
||||
case *ast.IncDecStmt:
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
|
||||
case *ast.AssignStmt:
|
||||
walkExprList(n.Lhs, depth+1, fun)
|
||||
walkExprList(n.Rhs, depth+1, fun)
|
||||
|
||||
case *ast.GoStmt:
|
||||
depthWalk(n.Call, depth+1, fun)
|
||||
|
||||
case *ast.DeferStmt:
|
||||
depthWalk(n.Call, depth+1, fun)
|
||||
|
||||
case *ast.ReturnStmt:
|
||||
walkExprList(n.Results, depth+1, fun)
|
||||
|
||||
case *ast.BranchStmt:
|
||||
if n.Label != nil {
|
||||
depthWalk(n.Label, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.BlockStmt:
|
||||
walkStmtList(n.List, depth+1, fun)
|
||||
|
||||
case *ast.IfStmt:
|
||||
if n.Init != nil {
|
||||
depthWalk(n.Init, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Cond, depth+1, fun)
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
if n.Else != nil {
|
||||
depthWalk(n.Else, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.CaseClause:
|
||||
walkExprList(n.List, depth+1, fun)
|
||||
walkStmtList(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.SwitchStmt:
|
||||
if n.Init != nil {
|
||||
depthWalk(n.Init, depth+1, fun)
|
||||
}
|
||||
if n.Tag != nil {
|
||||
depthWalk(n.Tag, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.TypeSwitchStmt:
|
||||
if n.Init != nil {
|
||||
depthWalk(n.Init, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Assign, depth+1, fun)
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.CommClause:
|
||||
if n.Comm != nil {
|
||||
depthWalk(n.Comm, depth+1, fun)
|
||||
}
|
||||
walkStmtList(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.SelectStmt:
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.ForStmt:
|
||||
if n.Init != nil {
|
||||
depthWalk(n.Init, depth+1, fun)
|
||||
}
|
||||
if n.Cond != nil {
|
||||
depthWalk(n.Cond, depth+1, fun)
|
||||
}
|
||||
if n.Post != nil {
|
||||
depthWalk(n.Post, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
case *ast.RangeStmt:
|
||||
if n.Key != nil {
|
||||
depthWalk(n.Key, depth+1, fun)
|
||||
}
|
||||
if n.Value != nil {
|
||||
depthWalk(n.Value, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.X, depth+1, fun)
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
|
||||
// Declarations
|
||||
case *ast.ImportSpec:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
if n.Name != nil {
|
||||
depthWalk(n.Name, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Path, depth+1, fun)
|
||||
if n.Comment != nil {
|
||||
depthWalk(n.Comment, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.ValueSpec:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
walkIdentList(n.Names, depth+1, fun)
|
||||
if n.Type != nil {
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
}
|
||||
walkExprList(n.Values, depth+1, fun)
|
||||
if n.Comment != nil {
|
||||
depthWalk(n.Comment, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.TypeSpec:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Name, depth+1, fun)
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
if n.Comment != nil {
|
||||
depthWalk(n.Comment, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.BadDecl:
|
||||
|
||||
case *ast.GenDecl:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
for _, s := range n.Specs {
|
||||
depthWalk(s, depth+1, fun)
|
||||
}
|
||||
|
||||
case *ast.FuncDecl:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
if n.Recv != nil {
|
||||
depthWalk(n.Recv, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Name, depth+1, fun)
|
||||
depthWalk(n.Type, depth+1, fun)
|
||||
if n.Body != nil {
|
||||
depthWalk(n.Body, depth+1, fun)
|
||||
}
|
||||
|
||||
// Files and packages
|
||||
case *ast.File:
|
||||
if n.Doc != nil {
|
||||
depthWalk(n.Doc, depth+1, fun)
|
||||
}
|
||||
depthWalk(n.Name, depth+1, fun)
|
||||
walkDeclList(n.Decls, depth+1, fun)
|
||||
// don't walk n.Comments - they have been
|
||||
// visited already through the individual
|
||||
// nodes
|
||||
|
||||
case *ast.Package:
|
||||
for _, f := range n.Files {
|
||||
depthWalk(f, depth+1, fun)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("gas.depthWalk: unexpected node type %T", n))
|
||||
}
|
||||
}
|
||||
|
||||
type Selector interface {
|
||||
Final(ast.Node)
|
||||
Partial(ast.Node) bool
|
||||
}
|
||||
|
||||
func Select(s Selector, n ast.Node, bits ...reflect.Type) {
|
||||
fun := func(n ast.Node, d int) bool {
|
||||
if d < len(bits) && reflect.TypeOf(n) == bits[d] {
|
||||
if d == len(bits)-1 {
|
||||
s.Final(n)
|
||||
return false
|
||||
} else if s.Partial(n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
depthWalk(n, 0, fun)
|
||||
}
|
||||
|
||||
// SimpleSelect will try to match a path through a sub-tree starting at a given AST node.
|
||||
// The type of each node in the path at a given depth must match its entry in list of
|
||||
// node types given.
|
||||
func SimpleSelect(n ast.Node, bits ...reflect.Type) ast.Node {
|
||||
var found ast.Node
|
||||
fun := func(n ast.Node, d int) bool {
|
||||
if found != nil {
|
||||
return false // short cut logic if we have found a match
|
||||
}
|
||||
|
||||
if d < len(bits) && reflect.TypeOf(n) == bits[d] {
|
||||
if d == len(bits)-1 {
|
||||
found = n
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
depthWalk(n, 0, fun)
|
||||
return found
|
||||
}
|
12
testutils/log.go
Normal file
12
testutils/log.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
)
|
||||
|
||||
// NewLogger returns a logger and the buffer that it will be written to
|
||||
func NewLogger() (*log.Logger, *bytes.Buffer) {
|
||||
var buf bytes.Buffer
|
||||
return log.New(&buf, "", log.Lshortfile), &buf
|
||||
}
|
132
testutils/pkg.go
Normal file
132
testutils/pkg.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
type buildObj struct {
|
||||
pkg *build.Package
|
||||
config loader.Config
|
||||
program *loader.Program
|
||||
}
|
||||
|
||||
type TestPackage struct {
|
||||
Path string
|
||||
Files map[string]string
|
||||
ondisk bool
|
||||
build *buildObj
|
||||
}
|
||||
|
||||
// NewPackage will create a new and empty package. Must call Close() to cleanup
|
||||
// auxilary files
|
||||
func NewTestPackage() *TestPackage {
|
||||
// Files must exist in $GOPATH
|
||||
sourceDir := path.Join(os.Getenv("GOPATH"), "src")
|
||||
workingDir, err := ioutil.TempDir(sourceDir, "gas_test")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &TestPackage{
|
||||
Path: workingDir,
|
||||
Files: make(map[string]string),
|
||||
ondisk: false,
|
||||
build: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// AddFile inserts the filename and contents into the package contents
|
||||
func (p *TestPackage) AddFile(filename, content string) {
|
||||
p.Files[path.Join(p.Path, filename)] = content
|
||||
}
|
||||
|
||||
func (p *TestPackage) write() error {
|
||||
if p.ondisk {
|
||||
return nil
|
||||
}
|
||||
for filename, content := range p.Files {
|
||||
if e := ioutil.WriteFile(filename, []byte(content), 0644); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
p.ondisk = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build ensures all files are persisted to disk and built
|
||||
func (p *TestPackage) Build() error {
|
||||
if p.build != nil {
|
||||
return nil
|
||||
}
|
||||
if err := p.write(); err != nil {
|
||||
return err
|
||||
}
|
||||
basePackage, err := build.Default.ImportDir(p.Path, build.ImportComment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packageConfig := loader.Config{Build: &build.Default, ParserMode: parser.ParseComments}
|
||||
packageFiles := make([]string, 0)
|
||||
for _, filename := range basePackage.GoFiles {
|
||||
packageFiles = append(packageFiles, path.Join(p.Path, filename))
|
||||
}
|
||||
|
||||
packageConfig.CreateFromFilenames(basePackage.Name, packageFiles...)
|
||||
program, err := packageConfig.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.build = &buildObj{
|
||||
pkg: basePackage,
|
||||
config: packageConfig,
|
||||
program: program,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateContext builds a context out of supplied package context
|
||||
func (p *TestPackage) CreateContext(filename string) *gas.Context {
|
||||
if err := p.Build(); err != nil {
|
||||
log.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, pkg := range p.build.program.Created {
|
||||
for _, file := range pkg.Files {
|
||||
pkgFile := p.build.program.Fset.File(file.Pos()).Name()
|
||||
strip := fmt.Sprintf("%s%c", p.Path, os.PathSeparator)
|
||||
pkgFile = strings.TrimPrefix(pkgFile, strip)
|
||||
if pkgFile == filename {
|
||||
ctx := &gas.Context{
|
||||
FileSet: p.build.program.Fset,
|
||||
Root: file,
|
||||
Config: gas.NewConfig(),
|
||||
Info: &pkg.Info,
|
||||
Pkg: pkg.Pkg,
|
||||
Imports: gas.NewImportTracker(),
|
||||
}
|
||||
ctx.Imports.TrackPackages(ctx.Pkg.Imports()...)
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close will delete the package and all files in that directory
|
||||
func (p *TestPackage) Close() {
|
||||
if p.ondisk {
|
||||
os.RemoveAll(p.Path)
|
||||
}
|
||||
}
|
193
testutils/source.go
Normal file
193
testutils/source.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
package testutils
|
||||
|
||||
// CodeSample encapsulates a snippet of source code that compiles, and how many errors should be detected
|
||||
type CodeSample struct {
|
||||
Code string
|
||||
Errors int
|
||||
}
|
||||
|
||||
var (
|
||||
// SampleCodeG101 code snippets for hardcoded credentials
|
||||
SampleCodeG101 = []CodeSample{{`
|
||||
package main
|
||||
import "fmt"
|
||||
func main() {
|
||||
username := "admin"
|
||||
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
fmt.Println("Doing something with: ", username, password)
|
||||
}`, 1}, {`
|
||||
// Entropy check should not report this error by default
|
||||
package main
|
||||
import "fmt"
|
||||
func main() {
|
||||
username := "admin"
|
||||
password := "secret"
|
||||
fmt.Println("Doing something with: ", username, password)
|
||||
}`, 0}, {`
|
||||
package main
|
||||
import "fmt"
|
||||
var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
func main() {
|
||||
username := "admin"
|
||||
fmt.Println("Doing something with: ", username, password)
|
||||
}`, 1}, {`
|
||||
package main
|
||||
import "fmt"
|
||||
const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
func main() {
|
||||
username := "admin"
|
||||
fmt.Println("Doing something with: ", username, password)
|
||||
}`, 1}, {`
|
||||
package main
|
||||
import "fmt"
|
||||
const (
|
||||
username = "user"
|
||||
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
)
|
||||
func main() {
|
||||
fmt.Println("Doing something with: ", username, password)
|
||||
}`, 1}, {`
|
||||
package main
|
||||
var password string
|
||||
func init() {
|
||||
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
}`, 1}, {`
|
||||
package main
|
||||
const (
|
||||
ATNStateSomethingElse = 1
|
||||
ATNStateTokenStart = 42
|
||||
)
|
||||
func main() {
|
||||
println(ATNStateTokenStart)
|
||||
}`, 0}, {`
|
||||
package main
|
||||
const (
|
||||
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
|
||||
)
|
||||
func main() {
|
||||
println(ATNStateTokenStart)
|
||||
}`, 1}}
|
||||
|
||||
// SampleCodeG102 code snippets for network binding
|
||||
SampleCodeG102 = []CodeSample{
|
||||
// Bind to all networks explicitly
|
||||
{`
|
||||
package main
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
func main() {
|
||||
l, err := net.Listen("tcp", "0.0.0.0:2000")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
}`, 1},
|
||||
|
||||
// Bind to all networks implicitly (default if host omitted)
|
||||
{`
|
||||
package main
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
func main() {
|
||||
l, err := net.Listen("tcp", ":2000")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
}`, 1},
|
||||
}
|
||||
// SampleCodeG103 find instances of unsafe blocks for auditing purposes
|
||||
SampleCodeG103 = []CodeSample{
|
||||
{`
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
type Fake struct{}
|
||||
func (Fake) Good() {}
|
||||
func main() {
|
||||
unsafeM := Fake{}
|
||||
unsafeM.Good()
|
||||
intArray := [...]int{1, 2}
|
||||
fmt.Printf("\nintArray: %v\n", intArray)
|
||||
intPtr := &intArray[0]
|
||||
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr)
|
||||
addressHolder := uintptr(unsafe.Pointer(intPtr)) + unsafe.Sizeof(intArray[0])
|
||||
intPtr = (*int)(unsafe.Pointer(addressHolder))
|
||||
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr)
|
||||
}`, 3}}
|
||||
|
||||
// SampleCodeG104 finds errors that aren't being handled
|
||||
SampleCodeG104 = []CodeSample{
|
||||
{`
|
||||
package main
|
||||
import "fmt"
|
||||
func test() (int,error) {
|
||||
return 0, nil
|
||||
}
|
||||
func main() {
|
||||
v, _ := test()
|
||||
fmt.Println(v)
|
||||
}`, 1}, {`
|
||||
package main
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"fmt"
|
||||
)
|
||||
func a() error {
|
||||
return fmt.Errorf("This is an error")
|
||||
}
|
||||
func b() {
|
||||
fmt.Println("b")
|
||||
ioutil.WriteFile("foo.txt", []byte("bar"), os.ModeExclusive)
|
||||
}
|
||||
func c() string {
|
||||
return fmt.Sprintf("This isn't anything")
|
||||
}
|
||||
func main() {
|
||||
_ = a()
|
||||
a()
|
||||
b()
|
||||
c()
|
||||
}`, 3}, {`
|
||||
package main
|
||||
import "fmt"
|
||||
func test() error {
|
||||
return nil
|
||||
}
|
||||
func main() {
|
||||
e := test()
|
||||
fmt.Println(e)
|
||||
}`, 0}}
|
||||
|
||||
// SampleCodeG401 - Use of weak crypto MD5
|
||||
SampleCodeG401 = []CodeSample{
|
||||
{`
|
||||
package main
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
func main() {
|
||||
f, err := os.Open("file.txt")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%x", h.Sum(nil))
|
||||
}`, 1}}
|
||||
)
|
28
testutils/visitor.go
Normal file
28
testutils/visitor.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
)
|
||||
|
||||
// MockVisitor is useful for stubbing out ast.Visitor with callback
|
||||
// and looking for specific conditions to exist.
|
||||
type MockVisitor struct {
|
||||
Context *gas.Context
|
||||
Callback func(n ast.Node, ctx *gas.Context) bool
|
||||
}
|
||||
|
||||
// NewMockVisitor creates a new empty struct, the Context and
|
||||
// Callback must be set manually. See call_list_test.go for an example.
|
||||
func NewMockVisitor() *MockVisitor {
|
||||
return &MockVisitor{}
|
||||
}
|
||||
|
||||
// Visit satisfies the ast.Visitor interface
|
||||
func (v *MockVisitor) Visit(n ast.Node) ast.Visitor {
|
||||
if v.Callback(n, v.Context) {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue