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)