Add golang.org/x/tools/go/analysis based linting tool: - fetchml-vet: Custom go vet tool for security invariants Add analyzers for critical security patterns: - noBareDetector: Ensures CreateDetector always captures DetectionInfo (prevents silent metadata loss in GPU detection) - manifestEnv: Validates functions returning Artifacts populate Environment (ensures reproducibility metadata capture) - noInlineCredentials: Detects inline credential patterns in config structs (enforces environment variable references) - hipaaComplete: Validates HIPAA mode configs have all required fields (structural check for compliance completeness) Integration with make lint-custom: - Builds bin/fetchml-vet from tools/fetchml-vet/cmd/fetchml-vet/ - Runs with: go vet -vettool=bin/fetchml-vet ./internal/... Part of: V.4 custom linting from security plan
120 lines
3 KiB
Go
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) (interface{}, 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 == "``"
|
|
}
|