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.
This commit is contained in:
Grant Murphy 2017-01-14 13:45:34 -08:00
parent a7ec9ccc63
commit 9bc02396e8
2 changed files with 106 additions and 10 deletions

View file

@ -15,15 +15,32 @@
package rules package rules
import ( import (
"fmt"
gas "github.com/GoASTScanner/gas/core" gas "github.com/GoASTScanner/gas/core"
"go/ast" "go/ast"
"go/token" "go/token"
"regexp" "regexp"
"github.com/nbutton23/zxcvbn-go"
"strconv"
) )
type Credentials struct { type Credentials struct {
gas.MetaData 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) { func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
@ -41,13 +58,15 @@ func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*ga
if ident, ok := i.(*ast.Ident); ok { if ident, ok := i.(*ast.Ident); ok {
if r.pattern.MatchString(ident.Name) { if r.pattern.MatchString(ident.Name) {
for _, e := range assign.Rhs { for _, e := range assign.Rhs {
if rhs, ok := e.(*ast.BasicLit); ok && rhs.Kind == token.STRING { 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 return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil
} }
} }
} }
} }
} }
}
return nil, nil return nil, 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) { func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) {
pattern := `(?i)passwd|pass|password|pwd|secret|token` 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 { 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{ return &Credentials{
pattern: regexp.MustCompile(pattern), pattern: regexp.MustCompile(pattern),
entropyThreshold: entropyThreshold,
perCharThreshold: perCharThreshold,
ignoreEntropy: ignoreEntropy,
truncate: truncateString,
MetaData: gas.MetaData{ MetaData: gas.MetaData{
What: "Potential hardcoded credentials", What: "Potential hardcoded credentials",
Confidence: gas.Low, Confidence: gas.Low,

View file

@ -25,6 +25,51 @@ func TestHardcoded(t *testing.T) {
analyzer := gas.NewAnalyzer(config, nil) analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config)) 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( issues := gasTestRunner(
` `
package samples package samples
@ -50,7 +95,7 @@ func TestHardcodedGlobalVar(t *testing.T) {
import "fmt" import "fmt"
var password = "admin" var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() { func main() {
username := "admin" username := "admin"
@ -70,7 +115,7 @@ func TestHardcodedConstant(t *testing.T) {
import "fmt" import "fmt"
const password = "secret" const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() { func main() {
username := "admin" username := "admin"
@ -92,7 +137,7 @@ func TestHardcodedConstantMulti(t *testing.T) {
const ( const (
username = "user" username = "user"
password = "secret" password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
) )
func main() { func main() {
@ -110,7 +155,7 @@ func TestHardecodedVarsNotAssigned(t *testing.T) {
package main package main
var password string var password string
func init() { func init() {
password = "this is a secret string" password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
}`, analyzer) }`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials") checkTestResults(t, issues, 1, "Potential hardcoded credentials")
} }
@ -140,7 +185,7 @@ func TestHardcodedConstString(t *testing.T) {
package main package main
const ( const (
ATNStateTokenStart = "foo bar" ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
) )
func main() { func main() {
println(ATNStateTokenStart) println(ATNStateTokenStart)