leakhound is a static analysis tool for Go that detects whether sensitive information is being accidentally logged.
Like a bloodhound sniffing out leaks, it tracks down potential data leakage risks in your code.
- Detects if struct fields tagged with
sensitive:"true"are being output by logging functions. - Supports multiple logging packages:
log/slogandfmt. - Zero runtime overhead (static analysis only).
- Can be run automatically in CI/CD pipelines.
go install github.com/nilpoona/leakhound@latestpackage main
import (
"fmt"
"log/slog"
)
type User struct {
ID int
Name string
Password string `sensitive:"true" json:"-"`
APIKey string `sensitive:"true" json:"-"`
Email string `sensitive:"true" json:"email"`
}
type Config struct {
Host string
Port int
Token string `sensitive:"true"`
Database string
}# Inspect the current directory
leakhound ./...
# Inspect a specific package
leakhound ./internal/...leakhound can also detect sensitive fields in nested/embedded structs:
type Config struct {
Secret string `sensitive:"true"`
}
type WrapConfig struct {
Config // Embedded struct with sensitive field
Description string
}
wrapConfig := WrapConfig{...}
// ✅ Both cases will be detected
slog.Info("wrapConfig", wrapConfig) // Detects embedded sensitive fields
slog.Info("secret", wrapConfig.Config.Secret) // Detects nested field accessleakhound uses static analysis rather than runtime masking.
- ✅ Preventative: Find issues at the code review stage.
- ✅ Zero runtime cost: No performance impact during execution.
- ✅ Reliable prevention: Blocks sensitive data before it can be logged.
Currently supported logging libraries:
- ✅
log/slog(Go 1.21+) - ✅
*slog.Loggertype custom loggers - ✅
log(standard log package) - ✅
*log.Loggertype custom loggers - ✅
fmt(Printf, Println, Print, etc.)
Due to the nature of static analysis, there are the following limitations:
// ❌ When passed through a function
func logPassword(p string) {
slog.Info("msg", "pass", p)
fmt.Println("pass:", p)
}
logPassword(user.Password) // Difficult to detect
// ❌ Via reflection
val := reflect.ValueOf(user).FieldByName("Password")
slog.Info("msg", "pass", val.Interface())
fmt.Println(val.Interface())
// ❌ Via an interface
var data interface{} = user.Password
slog.Info("msg", "pass", data)
fmt.Println(data)// ✅ Direct field access
slog.Info("msg", "pass", user.Password)
logger.Info("msg", "pass", user.Password) // logger is *slog.Logger
// ✅ When wrapped by slog.String, etc.
slog.Info("msg", slog.String("pass", user.Password))
// ✅ Via a pointer
userPtr := &user
slog.Info("msg", "pass", userPtr.Password)
// ✅ Entire struct containing sensitive fields
slog.Info("user data", user) // Detects if user has sensitive fields
slog.Info("user data", slog.Any("data", user)) // Also detects in nested function calls
logger.Error("config", config) // *slog.Logger detects struct with sensitive fields
// ✅ All *slog.Logger methods
logger.Debug("msg", "secret", user.Password)
logger.Error("msg", "secret", user.Password)
logger.Warn("msg", "secret", user.Password)
logger.InfoContext(ctx, "msg", "secret", user.Password)
logger.ErrorContext(ctx, "msg", "secret", user.Password)
logger.WarnContext(ctx, "msg", "secret", user.Password)
logger.DebugContext(ctx, "msg", "secret", user.Password)
logger.Log(ctx, slog.LevelInfo, "msg", "secret", user.Password)
logger.LogAttrs(ctx, slog.LevelInfo, "msg", slog.String("pass", user.Password))
// ✅ With method chaining (edge case)
logger.With("key", "val").Info("config", config) // Detects even after With()
// ✅ Nested/embedded structs with sensitive fields
type WrapConfig struct {
Config // Embedded struct with sensitive field
}
wrapConfig := WrapConfig{...}
slog.Info("wrapConfig", wrapConfig) // Detects embedded sensitive fields
slog.Info("secret", wrapConfig.Config.Secret) // Detects nested field access// ✅ Direct field access
log.Print("secret:", user.Password)
log.Printf("secret: %s", user.Password)
log.Println("secret:", user.Password)
customLogger.Print("token:", config.Token) // customLogger is *log.Logger
// ✅ All log package functions
log.Fatal("secret:", user.Password)
log.Fatalf("secret: %s", user.Password)
log.Fatalln("secret:", user.Password)
log.Panic("secret:", user.Password)
log.Panicf("secret: %s", user.Password)
log.Panicln("secret:", user.Password)
// ✅ Entire struct containing sensitive fields
log.Print("config:", config) // Detects if config has sensitive fields
log.Printf("config: %+v", config) // Detects with format verbs
customLogger.Println("user:", user) // *log.Logger detects struct with sensitive fields
// ✅ All *log.Logger methods
customLogger.Fatal("secret:", user.Password)
customLogger.Fatalf("secret: %s", user.Password)
customLogger.Fatalln("secret:", user.Password)
customLogger.Panic("secret:", user.Password)
customLogger.Panicf("secret: %s", user.Password)
customLogger.Panicln("secret:", user.Password)
customLogger.Output(2, user.Password)
// ✅ Nested/embedded structs with sensitive fields
type WrapConfig struct {
Config // Embedded struct with sensitive field
}
wrapConfig := WrapConfig{...}
log.Print("wrapConfig:", wrapConfig) // Detects embedded sensitive fields
log.Println("secret:", wrapConfig.Config.Secret) // Detects nested field access// ✅ Direct field access
fmt.Println(user.Password)
fmt.Printf("password: %s", user.Password)
fmt.Print("token:", config.Token)
// ✅ Via a pointer
userPtr := &user
fmt.Println(userPtr.Password)
// ✅ Entire struct containing sensitive fields
fmt.Println(user) // Detects if user has sensitive fields
fmt.Printf("%+v", user) // Detects with format verbs
fmt.Printf("%#v", config) // Detects with any format
// ✅ Multiple arguments
fmt.Println("User:", user.Name, "Pass:", user.Password) // Detects Password
// ✅ Nested/embedded structs with sensitive fields
type WrapConfig struct {
Config // Embedded struct with sensitive field
}
wrapConfig := WrapConfig{...}
fmt.Println("wrapConfig:", wrapConfig) // Detects embedded sensitive fields
fmt.Printf("secret: %s", wrapConfig.Config.Secret) // Detects nested field access$ leakhound ./...
./main.go:15:2: sensitive field "Password" should not be logged
./main.go:18:2: sensitive field "APIKey" should not be logged
./config.go:23:12: sensitive field "Token" should not be logged
./user.go:10:14: struct "User" contains sensitive fields and should not be loggedMIT License