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 == "``" }