Merge pull request #146 from GoASTScanner/experimental

Merge experimental / refactor
This commit is contained in:
Grant Murphy 2018-01-05 22:08:59 +10:00 committed by GitHub
commit 3520a5ae85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 3043 additions and 12012 deletions

2
.gitignore vendored
View file

@ -3,11 +3,11 @@
*.a
*.so
*.swp
gas
# Folders
_obj
_test
vendor
# Architecture specific extensions/prefixes
*.[568vq]

View file

@ -4,3 +4,11 @@ before_script:
go:
- 1.5
- tip
install:
- go get -v github.com/onsi/ginkgo/ginkgo
- go get -v github.com/onsi/gomega
- go get -v -t ./...
- export PATH=$PATH:$HOME/gopath/bin
script: ginkgo -r

285
Godeps/Godeps.json generated Normal file
View file

@ -0,0 +1,285 @@
{
"ImportPath": "github.com/GoASTScanner/gas",
"GoVersion": "go1.9",
"GodepVersion": "v79",
"Deps": [
{
"ImportPath": "github.com/nbutton23/zxcvbn-go",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/adjacency",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/data",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/entropy",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/frequency",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/match",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/matching",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/scoring",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/nbutton23/zxcvbn-go/utils/math",
"Rev": "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
},
{
"ImportPath": "github.com/onsi/ginkgo",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/config",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/codelocation",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/containernode",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/failer",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/leafnodes",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/remote",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/spec",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/spec_iterator",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/specrunner",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/suite",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/testingtproxy",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/internal/writer",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/reporters",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/reporters/stenographer",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/ginkgo/types",
"Comment": "v1.4.0-4-g11459a8",
"Rev": "11459a886d9cd66b319dac7ef1e917ee221372c9"
},
{
"ImportPath": "github.com/onsi/gomega",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/format",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/internal/assertion",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/internal/asyncassertion",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/internal/oraclematcher",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/internal/testingtsupport",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/matchers",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "github.com/onsi/gomega/types",
"Comment": "v1.2.0-2-gdcabb60",
"Rev": "dcabb60a477c2b6f456df65037cb6708210fbb02"
},
{
"ImportPath": "golang.org/x/net/html",
"Rev": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0"
},
{
"ImportPath": "golang.org/x/net/html/atom",
"Rev": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0"
},
{
"ImportPath": "golang.org/x/net/html/charset",
"Rev": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0"
},
{
"ImportPath": "golang.org/x/sys/unix",
"Rev": "164713f0dfcec4e80be8b53e1f0811f5f0d84578"
},
{
"ImportPath": "golang.org/x/text/encoding",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/charmap",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/htmlindex",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/internal",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/internal/identifier",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/japanese",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/korean",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/simplifiedchinese",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/traditionalchinese",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/encoding/unicode",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/internal/tag",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/internal/utf8internal",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/language",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/runes",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/text/transform",
"Rev": "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
},
{
"ImportPath": "golang.org/x/tools/go/ast/astutil",
"Rev": "e531a2a1c15f94033f6fa87666caeb19a688175f"
},
{
"ImportPath": "golang.org/x/tools/go/buildutil",
"Rev": "e531a2a1c15f94033f6fa87666caeb19a688175f"
},
{
"ImportPath": "golang.org/x/tools/go/loader",
"Rev": "e531a2a1c15f94033f6fa87666caeb19a688175f"
},
{
"ImportPath": "gopkg.in/yaml.v2",
"Rev": "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
}
]
}

5
Godeps/Readme generated Normal file
View file

@ -0,0 +1,5 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

View file

@ -64,12 +64,8 @@ $ gas -exclude=G303 ./...
#### Excluding files:
Gas can be told to \ignore paths that match a supplied pattern using the 'skip' command line option. This is
accomplished via [go-glob](github.com/ryanuber/go-glob). Multiple patterns can be specified as follows:
```
$ gas -skip=tests* -skip=*_example.go ./...
```
Gas will ignore dependencies in your vendor directory any files
that are not considered build artifacts by the compiler (so test files).
#### Annotating code

183
analyzer.go Normal file
View file

@ -0,0 +1,183 @@
// (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 holds the central scanning logic used by GAS
package gas
import (
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"path"
"reflect"
"strings"
"golang.org/x/tools/go/loader"
)
// The Context is populated with data parsed from the source code as it is scanned.
// It is passed through to all rule functions as they are called. Rules may use
// this data in conjunction withe the encoutered AST node.
type Context struct {
FileSet *token.FileSet
Comments ast.CommentMap
Info *types.Info
Pkg *types.Package
Root *ast.File
Config map[string]interface{}
Imports *ImportTracker
}
// Metrics used when reporting information about a scanning run.
type Metrics struct {
NumFiles int `json:"files"`
NumLines int `json:"lines"`
NumNosec int `json:"nosec"`
NumFound int `json:"found"`
}
// Analyzer object is the main object of GAS. It has methods traverse an AST
// and invoke the correct checking rules as on each node as required.
type Analyzer struct {
ignoreNosec bool
ruleset RuleSet
context *Context
config Config
logger *log.Logger
issues []*Issue
stats *Metrics
}
// NewAnalyzer builds a new anaylzer.
func NewAnalyzer(conf Config, logger *log.Logger) *Analyzer {
ignoreNoSec := false
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,
ruleset: make(RuleSet),
context: &Context{},
config: conf,
logger: logger,
issues: make([]*Issue, 0, 16),
stats: &Metrics{},
}
}
// LoadRules instantiates all the rules to be used when analyzing source
// packages
func (gas *Analyzer) LoadRules(ruleDefinitions ...RuleBuilder) {
for _, builder := range ruleDefinitions {
r, nodes := builder(gas.config)
gas.ruleset.Register(r, nodes...)
}
}
// Process kicks off the analysis process for a given package
func (gas *Analyzer) Process(packagePath string) error {
basePackage, err := build.Default.ImportDir(packagePath, build.ImportComment)
if err != nil {
return err
}
packageConfig := loader.Config{Build: &build.Default, ParserMode: parser.ParseComments}
var packageFiles []string
for _, filename := range basePackage.GoFiles {
packageFiles = append(packageFiles, path.Join(packagePath, filename))
}
packageConfig.CreateFromFilenames(basePackage.Name, packageFiles...)
builtPackage, err := packageConfig.Load()
if err != nil {
return err
}
for _, pkg := range builtPackage.Created {
gas.logger.Println("Checking package:", pkg.String())
for _, file := range pkg.Files {
gas.logger.Println("Checking file:", builtPackage.Fset.File(file.Pos()).Name())
gas.context.FileSet = builtPackage.Fset
gas.context.Config = gas.config
gas.context.Comments = ast.NewCommentMap(gas.context.FileSet, file, file.Comments)
gas.context.Root = file
gas.context.Info = &pkg.Info
gas.context.Pkg = pkg.Pkg
gas.context.Imports = NewImportTracker()
gas.context.Imports.TrackPackages(gas.context.Pkg.Imports()...)
ast.Walk(gas, file)
gas.stats.NumFiles++
gas.stats.NumLines += builtPackage.Fset.File(file.Pos()).LineCount()
}
}
return nil
}
// ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
func (gas *Analyzer) ignore(n ast.Node) bool {
if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
for _, group := range groups {
if strings.Contains(group.Text(), "#nosec") {
gas.stats.NumNosec++
return true
}
}
}
return false
}
// Visit runs the GAS visitor logic over an AST created by parsing go code.
// Rule methods added with AddRule will be invoked as necessary.
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
if !gas.ignore(n) {
// Track aliased and initialization imports
gas.context.Imports.TrackImport(n)
for _, rule := range gas.ruleset.RegisteredFor(n) {
issue, err := rule.Match(n, gas.context)
if err != nil {
file, line := GetLocation(n, gas.context)
file = path.Base(file)
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
}
if issue != nil {
gas.issues = append(gas.issues, issue)
gas.stats.NumFound++
}
}
return gas
}
return nil
}
// Report returns the current issues discovered and the metrics about the scan
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{}
}

134
analyzer_test.go Normal file
View file

@ -0,0 +1,134 @@
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 be able to analyze mulitple Go files", func() {
analyzer.LoadRules(rules.Generate().Builders()...)
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("foo.go", `
package main
func main(){
bar()
}`)
pkg.AddFile("bar.go", `
package main
func bar(){
println("package has two files!")
}`)
pkg.Build()
err := analyzer.Process(pkg.Path)
Expect(err).ShouldNot(HaveOccurred())
_, metrics := analyzer.Report()
Expect(metrics.NumFiles).To(Equal(2))
})
It("should find errors when nosec is not in use", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
controlPackage := testutils.NewTestPackage()
defer controlPackage.Close()
controlPackage.AddFile("md5.go", source)
controlPackage.Build()
analyzer.Process(controlPackage.Path)
controlIssues, _ := analyzer.Report()
Expect(controlIssues).Should(HaveLen(sample.Errors))
})
It("should not report errors when a nosec comment is present", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close()
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
nosecPackage.AddFile("md5.go", nosecSource)
nosecPackage.Build()
analyzer.Process(nosecPackage.Path)
nosecIssues, _ := analyzer.Report()
Expect(nosecIssues).Should(BeEmpty())
})
})
It("should be possible to overwrite nosec comments, and report issues", func() {
// Rule for MD5 weak crypto usage
sample := testutils.SampleCodeG401[0]
source := sample.Code
// overwrite nosec option
nosecIgnoreConfig := gas.NewConfig()
nosecIgnoreConfig.SetGlobal("nosec", "true")
customAnalyzer := gas.NewAnalyzer(nosecIgnoreConfig, logger)
customAnalyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, "G401")).Builders()...)
nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close()
nosecSource := strings.Replace(source, "h := md5.New()", "h := md5.New() // #nosec", 1)
nosecPackage.AddFile("md5.go", nosecSource)
nosecPackage.Build()
customAnalyzer.Process(nosecPackage.Path)
nosecIssues, _ := customAnalyzer.Report()
Expect(nosecIssues).Should(HaveLen(sample.Errors))
})
})

View file

@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package core
package gas
import (
"go/ast"
@ -19,23 +19,23 @@ import (
type set map[string]bool
/// CallList is used to check for usage of specific packages
/// and functions.
// CallList is used to check for usage of specific packages
// and functions.
type CallList map[string]set
/// NewCallList creates a new empty CallList
// NewCallList creates a new empty CallList
func NewCallList() CallList {
return make(CallList)
}
/// AddAll will add several calls to the call list at once
// AddAll will add several calls to the call list at once
func (c CallList) AddAll(selector string, idents ...string) {
for _, ident := range idents {
c.Add(selector, ident)
}
}
/// Add a selector and call to the call list
// Add a selector and call to the call list
func (c CallList) Add(selector, ident string) {
if _, ok := c[selector]; !ok {
c[selector] = make(set)
@ -43,7 +43,7 @@ func (c CallList) Add(selector, ident string) {
c[selector][ident] = true
}
/// Contains returns true if the package and function are
// Contains returns true if the package and function are
/// members of this call list.
func (c CallList) Contains(selector, ident string) bool {
if idents, ok := c[selector]; ok {
@ -53,21 +53,22 @@ func (c CallList) Contains(selector, ident string) bool {
return false
}
/// ContainsCallExpr resolves the call expression name and type
// 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
}

86
call_list_test.go Normal file
View file

@ -0,0 +1,86 @@
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("call list", func() {
var (
calls gas.CallList
)
BeforeEach(func() {
calls = gas.NewCallList()
})
It("should not return any matches when empty", func() {
Expect(calls.Contains("foo", "bar")).Should(BeFalse())
})
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))
})
})

241
cmd/gas/main.go Normal file
View file

@ -0,0 +1,241 @@
// (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 main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/output"
"github.com/GoASTScanner/gas/rules"
"github.com/kisielk/gotool"
)
const (
usageText = `
GAS - Go AST Scanner
Gas analyzes Go source code to look for common programming mistakes that
can lead to security problems.
USAGE:
# Check a single package
$ gas $GOPATH/src/github.com/example/project
# Check all packages under the current directory and save results in
# json format.
$ gas -fmt=json -out=results.json ./...
# Run a specific set of rules (by default all rules will be run):
$ gas -include=G101,G203,G401 ./...
# Run all rules except the provided
$ gas -exclude=G101 $GOPATH/src/github.com/example/project/...
`
)
var (
// #nosec flag
flagIgnoreNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set")
// format output
flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv, html, or text")
// output file
flagOutput = flag.String("out", "", "Set output file for results")
// config file
flagConfig = flag.String("conf", "", "Path to optional config file")
// quiet
flagQuiet = flag.Bool("quiet", false, "Only show output when errors are found")
// rules to explicitly include
flagRulesInclude = flag.String("include", "", "Comma separated list of rules IDs to include. (see rule list)")
// rules to explicitly exclude
flagRulesExclude = flag.String("exclude", "", "Comma separated list of rules IDs to exclude. (see rule list)")
// log to file or stderr
flagLogfile = flag.String("log", "", "Log messages to file rather than stderr")
logger *log.Logger
)
// #nosec
func usage() {
fmt.Fprintln(os.Stderr, usageText)
fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
flag.PrintDefaults()
fmt.Fprint(os.Stderr, "\n\nRULES:\n\n")
// sorted rule list for ease of reading
rl := rules.Generate()
keys := make([]string, 0, len(rl))
for key := range rl {
keys = append(keys, key)
}
sort.Strings(keys)
for _, k := range keys {
v := rl[k]
fmt.Fprintf(os.Stderr, "\t%s: %s\n", k, v.Description)
}
fmt.Fprint(os.Stderr, "\n")
}
func loadConfig(configFile string) (gas.Config, error) {
config := gas.NewConfig()
if configFile != "" {
file, err := os.Open(configFile)
if err != nil {
return nil, err
}
defer file.Close()
if _, err := config.ReadFrom(file); err != nil {
return nil, err
}
}
if *flagIgnoreNoSec {
config.SetGlobal("nosec", "true")
}
return config, nil
}
func loadRules(include, exclude string) rules.RuleList {
var filters []rules.RuleFilter
if include != "" {
log.Printf("including rules: %s", include)
including := strings.Split(include, ",")
filters = append(filters, rules.NewRuleFilter(false, including...))
} else {
log.Println("including rules: default")
}
if exclude != "" {
log.Printf("excluding rules: %s", exclude)
excluding := strings.Split(exclude, ",")
filters = append(filters, rules.NewRuleFilter(true, excluding...))
} else {
log.Println("excluding rules: default")
}
return rules.Generate(filters...)
}
func saveOutput(filename, format string, issues []*gas.Issue, metrics *gas.Metrics) error {
if filename != "" {
outfile, err := os.Create(filename)
if err != nil {
return err
}
defer outfile.Close()
output.CreateReport(outfile, format, issues, metrics)
} else {
output.CreateReport(os.Stdout, format, issues, metrics)
}
return nil
}
func main() {
// Setup usage description
flag.Usage = usage
// Parse command line arguments
flag.Parse()
// Ensure at least one file was specified
if flag.NArg() == 0 {
fmt.Fprintf(os.Stderr, "\nError: FILE [FILE...] or './...' expected\n")
flag.Usage()
os.Exit(1)
}
// Setup logging
logWriter := os.Stderr
if *flagLogfile != "" {
var e error
logWriter, e = os.Create(*flagLogfile)
if e != nil {
flag.Usage()
log.Fatal(e)
}
}
logger = log.New(logWriter, "[gas] ", log.LstdFlags)
// Load config
config, err := loadConfig(*flagConfig)
if err != nil {
logger.Fatal(err)
}
// Load enabled rule definitions
ruleDefinitions := loadRules(*flagRulesInclude, *flagRulesExclude)
if len(ruleDefinitions) <= 0 {
log.Fatal("cannot continue: no rules are configured.")
}
// Create the analyzer
analyzer := gas.NewAnalyzer(config, logger)
analyzer.LoadRules(ruleDefinitions.Builders()...)
vendor := regexp.MustCompile(`[\\/]vendor([\\/]|$)`)
// Iterate over packages on the import paths
for _, pkg := range gotool.ImportPaths(flag.Args()) {
// Skip vendor directory
if vendor.MatchString(pkg) {
continue
}
abspath, _ := filepath.Abs(pkg)
logger.Println("Searching directory:", abspath)
if err := analyzer.Process(pkg); err != nil {
logger.Fatal(err)
}
}
// Collect the results
issues, metrics := analyzer.Report()
issuesFound := len(issues) > 0
// Exit quietly if nothing was found
if !issuesFound && *flagQuiet {
os.Exit(0)
}
// Create output report
if err := saveOutput(*flagOutput, *flagFormat, issues, metrics); err != nil {
logger.Fatal(err)
}
// Finialize logging
logWriter.Close()
// Do we have an issue? If so exit 1
if issuesFound {
os.Exit(1)
}
}

