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
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,

View file

@ -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)