Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Static analyzer that sniffs out sensitive data leaks in Go logs

nilpoona/leakhound

Repository files navigation

leakhound 🐕

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.

Features

  • Detects if struct fields tagged with sensitive:"true" are being output by logging functions.
  • Supports multiple logging packages: log/slog and fmt.
  • Zero runtime overhead (static analysis only).
  • Can be run automatically in CI/CD pipelines.

Installation

As a CLI tool

go install github.com/nilpoona/leakhound@latest

Usage

1. Tag sensitive fields

package 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
}

2. Run static analysis

Run as a CLI tool

# Inspect the current directory
leakhound ./...

# Inspect a specific package
leakhound ./internal/...

3. Nested struct support

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 access

Design Philosophy

Why static analysis?

leakhound uses static analysis rather than runtime masking.

Advantages of Static Analysis

  • 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.

Supported Logging Libraries

Currently supported logging libraries:

  • log/slog (Go 1.21+)
  • *slog.Logger type custom loggers
  • log (standard log package)
  • *log.Logger type custom loggers
  • fmt (Printf, Println, Print, etc.)

Limitations

Due to the nature of static analysis, there are the following limitations:

Cases that cannot be detected

// ❌ 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)

Cases that can be detected

slog package (including *slog.Logger type)

// ✅ 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

log package (including *log.Logger type)

// ✅ 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

fmt package

// ✅ 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

Example Detection Output

$ 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 logged

License

MIT License

About

Static analyzer that sniffs out sensitive data leaks in Go logs

Resources

Stars

Watchers

Forks

Packages

No packages published