View file

@ -15,6 +15,7 @@
package main
import (
"flag"
"fmt"
"go/ast"
"go/importer"
@ -274,3 +275,14 @@ func dumpImports(files ...string) {
}
}
}
func main() {
tools := newUtils()
flag.Var(tools, "tool", "Utils to assist with rule development")
flag.Parse()
if len(tools.call) > 0 {
tools.run(flag.Args()...)
os.Exit(0)
}
}

88
config.go Normal file
View file

@ -0,0 +1,88 @@
package gas
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
)
const (
// Globals are applicable to all rules and used for general
// configuration settings for gas.
Globals = "global"
)
// Config is used to provide configuration and customization to each of the rules.
type Config map[string]interface{}
// NewConfig initializes a new configuration instance. The configuration data then
// needs to be loaded via c.ReadFrom(strings.NewReader("config data"))
// or from a *os.File.
func NewConfig() Config {
cfg := make(Config)
cfg[Globals] = make(map[string]string)
return cfg
}
// ReadFrom implements the io.ReaderFrom interface. This
// should be used with io.Reader to load configuration from
//file or from string etc.
func (c Config) ReadFrom(r io.Reader) (int64, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return int64(len(data)), err
}
if err = json.Unmarshal(data, &c); err != nil {
return int64(len(data)), err
}
return int64(len(data)), nil
}
// WriteTo implements the io.WriteTo interface. This should
// be used to save or print out the configuration information.
func (c Config) WriteTo(w io.Writer) (int64, error) {
data, err := json.Marshal(c)
if err != nil {
return int64(len(data)), err
}
return io.Copy(w, bytes.NewReader(data))
}
// 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("Section %s not in configuration", section)
}
return settings, nil
}
// 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[Globals]; 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[Globals]; ok {
if settings, ok := globals.(map[string]string); ok {
settings[option] = value
}
}
}

103
config_test.go Normal file
View file

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

View file

@ -1,235 +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 core holds the central scanning logic used by GAS
package core
import (
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"path"
"reflect"
"strings"
)
// ImportInfo is used to track aliased and initialization only imports.
type ImportInfo struct {
Imported map[string]string
Aliased map[string]string
InitOnly map[string]bool
}
func NewImportInfo() *ImportInfo {
return &ImportInfo{
make(map[string]string),
make(map[string]string),
make(map[string]bool),
}
}
// The Context is populated with data parsed from the source code as it is scanned.
// It is passed through to all rule functions as they are called. Rules may use
// this data in conjunction withe the encoutered AST node.
type Context struct {
FileSet *token.FileSet
Comments ast.CommentMap
Info *types.Info
Pkg *types.Package
Root *ast.File
Config map[string]interface{}
Imports *ImportInfo
}
// The Rule interface used by all rules supported by GAS.
type Rule interface {
Match(ast.Node, *Context) (*Issue, error)
}
// A RuleSet maps lists of rules to the type of AST node they should be run on.
// The anaylzer will only invoke rules contained in the list associated with the
// type of AST node it is currently visiting.
type RuleSet map[reflect.Type][]Rule
// Metrics used when reporting information about a scanning run.
type Metrics struct {
NumFiles int `json:"files"`
NumLines int `json:"lines"`
NumNosec int `json:"nosec"`
NumFound int `json:"found"`
}
// The Analyzer object is the main object of GAS. It has methods traverse an AST
// and invoke the correct checking rules as on each node as required.
type Analyzer struct {
ignoreNosec bool
ruleset RuleSet
context *Context
logger *log.Logger
Issues []*Issue `json:"issues"`
Stats *Metrics `json:"metrics"`
}
// NewAnalyzer builds a new anaylzer.
func NewAnalyzer(conf map[string]interface{}, logger *log.Logger) Analyzer {
if logger == nil {
logger = log.New(os.Stdout, "[gas]", 0)
}
a := Analyzer{
ignoreNosec: conf["ignoreNosec"].(bool),
ruleset: make(RuleSet),
context: &Context{nil, nil, nil, nil, nil, nil, nil},
logger: logger,
Issues: make([]*Issue, 0, 16),
Stats: &Metrics{0, 0, 0, 0},
}
// TODO(tkelsey): use the inc/exc lists
return a
}
func (gas *Analyzer) process(filename string, source interface{}) error {
mode := parser.ParseComments
gas.context.FileSet = token.NewFileSet()
root, err := parser.ParseFile(gas.context.FileSet, filename, source, mode)
if err == nil {
gas.context.Comments = ast.NewCommentMap(gas.context.FileSet, root, root.Comments)
gas.context.Root = root
// here we get type info
gas.context.Info = &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
Implicits: make(map[ast.Node]types.Object),
}
conf := types.Config{Importer: importer.Default()}
gas.context.Pkg, err = conf.Check("pkg", gas.context.FileSet, []*ast.File{root}, gas.context.Info)
if err != nil {
// TODO(gm) Type checker not currently considering all files within a package
// see: issue #113
gas.logger.Printf(`Error during type checking: "%s"`, err)
err = nil
}
gas.context.Imports = NewImportInfo()
for _, pkg := range gas.context.Pkg.Imports() {
gas.context.Imports.Imported[pkg.Path()] = pkg.Name()
}
ast.Walk(gas, root)
gas.Stats.NumFiles++
}
return err
}
// AddRule adds a rule into a rule set list mapped to the given AST node's type.
// The node is only needed for its type and is not otherwise used.
func (gas *Analyzer) AddRule(r Rule, nodes []ast.Node) {
for _, n := range nodes {
t := reflect.TypeOf(n)
if val, ok := gas.ruleset[t]; ok {
gas.ruleset[t] = append(val, r)
} else {
gas.ruleset[t] = []Rule{r}
}
}
}
// Process reads in a source file, convert it to an AST and traverse it.
// Rule methods added with AddRule will be invoked as necessary.
func (gas *Analyzer) Process(filename string) error {
err := gas.process(filename, nil)
fun := func(f *token.File) bool {
gas.Stats.NumLines += f.LineCount()
return true
}
gas.context.FileSet.Iterate(fun)
return err
}
// ProcessSource will convert a source code string into an AST and traverse it.
// Rule methods added with AddRule will be invoked as necessary. The string is
// identified by the filename given but no file IO will be done.
func (gas *Analyzer) ProcessSource(filename string, source string) error {
err := gas.process(filename, source)
fun := func(f *token.File) bool {
gas.Stats.NumLines += f.LineCount()
return true
}
gas.context.FileSet.Iterate(fun)
return err
}
// ignore a node (and sub-tree) if it is tagged with a "#nosec" comment
func (gas *Analyzer) ignore(n ast.Node) bool {
if groups, ok := gas.context.Comments[n]; ok && !gas.ignoreNosec {
for _, group := range groups {
if strings.Contains(group.Text(), "#nosec") {
gas.Stats.NumNosec++
return true
}
}
}
return false
}
// Visit runs the GAS visitor logic over an AST created by parsing go code.
// Rule methods added with AddRule will be invoked as necessary.
func (gas *Analyzer) Visit(n ast.Node) ast.Visitor {
if !gas.ignore(n) {
// Track aliased and initialization imports
if imported, ok := n.(*ast.ImportSpec); ok {
path := strings.Trim(imported.Path.Value, `"`)
if imported.Name != nil {
if imported.Name.Name == "_" {
// Initialization import
gas.context.Imports.InitOnly[path] = true
} else {
// Aliased import
gas.context.Imports.Aliased[path] = imported.Name.Name
}
}
// unsafe is not included in Package.Imports()
if path == "unsafe" {
gas.context.Imports.Imported[path] = path
}
}
if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok {
for _, rule := range val {
ret, err := rule.Match(n, gas.context)
if err != nil {
file, line := GetLocation(n, gas.context)
file = path.Base(file)
gas.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
}
if ret != nil {
gas.Issues = append(gas.Issues, ret)
gas.Stats.NumFound++
}
}
}
return gas
}
return nil
}

View file

@ -1,60 +0,0 @@
package core
import (
"go/ast"
"testing"
)
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"
)
func main() {
var b bytes.Buffer
b.Write([]byte("Hello "))
fmt.Fprintf(&b, "world!")
}`
analyzer.ProcessSource("dummy.go", source)
if rule.matched != 1 {
t.Errorf("Expected to match a bytes.Buffer.Write call")
}
}
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")
}
}

View file

@ -1,71 +0,0 @@
package core
import (
"go/ast"
"testing"
)
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)
}
}

View file

@ -1,404 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
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
}

View file

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

13
gas_suite_test.go Normal file
View file

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

25
glide.lock generated Normal file
View file

@ -0,0 +1,25 @@
hash: fb4cbcb4f806804f30683cd298d9316522f1d7678498039eccdb29f020de1c7f
updated: 2017-05-09T21:54:08.9517391-07:00
imports:
- name: github.com/kisielk/gotool
version: 0de1eaf82fa3f583ce21fde859f1e7e0c5e9b220
- name: github.com/nbutton23/zxcvbn-go
version: a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4
subpackages:
- adjacency
- data
- entropy
- frequency
- match
- matching
- scoring
- utils/math
- name: github.com/ryanuber/go-glob
version: 572520ed46dbddaed19ea3d9541bdd0494163693
- name: golang.org/x/tools
version: 1dbffd0798679c0c6b466e620725135944cfddea
subpackages:
- go/ast/astutil
- go/buildutil
- go/loader
testImports: []

View file

@ -12,41 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package core
package gas
import (
"fmt"
"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,13 +147,11 @@ 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
}
}
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
}

14
helpers_test.go Normal file
View file

@ -0,0 +1,14 @@
package gas_test
import (
. "github.com/onsi/ginkgo"
//. "github.com/onsi/gomega"
)
var _ = Describe("Helpers", func() {
Context("todo", func() {
It("should fail", func() {
Skip("Not implemented")
})
})
})

67
import_tracker.go Normal file
View file

@ -0,0 +1,67 @@
// 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 (
"go/ast"
"go/types"
"strings"
)
// ImportTracker is used to normalize the packages that have been imported
// by a source file. It is able to differentiate between plain imports, aliased
// imports and init only imports.
type ImportTracker struct {
Imported map[string]string
Aliased map[string]string
InitOnly map[string]bool
}
// NewImportTracker creates an empty Import tracker instance
func NewImportTracker() *ImportTracker {
return &ImportTracker{
make(map[string]string),
make(map[string]string),
make(map[string]bool),
}
}
// TrackPackages tracks all the imports used by the supplied packages
func (t *ImportTracker) TrackPackages(pkgs ...*types.Package) {
for _, pkg := range pkgs {
t.Imported[pkg.Path()] = pkg.Name()
// Transient imports
//for _, imp := range pkg.Imports() {
// t.Imported[imp.Path()] = imp.Name()
//}
}
}
// TrackImport tracks imports and handles the 'unsafe' import
func (t *ImportTracker) TrackImport(n ast.Node) {
if imported, ok := n.(*ast.ImportSpec); ok {
path := strings.Trim(imported.Path.Value, `"`)
if imported.Name != nil {
if imported.Name.Name == "_" {
// Initialization only import
t.InitOnly[path] = true
} else {
// Aliased import
t.Aliased[path] = imported.Name.Name
}
}
if path == "unsafe" {
t.Imported[path] = path
}
}
}

29
import_tracker_test.go Normal file
View file

@ -0,0 +1,29 @@
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() {
Skip("Not implemented")
})
It("should correctly track aliased package imports", func() {
Skip("Not implemented")
})
It("should correctly track init only packages", func() {
Skip("Not implemented")
})
})
})

View file

