Skip to content

Commit 9bc0239

Browse files
committed
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.
1 parent a7ec9cc commit 9bc0239

File tree

2 files changed

+106
-10
lines changed

2 files changed

+106
-10
lines changed

rules/hardcoded_credentials.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,32 @@
1515
package rules
1616

1717
import (
18+
"fmt"
1819
gas "github.com/GoASTScanner/gas/core"
1920
"go/ast"
2021
"go/token"
2122
"regexp"
23+
24+
"github.com/nbutton23/zxcvbn-go"
25+
"strconv"
2226
)
2327

2428
type Credentials struct {
2529
gas.MetaData
26-
pattern *regexp.Regexp
30+
pattern *regexp.Regexp
31+
entropyThreshold float64
32+
perCharThreshold float64
33+
truncate int64
34+
ignoreEntropy bool
35+
}
36+
37+
func (r *Credentials) isHighEntropyString(str string) bool {
38+
s := fmt.Sprintf("%.*s", r.truncate, str)
39+
info := zxcvbn.PasswordStrength(s, []string{})
40+
entropyPerChar := info.Entropy / float64(len(s))
41+
return (info.Entropy >= r.entropyThreshold ||
42+
(info.Entropy >= (r.entropyThreshold/2) &&
43+
entropyPerChar >= r.perCharThreshold))
2744
}
2845

2946
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
4158
if ident, ok := i.(*ast.Ident); ok {
4259
if r.pattern.MatchString(ident.Name) {
4360
for _, e := range assign.Rhs {
44-
if rhs, ok := e.(*ast.BasicLit); ok && rhs.Kind == token.STRING {
45-
return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil
61+
if val, err := gas.GetString(e); err == nil {
62+
if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) {
63+
return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil
64+
}
4665
}
4766
}
4867
}
@@ -75,11 +94,43 @@ func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Is
7594

7695
func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) {
7796
pattern := `(?i)passwd|pass|password|pwd|secret|token`
97+
entropyThreshold := 80.0
98+
perCharThreshold := 3.0
99+
ignoreEntropy := false
100+
var truncateString int64 = 16
78101
if val, ok := conf["G101"]; ok {
79-
pattern = val.(string)
102+
conf := val.(map[string]string)
103+
if configPattern, ok := conf["pattern"]; ok {
104+
pattern = configPattern
105+
}
106+
if configIgnoreEntropy, ok := conf["ignore_entropy"]; ok {
107+
if parsedBool, err := strconv.ParseBool(configIgnoreEntropy); err == nil {
108+
ignoreEntropy = parsedBool
109+
}
110+
}
111+
if configEntropyThreshold, ok := conf["entropy_threshold"]; ok {
112+
if parsedNum, err := strconv.ParseFloat(configEntropyThreshold, 64); err == nil {
113+
entropyThreshold = parsedNum
114+
}
115+
}
116+
if configCharThreshold, ok := conf["per_char_threshold"]; ok {
117+
if parsedNum, err := strconv.ParseFloat(configCharThreshold, 64); err == nil {
118+
perCharThreshold = parsedNum
119+
}
120+
}
121+
if configTruncate, ok := conf["truncate"]; ok {
122+
if parsedInt, err := strconv.ParseInt(configTruncate, 10, 64); err == nil {
123+
truncateString = parsedInt
124+
}
125+
}
80126
}
127+
81128
return &Credentials{
82-
pattern: regexp.MustCompile(pattern),
129+
pattern: regexp.MustCompile(pattern),
130+
entropyThreshold: entropyThreshold,
131+
perCharThreshold: perCharThreshold,
132+
ignoreEntropy: ignoreEntropy,
133+
truncate: truncateString,
83134
MetaData: gas.MetaData{
84135
What: "Potential hardcoded credentials",
85136
Confidence: gas.Low,

rules/hardcoded_credentials_test.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,51 @@ func TestHardcoded(t *testing.T) {
2525
analyzer := gas.NewAnalyzer(config, nil)
2626
analyzer.AddRule(NewHardcodedCredentials(config))
2727

28+
issues := gasTestRunner(
29+
`
30+
package samples
31+
32+
import "fmt"
33+
34+
func main() {
35+
username := "admin"
36+
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
37+
fmt.Println("Doing something with: ", username, password)
38+
}`, analyzer)
39+
40+
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
41+
}
42+
43+
func TestHardcodedWithEntropy(t *testing.T) {
44+
config := map[string]interface{}{"ignoreNosec": false}
45+
analyzer := gas.NewAnalyzer(config, nil)
46+
analyzer.AddRule(NewHardcodedCredentials(config))
47+
48+
issues := gasTestRunner(
49+
`
50+
package samples
51+
52+
import "fmt"
53+
54+
func main() {
55+
username := "admin"
56+
password := "secret"
57+
fmt.Println("Doing something with: ", username, password)
58+
}`, analyzer)
59+
60+
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
61+
}
62+
63+
func TestHardcodedIgnoreEntropy(t *testing.T) {
64+
config := map[string]interface{}{
65+
"ignoreNosec": false,
66+
"G101": map[string]string{
67+
"ignore_entropy": "true",
68+
},
69+
}
70+
analyzer := gas.NewAnalyzer(config, nil)
71+
analyzer.AddRule(NewHardcodedCredentials(config))
72+
2873
issues := gasTestRunner(
2974
`
3075
package samples
@@ -50,7 +95,7 @@ func TestHardcodedGlobalVar(t *testing.T) {
5095
5196
import "fmt"
5297
53-
var password = "admin"
98+
var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
5499
55100
func main() {
56101
username := "admin"
@@ -70,7 +115,7 @@ func TestHardcodedConstant(t *testing.T) {
70115
71116
import "fmt"
72117
73-
const password = "secret"
118+
const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
74119
75120
func main() {
76121
username := "admin"
@@ -92,7 +137,7 @@ func TestHardcodedConstantMulti(t *testing.T) {
92137
93138
const (
94139
username = "user"
95-
password = "secret"
140+
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
96141
)
97142
98143
func main() {
@@ -110,7 +155,7 @@ func TestHardecodedVarsNotAssigned(t *testing.T) {
110155
package main
111156
var password string
112157
func init() {
113-
password = "this is a secret string"
158+
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
114159
}`, analyzer)
115160
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
116161
}
@@ -140,7 +185,7 @@ func TestHardcodedConstString(t *testing.T) {
140185
package main
141186
142187
const (
143-
ATNStateTokenStart = "foo bar"
188+
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
144189
)
145190
func main() {
146191
println(ATNStateTokenStart)

0 commit comments

Comments
 (0)