From 9bc02396e86903a631795a3ec57d7b3ddb01268e Mon Sep 17 00:00:00 2001 From: Grant Murphy Date: Sat, 14 Jan 2017 13:45:34 -0800 Subject: [PATCH] Introduce entropy checking of string This will hopefully reduce the number of false positives when it comes to hard coded credentials. The zxcvbn library is used to calculate the entropy of the string. By default the first 16 characters are considered as doing the entropy check for strings much longer than that introduces a fairly significant performance hit. --- rules/hardcoded_credentials.go | 61 ++++++++++++++++++++++++++--- rules/hardcoded_credentials_test.go | 55 +++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/rules/hardcoded_credentials.go b/rules/hardcoded_credentials.go index 1a46ba5..ba66097 100644 --- a/rules/hardcoded_credentials.go +++ b/rules/hardcoded_credentials.go @@ -15,15 +15,32 @@ package rules import ( + "fmt" gas "github.com/GoASTScanner/gas/core" "go/ast" "go/token" "regexp" + + "github.com/nbutton23/zxcvbn-go" + "strconv" ) type Credentials struct { gas.MetaData - pattern *regexp.Regexp + pattern *regexp.Regexp + entropyThreshold float64 + perCharThreshold float64 + truncate int64 + ignoreEntropy bool +} + +func (r *Credentials) isHighEntropyString(str string) bool { + s := fmt.Sprintf("%.*s", r.truncate, str) + info := zxcvbn.PasswordStrength(s, []string{}) + entropyPerChar := info.Entropy / float64(len(s)) + return (info.Entropy >= r.entropyThreshold || + (info.Entropy >= (r.entropyThreshold/2) && + entropyPerChar >= r.perCharThreshold)) } func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) { @@ -41,8 +58,10 @@ func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*ga if ident, ok := i.(*ast.Ident); ok { if r.pattern.MatchString(ident.Name) { for _, e := range assign.Rhs { - if rhs, ok := e.(*ast.BasicLit); ok && rhs.Kind == token.STRING { - return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil + if val, err := gas.GetString(e); err == nil { + if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) { + return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil + } } } } @@ -75,11 +94,43 @@ func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Is func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) { pattern := `(?i)passwd|pass|password|pwd|secret|token` + entropyThreshold := 80.0 + perCharThreshold := 3.0 + ignoreEntropy := false + var truncateString int64 = 16 if val, ok := conf["G101"]; ok { - pattern = val.(string) + conf := val.(map[string]string) + if configPattern, ok := conf["pattern"]; ok { + pattern = configPattern + } + if configIgnoreEntropy, ok := conf["ignore_entropy"]; ok { + if parsedBool, err := strconv.ParseBool(configIgnoreEntropy); err == nil { + ignoreEntropy = parsedBool + } + } + if configEntropyThreshold, ok := conf["entropy_threshold"]; ok { + if parsedNum, err := strconv.ParseFloat(configEntropyThreshold, 64); err == nil { + entropyThreshold = parsedNum + } + } + if configCharThreshold, ok := conf["per_char_threshold"]; ok { + if parsedNum, err := strconv.ParseFloat(configCharThreshold, 64); err == nil { + perCharThreshold = parsedNum + } + } + if configTruncate, ok := conf["truncate"]; ok { + if parsedInt, err := strconv.ParseInt(configTruncate, 10, 64); err == nil { + truncateString = parsedInt + } + } } + return &Credentials{ - pattern: regexp.MustCompile(pattern), + pattern: regexp.MustCompile(pattern), + entropyThreshold: entropyThreshold, + perCharThreshold: perCharThreshold, + ignoreEntropy: ignoreEntropy, + truncate: truncateString, MetaData: gas.MetaData{ What: "Potential hardcoded credentials", Confidence: gas.Low, diff --git a/rules/hardcoded_credentials_test.go b/rules/hardcoded_credentials_test.go index fa164dc..63f3db1 100644 --- a/rules/hardcoded_credentials_test.go +++ b/rules/hardcoded_credentials_test.go @@ -25,6 +25,51 @@ func TestHardcoded(t *testing.T) { 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 @@ -50,7 +95,7 @@ func TestHardcodedGlobalVar(t *testing.T) { import "fmt" - var password = "admin" + var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" func main() { username := "admin" @@ -70,7 +115,7 @@ func TestHardcodedConstant(t *testing.T) { import "fmt" - const password = "secret" + const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" func main() { username := "admin" @@ -92,7 +137,7 @@ func TestHardcodedConstantMulti(t *testing.T) { const ( username = "user" - password = "secret" + password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" ) func main() { @@ -110,7 +155,7 @@ func TestHardecodedVarsNotAssigned(t *testing.T) { package main var password string func init() { - password = "this is a secret string" + password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" }`, analyzer) checkTestResults(t, issues, 1, "Potential hardcoded credentials") } @@ -140,7 +185,7 @@ func TestHardcodedConstString(t *testing.T) { package main const ( - ATNStateTokenStart = "foo bar" + ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" ) func main() { println(ATNStateTokenStart)