fetch_ml/tools/fetchml-vet/analyzers/noinlinecredentials.go
Jeremie Fraeys a49e8f593c
chore(tools): update fetchml-vet analyzers
Analyzer improvements:
- hipaacomplete.go: refined HIPAA compliance checks
- manifestenv.go: environment variable validation in manifests
- nobaredetector.go: detection of bare credential exposures
- noinlinecredentials.go: inline credential scanning improvements
2026-03-12 12:09:34 -04:00

120 lines
3 KiB
Go

package analyzers
import (
"go/ast"
"go/token"
"strings"
"golang.org/x/tools/go/analysis"
)
// NoInlineCredentialsAnalyzer flags struct literals of type worker.Config where
// sensitive fields (RedisPassword, SecretKey, AccessKey) are set to non-empty string
// literals instead of environment variable references. Credentials must not appear
// in source or config files.
var NoInlineCredentialsAnalyzer = &analysis.Analyzer{
Name: "noinlinecreds",
Doc: "flags inline credentials in Config struct literals",
Run: runNoInlineCredentials,
}
// sensitiveCredentialFields lists field names that should never have inline string literals
var sensitiveCredentialFields = []string{
"RedisPassword",
"SecretKey",
"AccessKey",
"Password",
"APIKey",
"Token",
"PrivateKey",
}
func runNoInlineCredentials(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
// Look for composite literals (struct initialization)
composite, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
// Check if this is a Config type
typeInfo := pass.TypesInfo.TypeOf(composite)
if typeInfo == nil {
return true
}
typeStr := typeInfo.String()
// Match worker.Config or Config types
if !strings.Contains(typeStr, "Config") {
return true
}
// Check each field in the composite literal
for _, elt := range composite.Elts {
kv, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := kv.Key.(*ast.Ident)
if !ok {
continue
}
// Check if this is a sensitive field
if !isSensitiveField(key.Name) {
continue
}
// Check if the value is a string literal (not env var or function call)
if isInlineStringLiteral(kv.Value) {
// Check if it's not empty (empty strings might be intentional)
if !isEmptyStringLiteral(kv.Value) {
pass.Reportf(kv.Value.Pos(),
"inline credential detected in field %s - use environment variables instead (e.g., os.Getenv)",
key.Name)
}
}
}
return true
})
}
return nil, nil
}
// isSensitiveField checks if a field name is in the sensitive credentials list
func isSensitiveField(name string) bool {
for _, sensitive := range sensitiveCredentialFields {
if name == sensitive {
return true
}
}
return false
}
// isInlineStringLiteral checks if an expression is a string literal (not env var ref)
func isInlineStringLiteral(expr ast.Expr) bool {
switch e := expr.(type) {
case *ast.BasicLit:
// String literal like "password123"
return e.Kind == token.STRING
case *ast.BinaryExpr:
// String concatenation like "prefix" + "suffix"
if e.Op == token.ADD {
return isInlineStringLiteral(e.X) || isInlineStringLiteral(e.Y)
}
}
return false
}
// isEmptyStringLiteral checks if an expression is an empty string literal
func isEmptyStringLiteral(expr ast.Expr) bool {
lit, ok := expr.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return false
}
// Remove quotes and check if empty
return lit.Value == `""` || lit.Value == "``"
}