@ -11,32 +11,37 @@
// 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 core
package gas
import (
"encoding/json"
"fmt"
"go/ast"
"os"
"strconv"
)
// Score type used by severity and confidence values
type Score int
const (
Low Score = iota // Low value
Medium // Medium value
High // High value
// Low severity or confidence
Low Score = iota
// Medium severity or confidence
Medium
// High severity or confidence
High
)
// An Issue is returnd by a GAS rule if it discovers an issue with the scanned code.
// Issue is returnd by a GAS rule if it discovers an issue with the scanned code.
type Issue struct {
Severity Score `json:"severity"` // issue severity (how problematic it is)
Confidence Score `json:"confidence"` // issue confidence (how sure we are we found it)
What string `json:"details"` // Human readable explanation
File string `json:"file"` // File name we found it in
Code string `json:"code"` // Impacted code line
Line int `json:"line"` // Line number in file
Line string `json:"line"` // Line number in file
}
// MetaData is embedded in all GAS rules. The Severity, Confidence and What message
@ -85,7 +90,12 @@ func NewIssue(ctx *Context, node ast.Node, desc string, severity Score, confiden
var code string
fobj := ctx.FileSet.File(node.Pos())
name := fobj.Name()
line := fobj.Line(node.Pos())
start, end := fobj.Line(node.Pos()), fobj.Line(node.End())
line := strconv.Itoa(start)
if start != end {
line = fmt.Sprintf("%d-%d", start, end)
}
if file, err := os.Open(fobj.Name()); err == nil {
defer file.Close()

99
issue_test.go Normal file
View file

@ -0,0 +1,99 @@
package gas_test
import (
"go/ast"
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/rules"
"github.com/GoASTScanner/gas/testutils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Issue", func() {
Context("when creating a new issue", func() {
It("should create a code snippet from the specified ast.Node", func() {
var target *ast.BasicLit
source := `package main
const foo = "bar"
func main(){
println(foo)
}
`
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("foo.go", source)
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 {
target = node
return false
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(target).ShouldNot(BeNil())
issue := gas.NewIssue(ctx, target, "", gas.High, gas.High)
Expect(issue).ShouldNot(BeNil())
Expect(issue.Code).Should(MatchRegexp(`"bar"`))
Expect(issue.Line).Should(Equal("2"))
})
It("should return an error if specific context is not able to be obtained", func() {
Skip("Not implemented")
})
It("should provide accurate line and file information", func() {
Skip("Not implemented")
})
It("should provide accurate line and file information for multi-line statements", func() {
var target *ast.BinaryExpr
source := `package main
import "os"
func main(){`
source += "q := `SELECT * FROM table WHERE` + \n os.Args[1] + `= ?` // nolint: gas\n"
source += `println(q)}`
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("foo.go", source)
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())
// Use SQL rule to check binary expr
cfg := gas.NewConfig()
rule, _ := rules.NewSQLStrConcat(cfg)
issue, err := rule.Match(target, ctx)
Expect(err).ShouldNot(HaveOccurred())
Expect(issue).ShouldNot(BeNil())
Expect(issue.File).Should(MatchRegexp("foo.go"))
Expect(issue.Line).Should(MatchRegexp("3-4"))
})
It("should maintain the provided severity score", func() {
Skip("Not implemented")
})
It("should maintain the provided confidence score", func() {
Skip("Not implemented")
})
})
})

293
main.go
View file

@ -1,293 +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 main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas/output"
)
type recursion bool
const (
recurse recursion = true
noRecurse recursion = false
)
var (
// #nosec flag
flagIgnoreNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set")
// format output
flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv, html, or text")
// output file
flagOutput = flag.String("out", "", "Set output file for results")
// config file
flagConfig = flag.String("conf", "", "Path to optional config file")
// quiet
flagQuiet = flag.Bool("quiet", false, "Only show output when errors are found")
usageText = `
GAS - Go AST Scanner
Gas analyzes Go source code to look for common programming mistakes that
can lead to security problems.
USAGE:
# Check a single Go file
$ gas example.go
# Check all files under the current directory and save results in
# json format.
$ gas -fmt=json -out=results.json ./...
# Run a specific set of rules (by default all rules will be run):
$ gas -include=G101,G203,G401 ./...
# Run all rules except the provided
$ gas -exclude=G101 ./...
`
logger *log.Logger
)
func extendConfList(conf map[string]interface{}, name string, inputStr string) {
if inputStr == "" {
conf[name] = []string{}
} else {
input := strings.Split(inputStr, ",")
if val, ok := conf[name]; ok {
if data, ok := val.(*[]string); ok {
conf[name] = append(*data, input...)
} else {
logger.Fatal("Config item must be a string list: ", name)
}
} else {
conf[name] = input
}
}
}
func buildConfig(incRules string, excRules string) map[string]interface{} {
config := make(map[string]interface{})
if flagConfig != nil && *flagConfig != "" { // parse config if we have one
if data, err := ioutil.ReadFile(*flagConfig); err == nil {
if err := json.Unmarshal(data, &(config)); err != nil {
logger.Fatal("Could not parse JSON config: ", *flagConfig, ": ", err)
}
} else {
logger.Fatal("Could not read config file: ", *flagConfig)
}
}
// add in CLI include and exclude data
extendConfList(config, "include", incRules)
extendConfList(config, "exclude", excRules)
// override ignoreNosec if given on CLI
if flagIgnoreNoSec != nil {
config["ignoreNosec"] = *flagIgnoreNoSec
} else {
val, ok := config["ignoreNosec"]
if !ok {
config["ignoreNosec"] = false
} else if _, ok := val.(bool); !ok {
logger.Fatal("Config value must be a bool: 'ignoreNosec'")
}
}
return config
}
// #nosec
func usage() {
fmt.Fprintln(os.Stderr, usageText)
fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
flag.PrintDefaults()
fmt.Fprint(os.Stderr, "\n\nRULES:\n\n")
// sorted rule list for eas of reading
rl := GetFullRuleList()
keys := make([]string, 0, len(rl))
for key := range rl {
keys = append(keys, key)
}
sort.Strings(keys)
for _, k := range keys {
v := rl[k]
fmt.Fprintf(os.Stderr, "\t%s: %s\n", k, v.description)
}
fmt.Fprint(os.Stderr, "\n")
}
func main() {
// Setup usage description
flag.Usage = usage
// Exclude files
excluded := newFileList("*_test.go")
flag.Var(excluded, "skip", "File pattern to exclude from scan. Uses simple * globs and requires full or partial match")
incRules := ""
flag.StringVar(&incRules, "include", "", "Comma separated list of rules IDs to include. (see rule list)")
excRules := ""
flag.StringVar(&excRules, "exclude", "", "Comma separated list of rules IDs to exclude. (see rule list)")
// Custom commands / utilities to run instead of default analyzer
tools := newUtils()
flag.Var(tools, "tool", "GAS utilities to assist with rule development")
// Setup logging
logger = log.New(os.Stderr, "[gas] ", log.LstdFlags)
// Parse command line arguments
flag.Parse()
// Ensure at least one file was specified
if flag.NArg() == 0 {
fmt.Fprintf(os.Stderr, "\nError: FILE [FILE...] or './...' expected\n")
flag.Usage()
os.Exit(1)
}
// Run utils instead of analysis
if len(tools.call) > 0 {
tools.run(flag.Args()...)
os.Exit(0)
}
// Setup analyzer
config := buildConfig(incRules, excRules)
analyzer := gas.NewAnalyzer(config, logger)
AddRules(&analyzer, config)
toAnalyze := getFilesToAnalyze(flag.Args(), excluded)
for _, file := range toAnalyze {
logger.Printf(`Processing "%s"...`, file)
if err := analyzer.Process(file); err != nil {
logger.Printf(`Failed to process: "%s"`, file)
logger.Println(err)
logger.Fatalf(`Halting execution.`)
}
}
issuesFound := len(analyzer.Issues) > 0
// Exit quietly if nothing was found
if !issuesFound && *flagQuiet {
os.Exit(0)
}
// Create output report
if *flagOutput != "" {
outfile, err := os.Create(*flagOutput)
if err != nil {
logger.Fatalf("Couldn't open: %s for writing. Reason - %s", *flagOutput, err)
}
defer outfile.Close()
output.CreateReport(outfile, *flagFormat, &analyzer)
} else {
output.CreateReport(os.Stdout, *flagFormat, &analyzer)
}
// Do we have an issue? If so exit 1
if issuesFound {
os.Exit(1)
}
}
// getFilesToAnalyze lists all files
func getFilesToAnalyze(paths []string, excluded *fileList) []string {
//log.Println("getFilesToAnalyze: start")
var toAnalyze []string
for _, relativePath := range paths {
//log.Printf("getFilesToAnalyze: processing \"%s\"\n", path)
// get the absolute path before doing anything else
path, err := filepath.Abs(relativePath)
if err != nil {
log.Fatal(err)
}
if filepath.Base(relativePath) == "..." {
toAnalyze = append(
toAnalyze,
listFiles(filepath.Dir(path), recurse, excluded)...,
)
} else {
var (
finfo os.FileInfo
err error
)
if finfo, err = os.Stat(path); err != nil {
logger.Fatal(err)
}
if !finfo.IsDir() {
if shouldInclude(path, excluded) {
toAnalyze = append(toAnalyze, path)
}
} else {
toAnalyze = listFiles(path, noRecurse, excluded)
}
}
}
//log.Println("getFilesToAnalyze: end")
return toAnalyze
}
// listFiles returns a list of all files found that pass the shouldInclude check.
// If doRecursiveWalk it true, it will walk the tree rooted at absPath, otherwise it
// will only include files directly within the dir referenced by absPath.
func listFiles(absPath string, doRecursiveWalk recursion, excluded *fileList) []string {
var files []string
walk := func(path string, info os.FileInfo, err error) error {
if info.IsDir() && doRecursiveWalk == noRecurse {
return filepath.SkipDir
}
if shouldInclude(path, excluded) {
files = append(files, path)
}
return nil
}
if err := filepath.Walk(absPath, walk); err != nil {
log.Fatal(err)
}
return files
}
// shouldInclude checks if a specific path which is expected to reference
// a regular file should be included
func shouldInclude(path string, excluded *fileList) bool {
return filepath.Ext(path) == ".go" && !excluded.Contains(path)
}

View file

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

View file

@ -19,18 +19,22 @@ import (
"encoding/json"
htmlTemplate "html/template"
"io"
"strconv"
plainTemplate "text/template"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
// The output format for reported issues
// ReportFormat enumrates the output format for reported issues
type ReportFormat int
const (
// ReportText is the default format that writes to stdout
ReportText ReportFormat = iota // Plain text format
// ReportJSON set the output format to json
ReportJSON // Json format
// ReportCSV set the output format to csv
ReportCSV // CSV format
)
@ -48,7 +52,18 @@ Summary:
`
func CreateReport(w io.Writer, format string, data *gas.Analyzer) error {
type reportInfo struct {
Issues []*gas.Issue
Stats *gas.Metrics
}
// CreateReport generates a report based for the supplied issues and metrics given
// the specified format. The formats currently accepted are: json, csv, html and text.
func CreateReport(w io.Writer, format string, issues []*gas.Issue, metrics *gas.Metrics) error {
data := &reportInfo{
Issues: issues,
Stats: metrics,
}
var err error
switch format {
case "json":
@ -65,7 +80,7 @@ func CreateReport(w io.Writer, format string, data *gas.Analyzer) error {
return err
}
func reportJSON(w io.Writer, data *gas.Analyzer) error {
func reportJSON(w io.Writer, data *reportInfo) error {
raw, err := json.MarshalIndent(data, "", "\t")
if err != nil {
panic(err)
@ -78,13 +93,13 @@ func reportJSON(w io.Writer, data *gas.Analyzer) error {
return err
}
func reportCSV(w io.Writer, data *gas.Analyzer) error {
func reportCSV(w io.Writer, data *reportInfo) error {
out := csv.NewWriter(w)
defer out.Flush()
for _, issue := range data.Issues {
err := out.Write([]string{
issue.File,
strconv.Itoa(issue.Line),
issue.Line,
issue.What,
issue.Severity.String(),
issue.Confidence.String(),
@ -97,7 +112,7 @@ func reportCSV(w io.Writer, data *gas.Analyzer) error {
return nil
}
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *gas.Analyzer) error {
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *reportInfo) error {
t, e := plainTemplate.New("gas").Parse(reportTemplate)
if e != nil {
return e
@ -106,7 +121,7 @@ func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *gas.A
return t.Execute(w, data)
}
func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *gas.Analyzer) error {
func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *reportInfo) error {
t, e := htmlTemplate.New("gas").Parse(reportTemplate)
if e != nil {
return e

View file

@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package core
package gas
import "go/ast"
func resolveIdent(n *ast.Ident, c *Context) bool {
if n.Obj == nil || n.Obj.Kind != ast.Var {
return true
}

99
resolve_test.go Normal file
View file

@ -0,0 +1,99 @@
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()
defer pkg.Close()
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()
defer pkg.Close()
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()
defer pkg.Close()
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()
defer pkg.Close()
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
})
})

58
rule.go Normal file
View file

@ -0,0 +1,58 @@
// 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 (
"go/ast"
"reflect"
)
// The Rule interface used by all rules supported by GAS.
type Rule interface {
Match(ast.Node, *Context) (*Issue, error)
}
// RuleBuilder is used to register a rule definition with the analyzer
type RuleBuilder func(c Config) (Rule, []ast.Node)
// A RuleSet maps lists of rules to the type of AST node they should be run on.
// The anaylzer will only invoke rules contained in the list associated with the
// type of AST node it is currently visiting.
type RuleSet map[reflect.Type][]Rule
// NewRuleSet constructs a new RuleSet
func NewRuleSet() RuleSet {
return make(RuleSet)
}
// Register adds a trigger for the supplied rule for the the
// specified ast nodes.
func (r RuleSet) Register(rule Rule, nodes ...ast.Node) {
for _, n := range nodes {
t := reflect.TypeOf(n)
if rules, ok := r[t]; ok {
r[t] = append(rules, rule)
} else {
r[t] = []Rule{rule}
}
}
}
// RegisteredFor will return all rules that are registered for a
// specified ast node.
func (r RuleSet) RegisteredFor(n ast.Node) []Rule {
if rules, found := r[reflect.TypeOf(n)]; found {
return rules
}
return []Rule{}
}

85
rule_test.go Normal file
View file

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

View file

@ -1,91 +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 main
import (
"go/ast"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas/rules"
)
type RuleInfo struct {
description string
build func(map[string]interface{}) (gas.Rule, []ast.Node)
}
// GetFullRuleList get the full list of all rules available to GAS
func GetFullRuleList() map[string]RuleInfo {
return map[string]RuleInfo{
// misc
"G101": RuleInfo{"Look for hardcoded credentials", rules.NewHardcodedCredentials},
"G102": RuleInfo{"Bind to all interfaces", rules.NewBindsToAllNetworkInterfaces},
"G103": RuleInfo{"Audit the use of unsafe block", rules.NewUsingUnsafe},
"G104": RuleInfo{"Audit errors not checked", rules.NewNoErrorCheck},
"G105": RuleInfo{"Audit the use of big.Exp function", rules.NewUsingBigExp},
// injection
"G201": RuleInfo{"SQL query construction using format string", rules.NewSqlStrFormat},
"G202": RuleInfo{"SQL query construction using string concatenation", rules.NewSqlStrConcat},
"G203": RuleInfo{"Use of unescaped data in HTML templates", rules.NewTemplateCheck},
"G204": RuleInfo{"Audit use of command execution", rules.NewSubproc},
// filesystem
"G301": RuleInfo{"Poor file permissions used when creating a directory", rules.NewMkdirPerms},
"G302": RuleInfo{"Poor file permisions used when creation file or using chmod", rules.NewFilePerms},
"G303": RuleInfo{"Creating tempfile using a predictable path", rules.NewBadTempFile},
// crypto
"G401": RuleInfo{"Detect the usage of DES, RC4, or MD5", rules.NewUsesWeakCryptography},
"G402": RuleInfo{"Look for bad TLS connection settings", rules.NewIntermediateTlsCheck},
"G403": RuleInfo{"Ensure minimum RSA key length of 2048 bits", rules.NewWeakKeyStrength},
"G404": RuleInfo{"Insecure random number source (rand)", rules.NewWeakRandCheck},
// blacklist
"G501": RuleInfo{"Import blacklist: crypto/md5", rules.NewBlacklist_crypto_md5},
"G502": RuleInfo{"Import blacklist: crypto/des", rules.NewBlacklist_crypto_des},
"G503": RuleInfo{"Import blacklist: crypto/rc4", rules.NewBlacklist_crypto_rc4},
"G504": RuleInfo{"Import blacklist: net/http/cgi", rules.NewBlacklist_net_http_cgi},
}
}
func AddRules(analyzer *gas.Analyzer, conf map[string]interface{}) {
var all map[string]RuleInfo
inc := conf["include"].([]string)
exc := conf["exclude"].([]string)
// add included rules
if len(inc) == 0 {
all = GetFullRuleList()
} else {
all = map[string]RuleInfo{}
tmp := GetFullRuleList()
for _, v := range inc {
if val, ok := tmp[v]; ok {
all[v] = val
}
}
}
// remove excluded rules
for _, v := range exc {
delete(all, v)
}
for _, v := range all {
analyzer.AddRule(v.build(conf))
}
}

View file

@ -15,24 +15,27 @@
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"go/ast"
"github.com/GoASTScanner/gas"
)
type UsingBigExp struct {
type usingBigExp struct {
gas.MetaData
pkg string
calls []string
}
func (r *UsingBigExp) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
func (r *usingBigExp) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if _, matched := gas.MatchCallByType(n, c, r.pkg, r.calls...); matched {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
}
return nil, nil
}
func NewUsingBigExp(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &UsingBigExp{
// NewUsingBigExp detects issues with modulus == 0 for Bignum
func NewUsingBigExp(conf gas.Config) (gas.Rule, []ast.Node) {
return &usingBigExp{
pkg: "*math/big.Int",
calls: []string{"Exp"},
MetaData: gas.MetaData{

View file

@ -1,49 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestBigExp(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewUsingBigExp(config))
issues := gasTestRunner(`
package main
import (
"math/big"
)
func main() {
z := new(big.Int)
x := new(big.Int)
x = x.SetUint64(2)
y := new(big.Int)
y = y.SetUint64(4)
m := new(big.Int)
m = m.SetUint64(0)
z = z.Exp(x, y, m)
}
`, analyzer)
checkTestResults(t, issues, 1, "Use of math/big.Int.Exp function")
}

View file

@ -18,30 +18,37 @@ import (
"go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
// Looks for net.Listen("0.0.0.0") or net.Listen(":8080")
type BindsToAllNetworkInterfaces struct {
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 {
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 map[string]interface{}) (gas.Rule, []ast.Node) {
return &BindsToAllNetworkInterfaces{
call: regexp.MustCompile(`^(net|tls)\.Listen$`),
// NewBindsToAllNetworkInterfaces detects socket connections that are setup to
// listen on all network interfaces.
func NewBindsToAllNetworkInterfaces(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("net", "Listen")
calls.Add("tls", "Listen")
return &bindsToAllNetworkInterfaces{
calls: calls,
pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`),
MetaData: gas.MetaData{
Severity: gas.Medium,

View file

@ -1,65 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestBind0000(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
issues := gasTestRunner(`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", "0.0.0.0:2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, analyzer)
checkTestResults(t, issues, 1, "Binds to all network interfaces")
}
func TestBindEmptyHost(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBindsToAllNetworkInterfaces(config))
issues := gasTestRunner(`
package main
import (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", ":2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
}`, analyzer)
checkTestResults(t, issues, 1, "Binds to all network interfaces")
}

View file

@ -16,64 +16,67 @@ package rules
import (
"go/ast"
"strings"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type BlacklistImport struct {
type blacklistedImport struct {
gas.MetaData
Path string
Blacklisted map[string]string
}
func (r *BlacklistImport) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
func unquote(original string) string {
copy := strings.TrimSpace(original)
copy = strings.TrimLeft(copy, `"`)
return strings.TrimRight(copy, `"`)
}
func (r *blacklistedImport) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node, ok := n.(*ast.ImportSpec); ok {
if r.Path == node.Path.Value && node.Name.String() != "_" {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
if description, ok := r.Blacklisted[unquote(node.Path.Value)]; ok {
return gas.NewIssue(c, node, description, r.Severity, r.Confidence), nil
}
}
return nil, nil
}
func NewBlacklist_crypto_md5(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &BlacklistImport{
// NewBlacklistedImports reports when a blacklisted import is being used.
// Typically when a deprecated technology is being used.
func NewBlacklistedImports(conf gas.Config, blacklist map[string]string) (gas.Rule, []ast.Node) {
return &blacklistedImport{
MetaData: gas.MetaData{
Severity: gas.High,
Severity: gas.Medium,
Confidence: gas.High,
What: "Use of weak cryptographic primitive",
},
Path: `"crypto/md5"`,
Blacklisted: blacklist,
}, []ast.Node{(*ast.ImportSpec)(nil)}
}
func NewBlacklist_crypto_des(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &BlacklistImport{
MetaData: gas.MetaData{
Severity: gas.High,
Confidence: gas.High,
What: "Use of weak cryptographic primitive",
},
Path: `"crypto/des"`,
}, []ast.Node{(*ast.ImportSpec)(nil)}
// NewBlacklistedImportMD5 fails if MD5 is imported
func NewBlacklistedImportMD5(conf gas.Config) (gas.Rule, []ast.Node) {
return NewBlacklistedImports(conf, map[string]string{
"crypto/md5": "Blacklisted import crypto/md5: weak cryptographic primitive",
})
}
func NewBlacklist_crypto_rc4(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &BlacklistImport{
MetaData: gas.MetaData{
Severity: gas.High,
Confidence: gas.High,
What: "Use of weak cryptographic primitive",
},
Path: `"crypto/rc4"`,
}, []ast.Node{(*ast.ImportSpec)(nil)}
// NewBlacklistedImportDES fails if DES is imported
func NewBlacklistedImportDES(conf gas.Config) (gas.Rule, []ast.Node) {
return NewBlacklistedImports(conf, map[string]string{
"crypto/des": "Blacklisted import crypto/des: weak cryptographic primitive",
})
}
func NewBlacklist_net_http_cgi(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &BlacklistImport{
MetaData: gas.MetaData{
Severity: gas.High,
Confidence: gas.High,
What: "Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)",
},
Path: `"net/http/cgi"`,
}, []ast.Node{(*ast.ImportSpec)(nil)}
// NewBlacklistedImportRC4 fails if DES is imported
func NewBlacklistedImportRC4(conf gas.Config) (gas.Rule, []ast.Node) {
return NewBlacklistedImports(conf, map[string]string{
"crypto/rc4": "Blacklisted import crypto/rc4: weak cryptographic primitive",
})
}
// NewBlacklistedImportCGI fails if CGI is imported
func NewBlacklistedImportCGI(conf gas.Config) (gas.Rule, []ast.Node) {
return NewBlacklistedImports(conf, map[string]string{
"net/http/cgi": "Blacklisted import net/http/cgi: Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)",
})
}

View file

@ -1,39 +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 (
gas "github.com/GoASTScanner/gas/core"
"testing"
)
const initOnlyImportSrc = `
package main
import (
_ "crypto/md5"
"fmt"
"os"
)
func main() {
for _, arg := range os.Args {
fmt.Println(arg)
}
}`
func TestInitOnlyImport(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_crypto_md5(config))
issues := gasTestRunner(initOnlyImportSrc, analyzer)
checkTestResults(t, issues, 0, "")
}

View file

@ -15,12 +15,13 @@
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"go/ast"
"go/types"
"github.com/GoASTScanner/gas"
)
type NoErrorCheck struct {
type noErrorCheck struct {
gas.MetaData
whitelist gas.CallList
}
@ -29,7 +30,7 @@ func returnsError(callExpr *ast.CallExpr, ctx *gas.Context) int {
if tv := ctx.Info.TypeOf(callExpr); tv != nil {
switch t := tv.(type) {
case *types.Tuple:
for pos := 0; pos < t.Len(); pos += 1 {
for pos := 0; pos < t.Len(); pos++ {
variable := t.At(pos)
if variable != nil && variable.Type().String() == "error" {
return pos
@ -44,11 +45,11 @@ func returnsError(callExpr *ast.CallExpr, ctx *gas.Context) int {
return -1
}
func (r *NoErrorCheck) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
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
@ -59,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
@ -69,7 +70,8 @@ func (r *NoErrorCheck) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
return nil, nil
}
func NewNoErrorCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewNoErrorCheck detects if the returned error is unchecked
func NewNoErrorCheck(conf gas.Config) (gas.Rule, []ast.Node) {
// TODO(gm) Come up with sensible defaults here. Or flip it to use a
// black list instead.
@ -85,7 +87,7 @@ func NewNoErrorCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
}
}
}
return &NoErrorCheck{
return &noErrorCheck{
MetaData: gas.MetaData{
Severity: gas.Low,
Confidence: gas.High,

View file

@ -1,144 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestErrorsMulti(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func test() (int,error) {
return 0, nil
}
func main() {
v, _ := test()
fmt.Println(v)
}`, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
}
func TestErrorsSingle(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func a() error {
return fmt.Errorf("This is an error")
}
func b() {
fmt.Println("b")
}
func c() string {
return fmt.Sprintf("This isn't anything")
}
func main() {
_ = a()
a()
b()
_ = c()
c()
}`, analyzer)
checkTestResults(t, issues, 2, "Errors unhandled")
}
func TestErrorsGood(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
issues := gasTestRunner(
`package main
import (
"fmt"
)
func test() err error {
return 0, nil
}
func main() {
e := test()
}`, analyzer)
checkTestResults(t, issues, 0, "")
}
func TestErrorsWhitelisted(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G104": map[string][]string{
"compress/zlib": []string{"NewReader"},
"io": []string{"Copy"},
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewNoErrorCheck(config))
source := `package main
import (
"io"
"os"
"fmt"
"bytes"
"compress/zlib"
)
func a() error {
return fmt.Errorf("This is an error ok")
}
func main() {
// Expect at least one failure
_ = a()
var b bytes.Buffer
// Default whitelist
nbytes, _ := b.Write([]byte("Hello "))
if nbytes <= 0 {
os.Exit(1)
}
// Whitelisted via configuration
r, _ := zlib.NewReader(&b)
io.Copy(os.Stdout, r)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "Errors unhandled")
}

View file

@ -19,10 +19,10 @@ import (
"go/ast"
"strconv"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type FilePermissions struct {
type filePermissions struct {
gas.MetaData
mode int64
pkg string
@ -30,7 +30,7 @@ type FilePermissions struct {
}
func getConfiguredMode(conf map[string]interface{}, configKey string, defaultMode int64) int64 {
var mode int64 = defaultMode
var mode = defaultMode
if value, ok := conf[configKey]; ok {
switch value.(type) {
case int64:
@ -46,7 +46,7 @@ func getConfiguredMode(conf map[string]interface{}, configKey string, defaultMod
return mode
}
func (r *FilePermissions) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
func (r *filePermissions) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if callexpr, matched := gas.MatchCallByPackage(n, c, r.pkg, r.calls...); matched {
modeArg := callexpr.Args[len(callexpr.Args)-1]
if mode, err := gas.GetInt(modeArg); err == nil && mode > r.mode {
@ -56,9 +56,11 @@ func (r *FilePermissions) Match(n ast.Node, c *gas.Context) (*gas.Issue, error)
return nil, nil
}
func NewFilePerms(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewFilePerms creates a rule to detect file creation with a more permissive than configured
// permission mask.
func NewFilePerms(conf gas.Config) (gas.Rule, []ast.Node) {
mode := getConfiguredMode(conf, "G302", 0600)
return &FilePermissions{
return &filePermissions{
mode: mode,
pkg: "os",
calls: []string{"OpenFile", "Chmod"},
@ -70,9 +72,11 @@ func NewFilePerms(conf map[string]interface{}) (gas.Rule, []ast.Node) {
}, []ast.Node{(*ast.CallExpr)(nil)}
}
func NewMkdirPerms(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewMkdirPerms creates a rule to detect directory creation with more permissive than
// configured permission mask.
func NewMkdirPerms(conf gas.Config) (gas.Rule, []ast.Node) {
mode := getConfiguredMode(conf, "G301", 0700)
return &FilePermissions{
return &filePermissions{
mode: mode,
pkg: "os",
calls: []string{"Mkdir", "MkdirAll"},

View file

@ -1,56 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestChmod(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewFilePerms(config))
issues := gasTestRunner(`
package main
import "os"
func main() {
os.Chmod("/tmp/somefile", 0777)
os.Chmod("/tmp/someotherfile", 0600)
os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0666)
os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600)
}`, analyzer)
checkTestResults(t, issues, 2, "Expect file permissions")
}
func TestMkdir(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewMkdirPerms(config))
issues := gasTestRunner(`
package main
import "os"
func main() {
os.Mkdir("/tmp/mydir", 0777)
os.Mkdir("/tmp/mydir", 0600)
os.MkdirAll("/tmp/mydir/mysubidr", 0775)
}`, analyzer)
checkTestResults(t, issues, 2, "Expect directory permissions")
}

View file

@ -15,16 +15,16 @@
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"go/ast"
"go/token"
"regexp"
"github.com/nbutton23/zxcvbn-go"
"strconv"
"github.com/GoASTScanner/gas"
"github.com/nbutton23/zxcvbn-go"
)
type Credentials struct {
type credentials struct {
gas.MetaData
pattern *regexp.Regexp
entropyThreshold float64
@ -40,7 +40,7 @@ func truncate(s string, n int) string {
return s[:n]
}
func (r *Credentials) isHighEntropyString(str string) bool {
func (r *credentials) isHighEntropyString(str string) bool {
s := truncate(str, r.truncate)
info := zxcvbn.PasswordStrength(s, []string{})
entropyPerChar := info.Entropy / float64(len(s))
@ -49,7 +49,7 @@ func (r *Credentials) isHighEntropyString(str string) bool {
entropyPerChar >= r.perCharThreshold))
}
func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
func (r *credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
switch node := n.(type) {
case *ast.AssignStmt:
return r.matchAssign(node, ctx)
@ -59,7 +59,7 @@ func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
return nil, nil
}
func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*gas.Issue, error) {
func (r *credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*gas.Issue, error) {
for _, i := range assign.Lhs {
if ident, ok := i.(*ast.Ident); ok {
if r.pattern.MatchString(ident.Name) {
@ -76,7 +76,7 @@ func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*ga
return nil, nil
}
func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Issue, error) {
func (r *credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Issue, error) {
if decl.Tok != token.CONST && decl.Tok != token.VAR {
return nil, nil
}
@ -100,12 +100,14 @@ func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Is
return nil, nil
}
func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewHardcodedCredentials attempts to find high entropy string constants being
// assigned to variables that appear to be related to credentials.
func NewHardcodedCredentials(conf gas.Config) (gas.Rule, []ast.Node) {
pattern := `(?i)passwd|pass|password|pwd|secret|token`
entropyThreshold := 80.0
perCharThreshold := 3.0
ignoreEntropy := false
var truncateString int = 16
var truncateString = 16
if val, ok := conf["G101"]; ok {
conf := val.(map[string]string)
if configPattern, ok := conf["pattern"]; ok {
@ -133,7 +135,7 @@ func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node)
}
}
return &Credentials{
return &credentials{
pattern: regexp.MustCompile(pattern),
entropyThreshold: entropyThreshold,
perCharThreshold: perCharThreshold,

View file

@ -1,194 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestHardcoded(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedWithEntropy(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "secret"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
}
func TestHardcodedIgnoreEntropy(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G101": map[string]string{
"ignore_entropy": "true",
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "admin"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedGlobalVar(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package samples
import "fmt"
var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedConstant(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package samples
import "fmt"
const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedConstantMulti(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package samples
import "fmt"
const (
username = "user"
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardecodedVarsNotAssigned(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
var password string
func init() {
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedConstInteger(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
const (
ATNStateSomethingElse = 1
ATNStateTokenStart = 42
)
func main() {
println(ATNStateTokenStart)
}`, analyzer)
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
}
func TestHardcodedConstString(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(`
package main
const (
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
println(ATNStateTokenStart)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}

View file

@ -1,39 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestHttpoxy(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBlacklist_net_http_cgi(config))
issues := gasTestRunner(`
package main
import (
"net/http/cgi"
"net/http"
)
func main() {
cgi.Serve(http.FileServer(http.Dir("/usr/share/doc")))
}`, analyzer)
checkTestResults(t, issues, 1, "Go versions < 1.6.3 are vulnerable to Httpoxy")
}

View file

@ -1,85 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestNosec(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", os.Getenv("BLAH")) // #nosec
cmd.Run()
}`, analyzer)
checkTestResults(t, issues, 0, "None")
}
func TestNosecBlock(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
// #nosec
if true {
cmd := exec.Command("sh", "-c", os.Getenv("BLAH"))
cmd.Run()
}
}`, analyzer)
checkTestResults(t, issues, 0, "None")
}
func TestNosecIgnore(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": true}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(
`package main
import (
"os"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", os.Args[1]) // #nosec
cmd.Run()
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with variable.")
}

View file

@ -17,16 +17,16 @@ package rules
import (
"go/ast"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type WeakRand struct {
type weakRand struct {
gas.MetaData
funcNames []string
packagePath string
}
func (w *WeakRand) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
func (w *weakRand) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
for _, funcName := range w.funcNames {
if _, matched := gas.MatchCallByPackage(n, c, w.packagePath, funcName); matched {
return gas.NewIssue(c, n, w.What, w.Severity, w.Confidence), nil
@ -36,8 +36,9 @@ func (w *WeakRand) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
return nil, nil
}
func NewWeakRandCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &WeakRand{
// NewWeakRandCheck detects the use of random number generator that isn't cryptographically secure
func NewWeakRandCheck(conf gas.Config) (gas.Rule, []ast.Node) {
return &weakRand{
funcNames: []string{"Read", "Int"},
packagePath: "math/rand",
MetaData: gas.MetaData{

View file

@ -1,85 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestRandOk(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import "crypto/rand"
func main() {
good, _ := rand.Read(nil)
println(good)
}`, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestRandBad(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import "math/rand"
func main() {
bad := rand.Int()
println(bad)
}`, analyzer)
checkTestResults(t, issues, 1, "Use of weak random number generator (math/rand instead of crypto/rand)")
}
func TestRandRenamed(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewWeakRandCheck(config))
issues := gasTestRunner(
`
package main
import (
"crypto/rand"
mrand "math/rand"
)
func main() {
good, _ := rand.Read(nil)
println(good)
i := mrand.Int31()
println(i)
}`, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}

View file

@ -17,30 +17,32 @@ package rules
import (
"fmt"
"go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type WeakKeyStrength struct {
type weakKeyStrength struct {
gas.MetaData
pattern *regexp.Regexp
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) {
func (w *weakKeyStrength) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
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
}
}
return nil, nil
}
func NewWeakKeyStrength(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewWeakKeyStrength builds a rule that detects RSA keys < 2048 bits
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$`),
return &weakKeyStrength{
calls: calls,
bits: bits,
MetaData: gas.MetaData{
Severity: gas.Medium,

View file

@ -1,50 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
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")
}

101
rules/rulelist.go Normal file
View file

@ -0,0 +1,101 @@
// (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 (
"github.com/GoASTScanner/gas"
)
// RuleDefinition contains the description of a rule and a mechanism to
// create it.
type RuleDefinition struct {
Description string
Create gas.RuleBuilder
}
// RuleList is a mapping of rule ID's to rule definitions
type RuleList map[string]RuleDefinition
// Builders returns all the create methods for a given rule list
func (rl RuleList) Builders() []gas.RuleBuilder {
builders := make([]gas.RuleBuilder, 0, len(rl))
for _, def := range rl {
builders = append(builders, def.Create)
}
return builders
}
// RuleFilter can be used to include or exclude a rule depending on the return
// value of the function
type RuleFilter func(string) bool
// NewRuleFilter is a closure that will include/exclude the rule ID's based on
// the supplied boolean value.
func NewRuleFilter(action bool, ruleIDs ...string) RuleFilter {
rulelist := make(map[string]bool)
for _, rule := range ruleIDs {
rulelist[rule] = true
}
return func(rule string) bool {
if _, found := rulelist[rule]; found {
return action
}
return !action
}
}
// Generate the list of rules to use
func Generate(filters ...RuleFilter) RuleList {
rules := map[string]RuleDefinition{
// misc
"G101": RuleDefinition{"Look for hardcoded credentials", NewHardcodedCredentials},
"G102": RuleDefinition{"Bind to all interfaces", NewBindsToAllNetworkInterfaces},
"G103": RuleDefinition{"Audit the use of unsafe block", NewUsingUnsafe},
"G104": RuleDefinition{"Audit errors not checked", NewNoErrorCheck},
"G105": RuleDefinition{"Audit the use of big.Exp function", NewUsingBigExp},
// injection
"G201": RuleDefinition{"SQL query construction using format string", NewSQLStrFormat},
"G202": RuleDefinition{"SQL query construction using string concatenation", NewSQLStrConcat},
"G203": RuleDefinition{"Use of unescaped data in HTML templates", NewTemplateCheck},
"G204": RuleDefinition{"Audit use of command execution", NewSubproc},
// filesystem
"G301": RuleDefinition{"Poor file permissions used when creating a directory", NewMkdirPerms},
"G302": RuleDefinition{"Poor file permisions used when creation file or using chmod", NewFilePerms},
"G303": RuleDefinition{"Creating tempfile using a predictable path", NewBadTempFile},
// crypto
"G401": RuleDefinition{"Detect the usage of DES, RC4, or MD5", NewUsesWeakCryptography},
"G402": RuleDefinition{"Look for bad TLS connection settings", NewIntermediateTLSCheck},
"G403": RuleDefinition{"Ensure minimum RSA key length of 2048 bits", NewWeakKeyStrength},
"G404": RuleDefinition{"Insecure random number source (rand)", NewWeakRandCheck},
// blacklist
"G501": RuleDefinition{"Import blacklist: crypto/md5", NewBlacklistedImportMD5},
"G502": RuleDefinition{"Import blacklist: crypto/des", NewBlacklistedImportDES},
"G503": RuleDefinition{"Import blacklist: crypto/rc4", NewBlacklistedImportRC4},
"G504": RuleDefinition{"Import blacklist: net/http/cgi", NewBlacklistedImportCGI},
}
for rule := range rules {
for _, filter := range filters {
if filter(rule) {
delete(rules, rule)
}
}
}
return rules
}

13
rules/rules_suite_test.go Normal file
View file

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

132
rules/rules_test.go Normal file
View file

@ -0,0 +1,132 @@
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()
defer pkg.Close()
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 detect hardcoded credentials", func() {
runner("G101", testutils.SampleCodeG101)
})
It("should detect binding to all network interfaces", func() {
runner("G102", testutils.SampleCodeG102)
})
It("should use of unsafe block", func() {
runner("G103", testutils.SampleCodeG103)
})
It("should errors not being checked", func() {
runner("G104", testutils.SampleCodeG104)
})
It("should detect of big.Exp function", func() {
runner("G105", testutils.SampleCodeG105)
})
It("should detect sql injection via format strings", func() {
runner("G201", testutils.SampleCodeG201)
})
It("should detect sql injection via string concatenation", func() {
runner("G202", testutils.SampleCodeG202)
})
It("should detect unescaped html in templates", func() {
runner("G203", testutils.SampleCodeG203)
})
It("should detect command execution", func() {
runner("G204", testutils.SampleCodeG204)
})
It("should detect poor file permissions on mkdir", func() {
runner("G301", testutils.SampleCodeG301)
})
It("should detect poor permissions when creating or chmod a file", func() {
runner("G302", testutils.SampleCodeG302)
})
It("should detect insecure temp file creation", func() {
runner("G303", testutils.SampleCodeG303)
})
It("should detect weak crypto algorithms", func() {
runner("G401", testutils.SampleCodeG401)
})
It("should find insecure tls settings", func() {
runner("G402", testutils.SampleCodeG402)
})
It("should detect weak creation of weak rsa keys", func() {
runner("G403", testutils.SampleCodeG403)
})
It("should find non cryptographically secure random number sources", func() {
runner("G404", testutils.SampleCodeG404)
})
It("should detect blacklisted imports - MD5", func() {
runner("G501", testutils.SampleCodeG501)
})
It("should detect blacklisted imports - DES", func() {
runner("G502", testutils.SampleCodeG502)
})
It("should detect blacklisted imports - RC4", func() {
runner("G503", testutils.SampleCodeG503)
})
It("should detect blacklisted imports - CGI (httpoxy)", func() {
runner("G504", testutils.SampleCodeG504)
})
})
})

View file

@ -18,20 +18,20 @@ import (
"go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type SqlStatement struct {
type sqlStatement struct {
gas.MetaData
pattern *regexp.Regexp
}
type SqlStrConcat struct {
SqlStatement
type sqlStrConcat struct {
sqlStatement
}
// see if we can figure out what it is
func (s *SqlStrConcat) checkObject(n *ast.Ident) bool {
func (s *sqlStrConcat) checkObject(n *ast.Ident) bool {
if n.Obj != nil {
return n.Obj.Kind != ast.Var && n.Obj.Kind != ast.Fun
}
@ -39,7 +39,7 @@ func (s *SqlStrConcat) checkObject(n *ast.Ident) bool {
}
// Look for "SELECT * FROM table WHERE " + " ' OR 1=1"
func (s *SqlStrConcat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
func (s *sqlStrConcat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node, ok := n.(*ast.BinaryExpr); ok {
if start, ok := node.X.(*ast.BasicLit); ok {
if str, e := gas.GetString(start); s.pattern.MatchString(str) && e == nil {
@ -56,9 +56,10 @@ func (s *SqlStrConcat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
return nil, nil
}
func NewSqlStrConcat(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &SqlStrConcat{
SqlStatement: SqlStatement{
// NewSQLStrConcat looks for cases where we are building SQL strings via concatenation
func NewSQLStrConcat(conf gas.Config) (gas.Rule, []ast.Node) {
return &sqlStrConcat{
sqlStatement: sqlStatement{
pattern: regexp.MustCompile(`(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) `),
MetaData: gas.MetaData{
Severity: gas.Medium,
@ -69,14 +70,16 @@ func NewSqlStrConcat(conf map[string]interface{}) (gas.Rule, []ast.Node) {
}, []ast.Node{(*ast.BinaryExpr)(nil)}
}
type SqlStrFormat struct {
SqlStatement
call *regexp.Regexp
type sqlStrFormat struct {
sqlStatement
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
}
@ -84,10 +87,11 @@ func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err err
return nil, nil
}
func NewSqlStrFormat(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &SqlStrFormat{
call: regexp.MustCompile(`^fmt\.Sprintf$`),
SqlStatement: SqlStatement{
// NewSQLStrFormat looks for cases where we're building SQL query strings using format strings
func NewSQLStrFormat(conf gas.Config) (gas.Rule, []ast.Node) {
rule := &sqlStrFormat{
calls: gas.NewCallList(),
sqlStatement: sqlStatement{
pattern: regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "),
MetaData: gas.MetaData{
Severity: gas.Medium,
@ -95,5 +99,7 @@ func NewSqlStrFormat(conf map[string]interface{}) (gas.Rule, []ast.Node) {
What: "SQL string formatting",
},
},
}, []ast.Node{(*ast.CallExpr)(nil)}
}
rule.calls.AddAll("fmt", "Sprint", "Sprintf", "Sprintln")
return rule, []ast.Node{(*ast.CallExpr)(nil)}
}

View file

@ -1,216 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestSQLInjectionViaConcatenation(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
"os"
)
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query("SELECT * FROM foo WHERE name = " + os.Args[1])
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "SQL string concatenation")
}
func TestSQLInjectionViaIntepolation(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
"fmt"
"os"
//_ "github.com/mattn/go-sqlite3"
)
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", os.Args[1])
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "SQL string formatting")
}
func TestSQLInjectionFalsePositiveA(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < 32"
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveB(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < 32"
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveC(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
var staticQuery = "SELECT * FROM foo WHERE age < "
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery + "32")
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}
func TestSQLInjectionFalsePositiveD(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSqlStrConcat(config))
analyzer.AddRule(NewSqlStrFormat(config))
source := `
package main
import (
"database/sql"
//_ "github.com/mattn/go-sqlite3"
)
const age = "32"
var staticQuery = "SELECT * FROM foo WHERE age < "
func main(){
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
rows, err := db.Query(staticQuery + age)
if err != nil {
panic(err)
}
defer rows.Close()
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "Not expected to match")
}

View file

@ -16,41 +16,43 @@ package rules
import (
"go/ast"
"regexp"
"strings"
"go/types"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type Subprocess struct {
pattern *regexp.Regexp
type subprocess struct {
gas.CallList
}
func (r *Subprocess) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if node := gas.MatchCall(n, r.pattern); node != nil {
// 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 := 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 map[string]interface{}) (gas.Rule, []ast.Node) {
return &Subprocess{
pattern: regexp.MustCompile(`^exec\.Command|syscall\.Exec$`),
}, []ast.Node{(*ast.CallExpr)(nil)}
// NewSubproc detects cases where we are forking out to an external process
func NewSubproc(conf gas.Config) (gas.Rule, []ast.Node) {
rule := &subprocess{gas.NewCallList()}
rule.Add("exec", "Command")
rule.Add("syscall", "Exec")
return rule, []ast.Node{(*ast.CallExpr)(nil)}
}

View file

@ -1,124 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestSubprocess(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os/exec"
)
func main() {
val := "/bin/" + "sleep"
cmd := exec.Command(val, "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching should be audited.")
}
func TestSubprocessVar(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os"
"os/exec"
)
func main() {
run := "sleep" + os.Getenv("SOMETHING")
cmd := exec.Command(run, "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with variable.")
}
func TestSubprocessPath(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching with partial path.")
}
func TestSubprocessSyscall(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewSubproc(config))
issues := gasTestRunner(`
package main
import (
"syscall"
)
func main() {
syscall.Exec("/bin/cat", []string{ "/etc/passwd" }, nil)
}`, analyzer)
checkTestResults(t, issues, 1, "Subprocess launching should be audited.")
}

View file

@ -18,17 +18,17 @@ import (
"go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type BadTempFile struct {
type badTempFile struct {
gas.MetaData
calls gas.CallList
args *regexp.Regexp
call *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 {
func (t *badTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
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
}
@ -36,9 +36,13 @@ func (t *BadTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err erro
return nil, nil
}
func NewBadTempFile(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &BadTempFile{
call: regexp.MustCompile(`ioutil\.WriteFile|os\.Create`),
// NewBadTempFile detects direct writes to predictable path in temporary directory
func NewBadTempFile(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("ioutil", "WriteFile")
calls.Add("os", "Create")
return &badTempFile{
calls: calls,
args: regexp.MustCompile(`^/tmp/.*$|^/var/tmp/.*$`),
MetaData: gas.MetaData{
Severity: gas.Medium,

View file

@ -1,47 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestTempfiles(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewBadTempFile(config))
source := `
package samples
import (
"io/ioutil"
"os"
)
func main() {
file1, _ := os.Create("/tmp/demo1")
defer file1.Close()
ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644)
}
`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 2, "shared tmp directory")
}

View file

@ -16,18 +16,17 @@ package rules
import (
"go/ast"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type TemplateCheck struct {
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
@ -37,9 +36,17 @@ func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err er
return nil, nil
}
func NewTemplateCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &TemplateCheck{
call: regexp.MustCompile(`^template\.(HTML|JS|URL)$`),
// NewTemplateCheck constructs the template check rule. This rule is used to
// find use of tempaltes where HTML/JS escaping is not being used
func NewTemplateCheck(conf gas.Config) (gas.Rule, []ast.Node) {
calls := gas.NewCallList()
calls.Add("template", "HTML")
calls.Add("template", "HTMLAttr")
calls.Add("template", "JS")
calls.Add("template", "URL")
return &templateCheck{
calls: calls,
MetaData: gas.MetaData{
Severity: gas.Medium,
Confidence: gas.Low,

View file

@ -1,136 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestTemplateCheckSafe(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.HTML("<script>alert(1)</script>"),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 0, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadHTML(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.HTML(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadJS(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.JS(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}
func TestTemplateCheckBadURL(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewTemplateCheck(config))
source := `
package samples
import (
"html/template"
"os"
)
const tmpl = ""
func main() {
a := "something from another place"
t := template.Must(template.New("ex").Parse(tmpl))
v := map[string]interface{}{
"Title": "Test <b>World</b>",
"Body": template.URL(a),
}
t.Execute(os.Stdout, v)
}`
issues := gasTestRunner(source, analyzer)
checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed")
}

View file

@ -17,16 +17,14 @@ package rules
import (
"fmt"
"go/ast"
"reflect"
"regexp"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type InsecureConfigTLS struct {
type insecureConfigTLS struct {
MinVersion int16
MaxVersion int16
pattern *regexp.Regexp
requiredType string
goodCiphers []string
}
@ -39,15 +37,14 @@ 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 {
func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gas.Context) *gas.Issue {
if ciphers, ok := n.(*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)
err := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name)
return gas.NewIssue(c, ident, err, gas.High, gas.High)
}
}
}
@ -55,9 +52,10 @@ func (t *InsecureConfigTLS) processTlsCipherSuites(n ast.Node, c *gas.Context) *
return nil
}
func (t *InsecureConfigTLS) processTlsConfVal(n *ast.KeyValueExpr, c *gas.Context) *gas.Issue {
func (t *insecureConfigTLS) processTLSConfVal(n *ast.KeyValueExpr, c *gas.Context) *gas.Issue {
if ident, ok := n.Key.(*ast.Ident); ok {
switch ident.Name {
case "InsecureSkipVerify":
if node, ok := n.Value.(*ast.Ident); ok {
if node.Name != "false" {
@ -97,7 +95,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.Value, c); ret != nil {
return ret
}
@ -107,24 +105,24 @@ func (t *InsecureConfigTLS) processTlsConfVal(n *ast.KeyValueExpr, c *gas.Contex
return nil
}
func (t *InsecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if node := gas.MatchCompLit(n, t.pattern); node != nil {
for _, elt := range node.Elts {
func (t *insecureConfigTLS) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
if complit, ok := n.(*ast.CompositeLit); ok && c.Info.TypeOf(complit.Type).String() == t.requiredType {
for _, elt := range complit.Elts {
if kve, ok := elt.(*ast.KeyValueExpr); ok {
gi = t.processTlsConfVal(kve, c)
if gi != nil {
break
issue := t.processTLSConfVal(kve, c)
if issue != nil {
return issue, nil
}
}
}
}
return
return nil, nil
}
func NewModernTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
// NewModernTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
func NewModernTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) {
return &insecureConfigTLS{
requiredType: "crypto/tls.Config",
MinVersion: 0x0303, // TLS 1.2 only
MaxVersion: 0x0303,
goodCiphers: []string{
@ -138,10 +136,10 @@ func NewModernTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
}, []ast.Node{(*ast.CompositeLit)(nil)}
}
func NewIntermediateTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
// NewIntermediateTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
func NewIntermediateTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) {
return &insecureConfigTLS{
requiredType: "crypto/tls.Config",
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
goodCiphers: []string{
@ -164,10 +162,10 @@ func NewIntermediateTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node)
}, []ast.Node{(*ast.CompositeLit)(nil)}
}
func NewCompatTlsCheck(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29
return &InsecureConfigTLS{
pattern: regexp.MustCompile(`^tls\.Config$`),
// NewCompatTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29
func NewCompatTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) {
return &insecureConfigTLS{
requiredType: "crypto/tls.Config",
MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0
MaxVersion: 0x0303,
goodCiphers: []string{

View file

@ -1,169 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestInsecureSkipVerify(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS InsecureSkipVerify set true")
}
func TestInsecureMinVersion(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: 0},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS MinVersion too low")
}
func TestInsecureMaxVersion(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{MaxVersion: 0},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS MaxVersion too low")
}
func TestInsecureCipherSuite(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{CipherSuites: []uint16{
tls.TLS_RSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS Bad Cipher Suite: TLS_RSA_WITH_RC4_128_SHA")
}
func TestPreferServerCipherSuites(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewModernTlsCheck(config))
issues := gasTestRunner(`
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{PreferServerCipherSuites: false},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
`, analyzer)
checkTestResults(t, issues, 1, "TLS PreferServerCipherSuites set false")
}

View file

@ -15,25 +15,28 @@
package rules
import (
gas "github.com/GoASTScanner/gas/core"
"go/ast"
"github.com/GoASTScanner/gas"
)
type UsingUnsafe struct {
type usingUnsafe struct {
gas.MetaData
pkg string
calls []string
}
func (r *UsingUnsafe) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
func (r *usingUnsafe) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) {
if _, matches := gas.MatchCallByPackage(n, c, r.pkg, r.calls...); matches {
return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil
}
return nil, nil
}
func NewUsingUnsafe(conf map[string]interface{}) (gas.Rule, []ast.Node) {
return &UsingUnsafe{
// NewUsingUnsafe rule detects the use of the unsafe package. This is only
// really useful for auditing purposes.
func NewUsingUnsafe(conf gas.Config) (gas.Rule, []ast.Node) {
return &usingUnsafe{
pkg: "unsafe",
calls: []string{"Alignof", "Offsetof", "Sizeof", "Pointer"},
MetaData: gas.MetaData{

View file

@ -1,55 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func TestUnsafe(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewUsingUnsafe(config))
issues := gasTestRunner(`
package main
import (
"fmt"
"unsafe"
)
type Fake struct{}
func (Fake) Good() {}
func main() {
unsafeM := Fake{}
unsafeM.Good()
intArray := [...]int{1, 2}
fmt.Printf("\nintArray: %v\n", intArray)
intPtr := &intArray[0]
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr)
addressHolder := uintptr(unsafe.Pointer(intPtr)) + unsafe.Sizeof(intArray[0])
intPtr = (*int)(unsafe.Pointer(addressHolder))
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr)
}
`, analyzer)
checkTestResults(t, issues, 3, "Use of unsafe calls")
}

View file

@ -1,40 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"strings"
"testing"
gas "github.com/GoASTScanner/gas/core"
)
func gasTestRunner(source string, analyzer gas.Analyzer) []*gas.Issue {
analyzer.ProcessSource("dummy.go", source)
return analyzer.Issues
}
func checkTestResults(t *testing.T, issues []*gas.Issue, expected int, msg string) {
found := len(issues)
if found != expected {
t.Errorf("Found %d issues, expected %d", found, expected)
}
for _, issue := range issues {
if !strings.Contains(issue.What, msg) {
t.Errorf("Unexpected issue identified: %s", issue.What)
}
}
}

View file

@ -17,15 +17,15 @@ package rules
import (
"go/ast"
gas "github.com/GoASTScanner/gas/core"
"github.com/GoASTScanner/gas"
)
type UsesWeakCryptography struct {
type usesWeakCryptography struct {
gas.MetaData
blacklist map[string][]string
}
func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
func (r *usesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) {
for pkg, funcs := range r.blacklist {
if _, matched := gas.MatchCallByPackage(n, c, pkg, funcs...); matched {
@ -35,13 +35,13 @@ func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, er
return nil, nil
}
// Uses des.* md5.* or rc4.*
func NewUsesWeakCryptography(conf map[string]interface{}) (gas.Rule, []ast.Node) {
// NewUsesWeakCryptography detects uses of des.* md5.* or rc4.*
func NewUsesWeakCryptography(conf gas.Config) (gas.Rule, []ast.Node) {
calls := make(map[string][]string)
calls["crypto/des"] = []string{"NewCipher", "NewTripleDESCipher"}
calls["crypto/md5"] = []string{"New", "Sum"}
calls["crypto/rc4"] = []string{"NewCipher"}
rule := &UsesWeakCryptography{
rule := &usesWeakCryptography{
blacklist: calls,
MetaData: gas.MetaData{
Severity: gas.Medium,

View file

@ -1,114 +0,0 @@
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"testing"
gas "github.com/GoASTScanner/gas/core"
)
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")
}

12
testutils/log.go Normal file
View file

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

133
testutils/pkg.go Normal file
View file

@ -0,0 +1,133 @@
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
}
// TestPackage is a mock package for testing purposes
type TestPackage struct {
Path string
Files map[string]string
ondisk bool
build *buildObj
}
// NewTestPackage 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
}
var packageFiles []string
packageConfig := loader.Config{Build: &build.Default, ParserMode: parser.ParseComments}
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)
}
}

652
testutils/source.go Normal file
View file

@ -0,0 +1,652 @@
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}}
// SampleCodeG105 - bignum overflow
SampleCodeG105 = []CodeSample{{`
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)
}`, 1}}
// SampleCodeG201 - SQL injection via format string
SampleCodeG201 = []CodeSample{
{`
// Format string without proper quoting
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()
}`, 1}, {
`
// Format string false positive
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()
}`, 0}}
// SampleCodeG202 - SQL query string building via string concatenation
SampleCodeG202 = []CodeSample{
{`
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()
}`, 1}, {`
// false positive
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()
}`, 0}, {`
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()
}
`, 0}}
// SampleCodeG203 - Template checks
SampleCodeG203 = []CodeSample{
{`
// We assume that hardcoded template strings are safe as the programmer would
// need to be explicitly shooting themselves in the foot (as below)
package main
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)
}`, 0}, {
`
// Using a variable to initialize could potentially be dangerous. Under the
// current model this will likely produce some false positives.
package main
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)
}`, 1}, {
`
package main
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)
}`, 1}, {
`
package main
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)
}`, 1}}
// SampleCodeG204 - Subprocess auditing
SampleCodeG204 = []CodeSample{{`
package main
import "syscall"
func main() {
syscall.Exec("/bin/cat", []string{ "/etc/passwd" }, nil)
}`, 1}, {`
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)
}`, 1}, {`
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)
}`, 1}}
// SampleCodeG301 - mkdir permission check
SampleCodeG301 = []CodeSample{{`
package main
import "os"
func main() {
os.Mkdir("/tmp/mydir", 0777)
os.Mkdir("/tmp/mydir", 0600)
os.MkdirAll("/tmp/mydir/mysubidr", 0775)
}`, 2}}
// SampleCodeG302 - file create / chmod permissions check
SampleCodeG302 = []CodeSample{{`
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)
}`, 2}}
// SampleCodeG303 - bad tempfile permissions & hardcoded shared path
SampleCodeG303 = []CodeSample{{`
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)
}`, 2}}
// 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}}
// SampleCodeG402 - TLS settings
SampleCodeG402 = []CodeSample{{`
// InsecureSkipVerify
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)
}
}`, 1}, {
`
// Insecure minimum version
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)
}
}`, 1}, {`
// Insecure max version
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)
}
}
`, 1}, {
`
// Insecure ciphersuite selection
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)
}
}`, 1}}
// SampleCodeG403 - weak key strength
SampleCodeG403 = []CodeSample{
{`
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)
}`, 1}}
// SampleCodeG404 - weak random number
SampleCodeG404 = []CodeSample{
{`
package main
import "crypto/rand"
func main() {
good, _ := rand.Read(nil)
println(good)
}`, 0}, {`
package main
import "math/rand"
func main() {
bad := rand.Int()
println(bad)
}`, 1}, {`
package main
import (
"crypto/rand"
mrand "math/rand"
)
func main() {
good, _ := rand.Read(nil)
println(good)
i := mrand.Int31()
println(i)
}`, 0}}
// SampleCodeG501 - Blacklisted import MD5
SampleCodeG501 = []CodeSample{
{`
package main
import (
"crypto/md5"
"fmt"
"os"
)
func main() {
for _, arg := range os.Args {
fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg)
}
}`, 1}}
// SampleCodeG502 - Blacklisted import DES
SampleCodeG502 = []CodeSample{
{`
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))
}`, 1}}
// SampleCodeG503 - Blacklisted import RC4
SampleCodeG503 = []CodeSample{{`
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))
}`, 1}}
// SampleCodeG504 - Blacklisted import CGI
SampleCodeG504 = []CodeSample{{`
package main
import (
"net/http/cgi"
"net/http"
)
func main() {
cgi.Serve(http.FileServer(http.Dir("/usr/share/doc")))
}`, 1}}
)

28
testutils/visitor.go Normal file
View file

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

View file

@ -1,7 +0,0 @@
# package
github.com/GoAstScanner/gas
# import
github.com/GoASTScanner/gas cc52ef5
github.com/nbutton23/zxcvbn-go a22cb81
github.com/ryanuber/go-glob v0.1

View file

@ -1,2 +0,0 @@
zxcvbn
debug.test

View file

@ -1,20 +0,0 @@
Copyright (c) Nathan Button
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,78 +0,0 @@
This is a goLang port of python-zxcvbn and zxcvbn, which are python and JavaScript password strength
generators. zxcvbn attempts to give sound password advice through pattern
matching and conservative entropy calculations. It finds 10k common passwords,
common American names and surnames, common English words, and common patterns
like dates, repeats (aaa), sequences (abcd), and QWERTY patterns.
Please refer to http://tech.dropbox.com/?p=165 for the full details and
motivation behind zxcbvn. The source code for the original JavaScript (well,
actually CoffeeScript) implementation can be found at:
https://github.com/lowe/zxcvbn
Python at:
https://github.com/dropbox/python-zxcvbn
For full motivation, see:
http://tech.dropbox.com/?p=165
------------------------------------------------------------------------
Use
------------------------------------------------------------------------
The zxcvbn module has the public method PasswordStrength() function. Import zxcvbn, and
call PasswordStrength(password string, userInputs []string). The function will return a
result dictionary with the following keys:
Entropy # bits
CrackTime # estimation of actual crack time, in seconds.
CrackTimeDisplay # same crack time, as a friendlier string:
# "instant", "6 minutes", "centuries", etc.
Score # [0,1,2,3,4] if crack time is less than
# [10^2, 10^4, 10^6, 10^8, Infinity].
# (useful for implementing a strength bar.)
MatchSequence # the list of patterns that zxcvbn based the
# entropy calculation on.
CalcTime # how long it took to calculate an answer,
# in milliseconds. usually only a few ms.
The userInputs argument is an splice of strings that zxcvbn
will add to its internal dictionary. This can be whatever list of
strings you like, but is meant for user inputs from other fields of the
form, like name and email. That way a password that includes the user's
personal info can be heavily penalized. This list is also good for
site-specific vocabulary.
Bug reports and pull requests welcome!
------------------------------------------------------------------------
Project Status
------------------------------------------------------------------------
Use zxcvbn_test.go to check how close to feature parity the project is.
------------------------------------------------------------------------
Acknowledgment
------------------------------------------------------------------------
Thanks to Dan Wheeler (https://github.com/lowe) for the CoffeeScript implementation
(see above.) To repeat his outside acknowledgements (which remain useful, as always):
Many thanks to Mark Burnett for releasing his 10k top passwords list:
http://xato.net/passwords/more-top-worst-passwords
and for his 2006 book,
"Perfect Passwords: Selection, Protection, Authentication"
Huge thanks to Wiktionary contributors for building a frequency list
of English as used in television and movies:
http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists
Last but not least, big thanks to xkcd :)
https://xkcd.com/936/

View file

@ -1,96 +0,0 @@
package adjacency
import (
"encoding/json"
"log"
// "fmt"
"github.com/nbutton23/zxcvbn-go/data"
)
type AdjacencyGraph struct {
Graph map[string][]string
averageDegree float64
Name string
}
var AdjacencyGph = make(map[string]AdjacencyGraph)
func init() {
AdjacencyGph["qwerty"] = BuildQwerty()
AdjacencyGph["dvorak"] = BuildDvorak()
AdjacencyGph["keypad"] = BuildKeypad()
AdjacencyGph["macKeypad"] = BuildMacKeypad()
AdjacencyGph["l33t"] = BuildLeet()
}
func BuildQwerty() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Qwerty.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "qwerty")
}
func BuildDvorak() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Dvorak.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "dvorak")
}
func BuildKeypad() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Keypad.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "keypad")
}
func BuildMacKeypad() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/MacKeypad.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "mac_keypad")
}
func BuildLeet() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/L33t.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "keypad")
}
func GetAdjancencyGraphFromFile(data []byte, name string) AdjacencyGraph {
var graph AdjacencyGraph
err := json.Unmarshal(data, &graph)
if err != nil {
log.Fatal(err)
}
graph.Name = name
return graph
}
//on qwerty, 'g' has degree 6, being adjacent to 'ftyhbv'. '\' has degree 1.
//this calculates the average over all keys.
//TODO double check that i ported this correctly scoring.coffee ln 5
func (adjGrp AdjacencyGraph) CalculateAvgDegree() float64 {
if adjGrp.averageDegree != float64(0) {
return adjGrp.averageDegree
}
var avg float64
var count float64
for _, value := range adjGrp.Graph {
for _, char := range value {
if char != "" || char != " " {
avg += float64(len(char))
count++
}
}
}
adjGrp.averageDegree = avg / count
return adjGrp.averageDegree
}

File diff suppressed because one or more lines are too long

View file

@ -1,756 +0,0 @@
{
"Graph": {
"0": [
"9(",
null,
null,
"[{",
"lL",
"rR"
],
"1": [
"`~",
null,
null,
"2@",
"'\"",
null
],
"2": [
"1!",
null,
null,
"3#",
",<",
"'\""
],
"3": [
"2@",
null,
null,
"4$",
".>",
",<"
],
"4": [
"3#",
null,
null,
"5%",
"pP",
".>"
],
"5": [
"4$",
null,
null,
"6^",
"yY",
"pP"
],
"6": [
"5%",
null,
null,
"7&",
"fF",
"yY"
],
"7": [
"6^",
null,
null,
"8*",
"gG",
"fF"
],
"8": [
"7&",
null,
null,
"9(",
"cC",
"gG"
],
"9": [
"8*",
null,
null,
"0)",
"rR",
"cC"
],
"!": [
"`~",
null,
null,
"2@",
"'\"",
null
],
"\"": [
null,
"1!",
"2@",
",<",
"aA",
null
],
"#": [
"2@",
null,
null,
"4$",
".>",
",<"
],
"$": [
"3#",
null,
null,
"5%",
"pP",
".>"
],
"%": [
"4$",
null,
null,
"6^",
"yY",
"pP"
],
"&": [
"6^",
null,
null,
"8*",
"gG",
"fF"
],
"'": [
null,
"1!",
"2@",
",<",
"aA",
null
],
"(": [
"8*",
null,
null,
"0)",
"rR",
"cC"
],
")": [
"9(",
null,
null,
"[{",
"lL",
"rR"
],
"*": [
"7&",
null,
null,
"9(",
"cC",
"gG"
],
"+": [
"/?",
"]}",
null,
"\\|",
null,
"-_"
],
",": [
"'\"",
"2@",
"3#",
".>",
"oO",
"aA"
],
"-": [
"sS",
"/?",
"=+",
null,
null,
"zZ"
],
".": [
",<",
"3#",
"4$",
"pP",
"eE",
"oO"
],
"/": [
"lL",
"[{",
"]}",
"=+",
"-_",
"sS"
],
":": [
null,
"aA",
"oO",
"qQ",
null,
null
],
";": [
null,
"aA",
"oO",
"qQ",
null,
null
],
"<": [
"'\"",
"2@",
"3#",
".>",
"oO",
"aA"
],
"=": [
"/?",
"]}",
null,
"\\|",
null,
"-_"
],
">": [
",<",
"3#",
"4$",
"pP",
"eE",
"oO"
],
"?": [
"lL",
"[{",
"]}",
"=+",
"-_",
"sS"
],
"@": [
"1!",
null,
null,
"3#",
",<",
"'\""
],
"A": [
null,
"'\"",
",<",
"oO",
";:",
null
],
"B": [
"xX",
"dD",
"hH",
"mM",
null,
null
],
"C": [
"gG",
"8*",
"9(",
"rR",
"tT",
"hH"
],
"D": [
"iI",
"fF",
"gG",
"hH",
"bB",
"xX"
],
"E": [
"oO",
".>",
"pP",
"uU",
"jJ",
"qQ"
],
"F": [
"yY",
"6^",
"7&",
"gG",
"dD",
"iI"
],
"G": [
"fF",
"7&",
"8*",
"cC",
"hH",
"dD"
],
"H": [
"dD",
"gG",
"cC",
"tT",
"mM",
"bB"
],
"I": [
"uU",
"yY",
"fF",
"dD",
"xX",
"kK"
],
"J": [
"qQ",
"eE",
"uU",
"kK",
null,
null
],
"K": [
"jJ",
"uU",
"iI",
"xX",
null,
null
],
"L": [
"rR",
"0)",
"[{",
"/?",
"sS",
"nN"
],
"M": [
"bB",
"hH",
"tT",
"wW",
null,
null
],
"N": [
"tT",
"rR",
"lL",
"sS",
"vV",
"wW"
],
"O": [
"aA",
",<",
".>",
"eE",
"qQ",
";:"
],
"P": [
".>",
"4$",
"5%",
"yY",
"uU",
"eE"
],
"Q": [
";:",
"oO",
"eE",
"jJ",
null,
null
],
"R": [
"cC",
"9(",
"0)",
"lL",
"nN",
"tT"
],
"S": [
"nN",
"lL",
"/?",
"-_",
"zZ",
"vV"
],
"T": [
"hH",
"cC",
"rR",
"nN",
"wW",
"mM"
],
"U": [
"eE",
"pP",
"yY",
"iI",
"kK",
"jJ"
],
"V": [
"wW",
"nN",
"sS",
"zZ",
null,
null
],
"W": [
"mM",
"tT",
"nN",
"vV",
null,
null
],
"X": [
"kK",
"iI",
"dD",
"bB",
null,
null
],
"Y": [
"pP",
"5%",
"6^",
"fF",
"iI",
"uU"
],
"Z": [
"vV",
"sS",
"-_",
null,
null,
null
],
"[": [
"0)",
null,
null,
"]}",
"/?",
"lL"
],
"\\": [
"=+",
null,
null,
null,
null,
null
],
"]": [
"[{",
null,
null,
null,
"=+",
"/?"
],
"^": [
"5%",
null,
null,
"7&",
"fF",
"yY"
],
"_": [
"sS",
"/?",
"=+",
null,
null,
"zZ"
],
"`": [
null,
null,
null,
"1!",
null,
null
],
"a": [
null,
"'\"",
",<",
"oO",
";:",
null
],
"b": [
"xX",
"dD",
"hH",
"mM",
null,
null
],
"c": [
"gG",
"8*",
"9(",
"rR",
"tT",
"hH"
],
"d": [
"iI",
"fF",
"gG",
"hH",
"bB",
"xX"
],
"e": [
"oO",
".>",
"pP",
"uU",
"jJ",
"qQ"
],
"f": [
"yY",
"6^",
"7&",
"gG",
"dD",
"iI"
],
"g": [
"fF",
"7&",
"8*",
"cC",
"hH",
"dD"
],
"h": [
"dD",
"gG",
"cC",
"tT",
"mM",
"bB"
],
"i": [
"uU",
"yY",
"fF",
"dD",
"xX",
"kK"
],
"j": [
"qQ",
"eE",
"uU",
"kK",
null,
null
],
"k": [
"jJ",
"uU",
"iI",
"xX",
null,
null
],
"l": [
"rR",
"0)",
"[{",
"/?",
"sS",
"nN"
],
"m": [
"bB",
"hH",
"tT",
"wW",
null,
null
],
"n": [
"tT",
"rR",
"lL",
"sS",
"vV",
"wW"
],
"o": [
"aA",
",<",
".>",
"eE",
"qQ",
";:"
],
"p": [
".>",
"4$",
"5%",
"yY",
"uU",
"eE"
],
"q": [
";:",
"oO",
"eE",
"jJ",
null,
null
],
"r": [
"cC",
"9(",
"0)",
"lL",
"nN",
"tT"
],
"s": [
"nN",
"lL",
"/?",
"-_",
"zZ",
"vV"
],
"t": [
"hH",
"cC",
"rR",
"nN",
"wW",
"mM"
],
"u": [
"eE",
"pP",
"yY",
"iI",
"kK",
"jJ"
],
"v": [
"wW",
"nN",
"sS",
"zZ",
null,
null
],
"w": [
"mM",
"tT",
"nN",
"vV",
null,
null
],
"x": [
"kK",
"iI",
"dD",
"bB",
null,
null
],
"y": [
"pP",
"5%",
"6^",
"fF",
"iI",
"uU"
],
"z": [
"vV",
"sS",
"-_",
null,
null,
null
],
"{": [
"0)",
null,
null,
"]}",
"/?",
"lL"
],
"|": [
"=+",
null,
null,
null,
null,
null
],
"}": [
"[{",
null,
null,
null,
"=+",
"/?"
],
"~": [
null,
null,
null,
"1!",
null,
null
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,154 +0,0 @@
{
"Graph": {
"0": [
null,
"1",
"2",
"3",
".",
null,
null,
null
],
"1": [
null,
null,
"4",
"5",
"2",
"0",
null,
null
],
"2": [
"1",
"4",
"5",
"6",
"3",
".",
"0",
null
],
"3": [
"2",
"5",
"6",
null,
null,
null,
".",
"0"
],
"4": [
null,
null,
"7",
"8",
"5",
"2",
"1",
null
],
"5": [
"4",
"7",
"8",
"9",
"6",
"3",
"2",
"1"
],
"6": [
"5",
"8",
"9",
"+",
null,
null,
"3",
"2"
],
"7": [
null,
null,
null,
"/",
"8",
"5",
"4",
null
],
"8": [
"7",
null,
"/",
"*",
"9",
"6",
"5",
"4"
],
"9": [
"8",
"/",
"*",
"-",
"+",
null,
"6",
"5"
],
"*": [
"/",
null,
null,
null,
"-",
"+",
"9",
"8"
],
"+": [
"9",
"*",
"-",
null,
null,
null,
null,
"6"
],
"-": [
"*",
null,
null,
null,
null,
null,
"+",
"9"
],
".": [
"0",
"2",
"3",
null,
null,
null,
null,
null
],
"/": [
null,
null,
null,
null,
"*",
"9",
"8",
"7"
]
}
}

View file

@ -1,51 +0,0 @@
{
"graph": {
"a": [
"4",
"@"
],
"b": [
"8"
],
"c": [
"(",
"{",
"[",
"<"
],
"e": [
"3"
],
"g": [
"6",
"9"
],
"i": [
"1",
"!",
"|"
],
"l": [
"1",
"|",
"7"
],
"o": [
"0"
],
"s": [
"$",
"5"
],
"t": [
"+",
"7"
],
"x": [
"%"
],
"z": [
"2"
]
}
}

View file

@ -1,164 +0,0 @@
{
"Graph": {
"0": [
null,
"1",
"2",
"3",
".",
null,
null,
null
],
"1": [
null,
null,
"4",
"5",
"2",
"0",
null,
null
],
"2": [
"1",
"4",
"5",
"6",
"3",
".",
"0",
null
],
"3": [
"2",
"5",
"6",
"+",
null,
null,
".",
"0"
],
"4": [
null,
null,
"7",
"8",
"5",
"2",
"1",
null
],
"5": [
"4",
"7",
"8",
"9",
"6",
"3",
"2",
"1"
],
"6": [
"5",
"8",
"9",
"-",
"+",
null,
"3",
"2"
],
"7": [
null,
null,
null,
"=",
"8",
"5",
"4",
null
],
"8": [
"7",
null,
"=",
"/",
"9",
"6",
"5",
"4"
],
"9": [
"8",
"=",
"/",
"*",
"-",
"+",
"6",
"5"
],
"*": [
"/",
null,
null,
null,
null,
null,
"-",
"9"
],
"+": [
"6",
"9",
"-",
null,
null,
null,
null,
"3"
],
"-": [
"9",
"/",
"*",
null,
null,
null,
"+",
"6"
],
".": [
"0",
"2",
"3",
null,
null,
null,
null,
null
],
"/": [
"=",
null,
null,
null,
"*",
"-",
"9",
"8"
],
"=": [
null,
null,
null,
null,
"/",
"9",
"8",
"7"
]
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,756 +0,0 @@
{
"Graph": {
"!": [
"`~",
null,
null,
"2@",
"qQ",
null
],
"\"": [
";:",
"[{",
"]}",
null,
null,
"/?"
],
"#": [
"2@",
null,
null,
"4$",
"eE",
"wW"
],
"$": [
"3#",
null,
null,
"5%",
"rR",
"eE"
],
"%": [
"4$",
null,
null,
"6^",
"tT",
"rR"
],
"&": [
"6^",
null,
null,
"8*",
"uU",
"yY"
],
"'": [
";:",
"[{",
"]}",
null,
null,
"/?"
],
"(": [
"8*",
null,
null,
"0)",
"oO",
"iI"
],
")": [
"9(",
null,
null,
"-_",
"pP",
"oO"
],
"*": [
"7&",
null,
null,
"9(",
"iI",
"uU"
],
"+": [
"-_",
null,
null,
null,
"]}",
"[{"
],
",": [
"mM",
"kK",
"lL",
".>",
null,
null
],
"-": [
"0)",
null,
null,
"=+",
"[{",
"pP"
],
".": [
",<",
"lL",
";:",
"/?",
null,
null
],
"/": [
".>",
";:",
"'\"",
null,
null,
null
],
"0": [
"9(",
null,
null,
"-_",
"pP",
"oO"
],
"1": [
"`~",
null,
null,
"2@",
"qQ",
null
],
"2": [
"1!",
null,
null,
"3#",
"wW",
"qQ"
],
"3": [
"2@",
null,
null,
"4$",
"eE",
"wW"
],
"4": [
"3#",
null,
null,
"5%",
"rR",
"eE"
],
"5": [
"4$",
null,
null,
"6^",
"tT",
"rR"
],
"6": [
"5%",
null,
null,
"7&",
"yY",
"tT"
],
"7": [
"6^",
null,
null,
"8*",
"uU",
"yY"
],
"8": [
"7&",
null,
null,
"9(",
"iI",
"uU"
],
"9": [
"8*",
null,
null,
"0)",
"oO",
"iI"
],
":": [
"lL",
"pP",
"[{",
"'\"",
"/?",
".>"
],
";": [
"lL",
"pP",
"[{",
"'\"",
"/?",
".>"
],
"<": [
"mM",
"kK",
"lL",
".>",
null,
null
],
"=": [
"-_",
null,
null,
null,
"]}",
"[{"
],
">": [
",<",
"lL",
";:",
"/?",
null,
null
],
"?": [
".>",
";:",
"'\"",
null,
null,
null
],
"@": [
"1!",
null,
null,
"3#",
"wW",
"qQ"
],
"A": [
null,
"qQ",
"wW",
"sS",
"zZ",
null
],
"B": [
"vV",
"gG",
"hH",
"nN",
null,
null
],
"C": [
"xX",
"dD",
"fF",
"vV",
null,
null
],
"D": [
"sS",
"eE",
"rR",
"fF",
"cC",
"xX"
],
"E": [
"wW",
"3#",
"4$",
"rR",
"dD",
"sS"
],
"F": [
"dD",
"rR",
"tT",
"gG",
"vV",
"cC"
],
"G": [
"fF",
"tT",
"yY",
"hH",
"bB",
"vV"
],
"H": [
"gG",
"yY",
"uU",
"jJ",
"nN",
"bB"
],
"I": [
"uU",
"8*",
"9(",
"oO",
"kK",
"jJ"
],
"J": [
"hH",
"uU",
"iI",
"kK",
"mM",
"nN"
],
"K": [
"jJ",
"iI",
"oO",
"lL",
",<",
"mM"
],
"L": [
"kK",
"oO",
"pP",
";:",
".>",
",<"
],
"M": [
"nN",
"jJ",
"kK",
",<",
null,
null
],
"N": [
"bB",
"hH",
"jJ",
"mM",
null,
null
],
"O": [
"iI",
"9(",
"0)",
"pP",
"lL",
"kK"
],
"P": [
"oO",
"0)",
"-_",
"[{",
";:",
"lL"
],
"Q": [
null,
"1!",
"2@",
"wW",
"aA",
null
],
"R": [
"eE",
"4$",
"5%",
"tT",
"fF",
"dD"
],
"S": [
"aA",
"wW",
"eE",
"dD",
"xX",
"zZ"
],
"T": [
"rR",
"5%",
"6^",
"yY",
"gG",
"fF"
],
"U": [
"yY",
"7&",
"8*",
"iI",
"jJ",
"hH"
],
"V": [
"cC",
"fF",
"gG",
"bB",
null,
null
],
"W": [
"qQ",
"2@",
"3#",
"eE",
"sS",
"aA"
],
"X": [
"zZ",
"sS",
"dD",
"cC",
null,
null
],
"Y": [
"tT",
"6^",
"7&",
"uU",
"hH",
"gG"
],
"Z": [
null,
"aA",
"sS",
"xX",
null,
null
],
"[": [
"pP",
"-_",
"=+",
"]}",
"'\"",
";:"
],
"\\": [
"]}",
null,
null,
null,
null,
null
],
"]": [
"[{",
"=+",
null,
"\\|",
null,
"'\""
],
"^": [
"5%",
null,
null,
"7&",
"yY",
"tT"
],
"_": [
"0)",
null,
null,
"=+",
"[{",
"pP"
],
"`": [
null,
null,
null,
"1!",
null,
null
],
"a": [
null,
"qQ",
"wW",
"sS",
"zZ",
null
],
"b": [
"vV",
"gG",
"hH",
"nN",
null,
null
],
"c": [
"xX",
"dD",
"fF",
"vV",
null,
null
],
"d": [
"sS",
"eE",
"rR",
"fF",
"cC",
"xX"
],
"e": [
"wW",
"3#",
"4$",
"rR",
"dD",
"sS"
],
"f": [
"dD",
"rR",
"tT",
"gG",
"vV",
"cC"
],
"g": [
"fF",
"tT",
"yY",
"hH",
"bB",
"vV"
],
"h": [
"gG",
"yY",
"uU",
"jJ",
"nN",
"bB"
],
"i": [
"uU",
"8*",
"9(",
"oO",
"kK",
"jJ"
],
"j": [
"hH",
"uU",
"iI",
"kK",
"mM",
"nN"
],
"k": [
"jJ",
"iI",
"oO",
"lL",
",<",
"mM"
],
"l": [
"kK",
"oO",
"pP",
";:",
".>",
",<"
],
"m": [
"nN",
"jJ",
"kK",
",<",
null,
null
],
"n": [
"bB",
"hH",
"jJ",
"mM",
null,
null
],
"o": [
"iI",
"9(",
"0)",
"pP",
"lL",
"kK"
],
"p": [
"oO",
"0)",
"-_",
"[{",
";:",
"lL"
],
"q": [
null,
"1!",
"2@",
"wW",
"aA",
null
],
"r": [
"eE",
"4$",
"5%",
"tT",
"fF",
"dD"
],
"s": [
"aA",
"wW",
"eE",
"dD",
"xX",
"zZ"
],
"t": [
"rR",
"5%",
"6^",
"yY",
"gG",
"fF"
],
"u": [
"yY",
"7&",
"8*",
"iI",
"jJ",
"hH"
],
"v": [
"cC",
"fF",
"gG",
"bB",
null,
null
],
"w": [
"qQ",
"2@",
"3#",
"eE",
"sS",
"aA"
],
"x": [
"zZ",
"sS",
"dD",
"cC",
null,
null
],
"y": [
"tT",
"6^",
"7&",
"uU",
"hH",
"gG"
],
"z": [
null,
"aA",
"sS",
"xX",
null,
null
],
"{": [
"pP",
"-_",
"=+",
"]}",
"'\"",
";:"
],
"|": [
"]}",
null,
null,
null,
null,
null
],
"}": [
"[{",
"=+",
null,
"\\|",
null,
"'\""
],
"~": [
null,
null,
null,
"1!",
null,
null
]
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,215 +0,0 @@
package entropy
import (
"github.com/nbutton23/zxcvbn-go/adjacency"
"github.com/nbutton23/zxcvbn-go/match"
"github.com/nbutton23/zxcvbn-go/utils/math"
"math"
"regexp"
"unicode"
)
const (
START_UPPER string = `^[A-Z][^A-Z]+$`
END_UPPER string = `^[^A-Z]+[A-Z]$'`
ALL_UPPER string = `^[A-Z]+$`
NUM_YEARS = float64(119) // years match against 1900 - 2019
NUM_MONTHS = float64(12)
NUM_DAYS = float64(31)
)
var (
KEYPAD_STARTING_POSITIONS = len(adjacency.AdjacencyGph["keypad"].Graph)
KEYPAD_AVG_DEGREE = adjacency.AdjacencyGph["keypad"].CalculateAvgDegree()
)
func DictionaryEntropy(match match.Match, rank float64) float64 {
baseEntropy := math.Log2(rank)
upperCaseEntropy := extraUpperCaseEntropy(match)
//TODO: L33t
return baseEntropy + upperCaseEntropy
}
func extraUpperCaseEntropy(match match.Match) float64 {
word := match.Token
allLower := true
for _, char := range word {
if unicode.IsUpper(char) {
allLower = false
break
}
}
if allLower {
return float64(0)
}
//a capitalized word is the most common capitalization scheme,
//so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy.
//allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe.
for _, regex := range []string{START_UPPER, END_UPPER, ALL_UPPER} {
matcher := regexp.MustCompile(regex)
if matcher.MatchString(word) {
return float64(1)
}
}
//Otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or
//less. Or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters
//with L lowercase letters or less.
countUpper, countLower := float64(0), float64(0)
for _, char := range word {
if unicode.IsUpper(char) {
countUpper++
} else if unicode.IsLower(char) {
countLower++
}
}
totalLenght := countLower + countUpper
var possibililities float64
for i := float64(0); i <= math.Min(countUpper, countLower); i++ {
possibililities += float64(zxcvbn_math.NChoseK(totalLenght, i))
}
if possibililities < 1 {
return float64(1)
}
return float64(math.Log2(possibililities))
}
func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 {
var s, d float64
if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" {
//todo: verify qwerty and dvorak have the same length and degree
s = float64(len(adjacency.BuildQwerty().Graph))
d = adjacency.BuildQwerty().CalculateAvgDegree()
} else {
s = float64(KEYPAD_STARTING_POSITIONS)
d = KEYPAD_AVG_DEGREE
}
possibilities := float64(0)
length := float64(len(match.Token))
//TODO: Should this be <= or just < ?
//Estimate the number of possible patterns w/ length L or less with t turns or less
for i := float64(2); i <= length+1; i++ {
possibleTurns := math.Min(float64(turns), i-1)
for j := float64(1); j <= possibleTurns+1; j++ {
x := zxcvbn_math.NChoseK(i-1, j-1) * s * math.Pow(d, j)
possibilities += x
}
}
entropy := math.Log2(possibilities)
//add extra entropu for shifted keys. ( % instead of 5 A instead of a)
//Math is similar to extra entropy for uppercase letters in dictionary matches.
if S := float64(shiftCount); S > float64(0) {
possibilities = float64(0)
U := length - S
for i := float64(0); i < math.Min(S, U)+1; i++ {
possibilities += zxcvbn_math.NChoseK(S+U, i)
}
entropy += math.Log2(possibilities)
}
return entropy
}
func RepeatEntropy(match match.Match) float64 {
cardinality := CalcBruteForceCardinality(match.Token)
entropy := math.Log2(cardinality * float64(len(match.Token)))
return entropy
}
//TODO: Validate against python
func CalcBruteForceCardinality(password string) float64 {
lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0)
for _, char := range password {
if unicode.IsLower(char) {
lower = float64(26)
} else if unicode.IsDigit(char) {
digits = float64(10)
} else if unicode.IsUpper(char) {
upper = float64(26)
} else {
symbols = float64(33)
}
}
cardinality := lower + upper + digits + symbols
return cardinality
}
func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) float64 {
firstChar := match.Token[0]
baseEntropy := float64(0)
if string(firstChar) == "a" || string(firstChar) == "1" {
baseEntropy = float64(0)
} else {
baseEntropy = math.Log2(float64(dictionaryLength))
//TODO: should this be just the first or any char?
if unicode.IsUpper(rune(firstChar)) {
baseEntropy++
}
}
if !ascending {
baseEntropy++
}
return baseEntropy + math.Log2(float64(len(match.Token)))
}
func ExtraLeetEntropy(match match.Match, password string) float64 {
var subsitutions float64
var unsub float64
subPassword := password[match.I:match.J]
for index, char := range subPassword {
if string(char) != string(match.Token[index]) {
subsitutions++
} else {
//TODO: Make this only true for 1337 chars that are not subs?
unsub++
}
}
var possibilities float64
for i := float64(0); i <= math.Min(subsitutions, unsub)+1; i++ {
possibilities += zxcvbn_math.NChoseK(subsitutions+unsub, i)
}
if possibilities <= 1 {
return float64(1)
}
return math.Log2(possibilities)
}
func YearEntropy(dateMatch match.DateMatch) float64 {
return math.Log2(NUM_YEARS)
}
func DateEntropy(dateMatch match.DateMatch) float64 {
var entropy float64
if dateMatch.Year < 100 {
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * 100)
} else {
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
}
if dateMatch.Separator != "" {
entropy += 2 //add two bits for separator selection [/,-,.,etc]
}
return entropy
}

View file

@ -1,47 +0,0 @@
package frequency
import (
"encoding/json"
"github.com/nbutton23/zxcvbn-go/data"
"log"
)
type FrequencyList struct {
Name string
List []string
}
var FrequencyLists = make(map[string]FrequencyList)
func init() {
maleFilePath := getAsset("data/MaleNames.json")
femaleFilePath := getAsset("data/FemaleNames.json")
surnameFilePath := getAsset("data/Surnames.json")
englishFilePath := getAsset("data/English.json")
passwordsFilePath := getAsset("data/Passwords.json")
FrequencyLists["MaleNames"] = GetStringListFromAsset(maleFilePath, "MaleNames")
FrequencyLists["FemaleNames"] = GetStringListFromAsset(femaleFilePath, "FemaleNames")
FrequencyLists["Surname"] = GetStringListFromAsset(surnameFilePath, "Surname")
FrequencyLists["English"] = GetStringListFromAsset(englishFilePath, "English")
FrequencyLists["Passwords"] = GetStringListFromAsset(passwordsFilePath, "Passwords")
}
func getAsset(name string) []byte {
data, err := zxcvbn_data.Asset(name)
if err != nil {
panic("Error getting asset " + name)
}
return data
}
func GetStringListFromAsset(data []byte, name string) FrequencyList {
var tempList FrequencyList
err := json.Unmarshal(data, &tempList)
if err != nil {
log.Fatal(err)
}
tempList.Name = name
return tempList
}

View file

@ -1,35 +0,0 @@
package match
type Matches []Match
func (s Matches) Len() int {
return len(s)
}
func (s Matches) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Matches) Less(i, j int) bool {
if s[i].I < s[j].I {
return true
} else if s[i].I == s[j].I {
return s[i].J < s[j].J
} else {
return false
}
}
type Match struct {
Pattern string
I, J int
Token string
DictionaryName string
Entropy float64
}
type DateMatch struct {
Pattern string
I, J int
Token string
Separator string
Day, Month, Year int64
}

View file

@ -1,189 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"regexp"
"strconv"
"strings"
)
func checkDate(day, month, year int64) (bool, int64, int64, int64) {
if (12 <= month && month <= 31) && day <= 12 {
day, month = month, day
}
if day > 31 || month > 12 {
return false, 0, 0, 0
}
if !((1900 <= year && year <= 2019) || (0 <= year && year <= 99)) {
return false, 0, 0, 0
}
return true, day, month, year
}
func dateSepMatcher(password string) []match.Match {
dateMatches := dateSepMatchHelper(password)
var matches []match.Match
for _, dateMatch := range dateMatches {
match := match.Match{
I: dateMatch.I,
J: dateMatch.J,
Entropy: entropy.DateEntropy(dateMatch),
DictionaryName: "date_match",
Token: dateMatch.Token,
}
matches = append(matches, match)
}
return matches
}
func dateSepMatchHelper(password string) []match.DateMatch {
var matches []match.DateMatch
matcher := regexp.MustCompile(DATE_RX_YEAR_SUFFIX)
for _, v := range matcher.FindAllString(password, len(password)) {
splitV := matcher.FindAllStringSubmatch(v, len(v))
i := strings.Index(password, v)
j := i + len(v)
day, _ := strconv.ParseInt(splitV[0][4], 10, 16)
month, _ := strconv.ParseInt(splitV[0][2], 10, 16)
year, _ := strconv.ParseInt(splitV[0][6], 10, 16)
match := match.DateMatch{Day: day, Month: month, Year: year, Separator: splitV[0][5], I: i, J: j, Token: password[i:j]}
matches = append(matches, match)
}
matcher = regexp.MustCompile(DATE_RX_YEAR_PREFIX)
for _, v := range matcher.FindAllString(password, len(password)) {
splitV := matcher.FindAllStringSubmatch(v, len(v))
i := strings.Index(password, v)
j := i + len(v)
day, _ := strconv.ParseInt(splitV[0][4], 10, 16)
month, _ := strconv.ParseInt(splitV[0][6], 10, 16)
year, _ := strconv.ParseInt(splitV[0][2], 10, 16)
match := match.DateMatch{Day: day, Month: month, Year: year, Separator: splitV[0][5], I: i, J: j, Token: password[i:j]}
matches = append(matches, match)
}
var out []match.DateMatch
for _, match := range matches {
if valid, day, month, year := checkDate(match.Day, match.Month, match.Year); valid {
match.Pattern = "date"
match.Day = day
match.Month = month
match.Year = year
out = append(out, match)
}
}
return out
}
type DateMatchCandidate struct {
DayMonth string
Year string
I, J int
}
type DateMatchCandidateTwo struct {
Day string
Month string
Year string
I, J int
}
func dateWithoutSepMatch(password string) []match.Match {
dateMatches := dateWithoutSepMatchHelper(password)
var matches []match.Match
for _, dateMatch := range dateMatches {
match := match.Match{
I: dateMatch.I,
J: dateMatch.J,
Entropy: entropy.DateEntropy(dateMatch),
DictionaryName: "date_match",
Token: dateMatch.Token,
}
matches = append(matches, match)
}
return matches
}
//TODO Has issues with 6 digit dates
func dateWithoutSepMatchHelper(password string) (matches []match.DateMatch) {
matcher := regexp.MustCompile(DATE_WITHOUT_SEP_MATCH)
for _, v := range matcher.FindAllString(password, len(password)) {
i := strings.Index(password, v)
j := i + len(v)
length := len(v)
lastIndex := length - 1
var candidatesRoundOne []DateMatchCandidate
if length <= 6 {
//2-digit year prefix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[2:], v[0:2], i, j))
//2-digityear suffix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-2], v[lastIndex-2:], i, j))
}
if length >= 6 {
//4-digit year prefix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[4:], v[0:4], i, j))
//4-digit year sufix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-3], v[lastIndex-3:], i, j))
}
var candidatesRoundTwo []DateMatchCandidateTwo
for _, c := range candidatesRoundOne {
if len(c.DayMonth) == 2 {
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:0], c.DayMonth[1:1], c.Year, c.I, c.J))
} else if len(c.DayMonth) == 3 {
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:2], c.DayMonth[2:2], c.Year, c.I, c.J))
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:0], c.DayMonth[1:3], c.Year, c.I, c.J))
} else if len(c.DayMonth) == 4 {
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:2], c.DayMonth[2:4], c.Year, c.I, c.J))
}
}
for _, candidate := range candidatesRoundTwo {
intDay, err := strconv.ParseInt(candidate.Day, 10, 16)
if err != nil {
continue
}
intMonth, err := strconv.ParseInt(candidate.Month, 10, 16)
if err != nil {
continue
}
intYear, err := strconv.ParseInt(candidate.Year, 10, 16)
if err != nil {
continue
}
if ok, _, _, _ := checkDate(intDay, intMonth, intYear); ok {
matches = append(matches, match.DateMatch{Token: password, Pattern: "date", Day: intDay, Month: intMonth, Year: intYear, I: i, J: j})
}
}
}
return matches
}
func buildDateMatchCandidate(dayMonth, year string, i, j int) DateMatchCandidate {
return DateMatchCandidate{DayMonth: dayMonth, Year: year, I: i, J: j}
}
func buildDateMatchCandidateTwo(day, month string, year string, i, j int) DateMatchCandidateTwo {
return DateMatchCandidateTwo{Day: day, Month: month, Year: year, I: i, J: j}
}

View file

@ -1,54 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func buildDictMatcher(dictName string, rankedDict map[string]int) func(password string) []match.Match {
return func(password string) []match.Match {
matches := dictionaryMatch(password, dictName, rankedDict)
for _, v := range matches {
v.DictionaryName = dictName
}
return matches
}
}
func dictionaryMatch(password string, dictionaryName string, rankedDict map[string]int) []match.Match {
length := len(password)
var results []match.Match
pwLower := strings.ToLower(password)
for i := 0; i < length; i++ {
for j := i; j < length; j++ {
word := pwLower[i : j+1]
if val, ok := rankedDict[word]; ok {
matchDic := match.Match{Pattern: "dictionary",
DictionaryName: dictionaryName,
I: i,
J: j,
Token: password[i : j+1],
}
matchDic.Entropy = entropy.DictionaryEntropy(matchDic, float64(val))
results = append(results, matchDic)
}
}
}
return results
}
func buildRankedDict(unrankedList []string) map[string]int {
result := make(map[string]int)
for i, v := range unrankedList {
result[strings.ToLower(v)] = i + 1
}
return result
}

View file

@ -1,68 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func l33tMatch(password string) []match.Match {
substitutions := relevantL33tSubtable(password)
permutations := getAllPermutationsOfLeetSubstitutions(password, substitutions)
var matches []match.Match
for _, permutation := range permutations {
for _, mather := range DICTIONARY_MATCHERS {
matches = append(matches, mather(permutation)...)
}
}
for _, match := range matches {
match.Entropy += entropy.ExtraLeetEntropy(match, password)
match.DictionaryName = match.DictionaryName + "_3117"
}
return matches
}
func getAllPermutationsOfLeetSubstitutions(password string, substitutionsMap map[string][]string) []string {
var permutations []string
for index, char := range password {
for value, splice := range substitutionsMap {
for _, sub := range splice {
if string(char) == sub {
var permutation string
permutation = password[:index] + value + password[index+1:]
permutations = append(permutations, permutation)
if index < len(permutation) {
tempPermutations := getAllPermutationsOfLeetSubstitutions(permutation[index+1:], substitutionsMap)
for _, temp := range tempPermutations {
permutations = append(permutations, permutation[:index+1]+temp)
}
}
}
}
}
}
return permutations
}
func relevantL33tSubtable(password string) map[string][]string {
relevantSubs := make(map[string][]string)
for key, values := range L33T_TABLE.Graph {
for _, value := range values {
if strings.Contains(password, value) {
relevantSubs[key] = append(relevantSubs[key], value)
}
}
}
return relevantSubs
}

View file

@ -1,77 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/adjacency"
"github.com/nbutton23/zxcvbn-go/frequency"
"github.com/nbutton23/zxcvbn-go/match"
"sort"
)
var (
DICTIONARY_MATCHERS []func(password string) []match.Match
MATCHERS []func(password string) []match.Match
ADJACENCY_GRAPHS []adjacency.AdjacencyGraph
L33T_TABLE adjacency.AdjacencyGraph
SEQUENCES map[string]string
)
const (
DATE_RX_YEAR_SUFFIX string = `((\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})(\s|-|\/|\\|_|\.)(19\d{2}|200\d|201\d|\d{2}))`
DATE_RX_YEAR_PREFIX string = `((19\d{2}|200\d|201\d|\d{2})(\s|-|/|\\|_|\.)(\d{1,2})(\s|-|/|\\|_|\.)(\d{1,2}))`
DATE_WITHOUT_SEP_MATCH string = `\d{4,8}`
)
func init() {
loadFrequencyList()
}
func Omnimatch(password string, userInputs []string) (matches []match.Match) {
//Can I run into the issue where nil is not equal to nil?
if DICTIONARY_MATCHERS == nil || ADJACENCY_GRAPHS == nil {
loadFrequencyList()
}
if userInputs != nil {
userInputMatcher := buildDictMatcher("user_inputs", buildRankedDict(userInputs))
matches = userInputMatcher(password)
}
for _, matcher := range MATCHERS {
matches = append(matches, matcher(password)...)
}
sort.Sort(match.Matches(matches))
return matches
}
func loadFrequencyList() {
for n, list := range frequency.FrequencyLists {
DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher(n, buildRankedDict(list.List)))
}
L33T_TABLE = adjacency.AdjacencyGph["l33t"]
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["qwerty"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["dvorak"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["keypad"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["macKeypad"])
//l33tFilePath, _ := filepath.Abs("adjacency/L33t.json")
//L33T_TABLE = adjacency.GetAdjancencyGraphFromFile(l33tFilePath, "l33t")
SEQUENCES = make(map[string]string)
SEQUENCES["lower"] = "abcdefghijklmnopqrstuvwxyz"
SEQUENCES["upper"] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
SEQUENCES["digits"] = "0123456789"
MATCHERS = append(MATCHERS, DICTIONARY_MATCHERS...)
MATCHERS = append(MATCHERS, spatialMatch)
MATCHERS = append(MATCHERS, repeatMatch)
MATCHERS = append(MATCHERS, sequenceMatch)
MATCHERS = append(MATCHERS, l33tMatch)
MATCHERS = append(MATCHERS, dateSepMatcher)
MATCHERS = append(MATCHERS, dateWithoutSepMatch)
}

View file

@ -1,59 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func repeatMatch(password string) []match.Match {
var matches []match.Match
//Loop through password. if current == prev currentStreak++ else if currentStreak > 2 {buildMatch; currentStreak = 1} prev = current
var current, prev string
currentStreak := 1
var i int
var char rune
for i, char = range password {
current = string(char)
if i == 0 {
prev = current
continue
}
if strings.ToLower(current) == strings.ToLower(prev) {
currentStreak++
} else if currentStreak > 2 {
iPos := i - currentStreak
jPos := i - 1
matchRepeat := match.Match{
Pattern: "repeat",
I: iPos,
J: jPos,
Token: password[iPos : jPos+1],
DictionaryName: prev}
matchRepeat.Entropy = entropy.RepeatEntropy(matchRepeat)
matches = append(matches, matchRepeat)
currentStreak = 1
} else {
currentStreak = 1
}
prev = current
}
if currentStreak > 2 {
iPos := i - currentStreak + 1
jPos := i
matchRepeat := match.Match{
Pattern: "repeat",
I: iPos,
J: jPos,
Token: password[iPos : jPos+1],
DictionaryName: prev}
matchRepeat.Entropy = entropy.RepeatEntropy(matchRepeat)
matches = append(matches, matchRepeat)
}
return matches
}

View file

@ -1,68 +0,0 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func sequenceMatch(password string) []match.Match {
var matches []match.Match
for i := 0; i < len(password); {
j := i + 1
var seq string
var seqName string
seqDirection := 0
for seqCandidateName, seqCandidate := range SEQUENCES {
iN := strings.Index(seqCandidate, string(password[i]))
var jN int
if j < len(password) {
jN = strings.Index(seqCandidate, string(password[j]))
} else {
jN = -1
}
if iN > -1 && jN > -1 {
direction := jN - iN
if direction == 1 || direction == -1 {
seq = seqCandidate
seqName = seqCandidateName
seqDirection = direction
break
}
}
}
if seq != "" {
for {
var prevN, curN int
if j < len(password) {
prevChar, curChar := password[j-1], password[j]
prevN, curN = strings.Index(seq, string(prevChar)), strings.Index(seq, string(curChar))
}
if j == len(password) || curN-prevN != seqDirection {
if j-i > 2 {
matchSequence := match.Match{
Pattern: "sequence",
I: i,
J: j - 1,
Token: password[i:j],
DictionaryName: seqName,
}
matchSequence.Entropy = entropy.SequenceEntropy(matchSequence, len(seq), (seqDirection == 1))
matches = append(matches, matchSequence)
}
break
} else {
j += 1
}
}
}
i = j
}
return matches
}

Some files were not shown because too many files have changed in this diff Show more