mirror of
https://github.com/securego/gosec.git
synced 2024-12-26 12:35:52 +00:00
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:
parent
a7ec9ccc63
commit
9bc02396e8
2 changed files with 106 additions and 10 deletions
|
@ -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,8 +58,10 @@ 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 {
|
||||||
return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), 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) {
|
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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue