masq

package module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 6, 2024 License: Apache-2.0 Imports: 7 Imported by: 9

README

masq: redacting sensitive data in slog Go Reference test gosec trivy

masq is a redacting utility to conceal sensitive data for slog that is official Go structured logging library. The concealing feature reduce risk to store secret values (API token, password and such things) and sensitive data like PII (Personal Identifiable Information) such as address, phone number, email address and etc into logging storage.

u := struct {
    ID    string
    Email EmailAddr
}{
    ID:    "u123",
    Email: "mizutani@hey.com",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
        },
    ),
)

logger.Info("hello", slog.Any("user", u))

Then, output is following (jq formatted).

{
  "time": "2022-12-25T09:00:00.123456789",
  "level": "INFO",
  "msg": "hello",
  "user": {
    "ID": "u123",
    "Email": "[FILTERED]" // <- Concealed
  }
}

Usage

masq.New() provides a function for ReplaceAttr of slog.HandlerOptions. masq.New can specify one or multiple masq.Option to identify value and field to be concealed.

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
		&slog.HandlerOptions{
            ReplaceAttr: masq.New(
                // By user defined custom type
                masq.WithType[AccessToken](),
        
                // By regex of phone number as e164 format
                masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{1,14}$`)),
        
                // By field tag such as masq:"secret"
                masq.WithTag("secret"),
        
                // By by field name prefix. Concealing SecureXxx field
                masq.WithFieldPrefix("Secure"),
            ),
        },
    ),
)
With custom type
type password string
type myRecord struct {
    ID       string
    Password password
}
record := myRecord{
    ID:       "m-mizutani",
    Password: "abcd1234",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
		&slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithType[password]()),
        },
    ),
)

logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Password":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}
With fixed string
const issuedToken = "abcd1234"
authHeader := "Authorization: Bearer " + issuedToken

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithContain("abcd1234")),
        },
    ),
)

logger.With("auth", authHeader).Info("send header")
out.Flush()
// Output:
// {"auth":"[REDACTED]","level":"INFO","msg":"send header","time":"2022-12-25T09:00:00.123456789"}

With regex

type myRecord struct {
    ID    string
    Phone string
}
record := myRecord{
    ID:    "m-mizutani",
    Phone: "090-0000-0000",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithRegex(regexp.MustCompile(`^\d{3}-\d{4}-\d{4}$`)),
        },
    ),
)

logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}
With struct field tag
type myRecord struct {
    ID    string
    EMail string `masq:"secret"`
}
record := myRecord{
    ID:    "m-mizutani",
    EMail: "mizutani@hey.com",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithTag("secret")),
        },
    ),
)

logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"EMail":"[FILTERED]","ID":"m-mizutani"},"time":"2022-12-25T09:00:00.123456789"}
With struct field name
type myRecord struct {
    ID    string
    Phone string
}
record := myRecord{
    ID:    "m-mizutani",
    Phone: "090-0000-0000",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithFieldName("Phone")),
        },
    ),
)

logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}
With struct field prefix
type myRecord struct {
    ID          string
    SecurePhone string
}
record := myRecord{
    ID:          "m-mizutani",
    SecurePhone: "090-0000-0000",
}

logger := slog.New(
    slog.NewJSONHandler(
        os.Stdout,
        &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithFieldPrefix("Secure")),
        },
    ),
)

logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","SecurePhone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}

License

Apache License v2.0

Documentation

Overview

Example
package main

import (
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

type EmailAddr string

func main() {
	u := struct {
		ID    string
		Email EmailAddr
	}{
		ID:    "u123",
		Email: "mizutani@hey.com",
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
	}))

	logger.Info("hello", slog.Any("user", u))
}
Output:

Index

Examples

Constants

View Source
const (
	DefaultRedactMessage = "[REDACTED]"
)

Variables

This section is empty.

Functions

func New

func New(options ...Option) func(groups []string, a slog.Attr) slog.Attr

Types

type Censor added in v0.1.4

type Censor func(fieldName string, value any, tag string) bool

Censor is a function to check if the field should be redacted. It receives field name, value, and tag of struct if the value is in struct. If the field should be redacted, it returns true.

type Censors added in v0.1.4

type Censors []Censor

func (Censors) ShouldRedact added in v0.1.4

func (x Censors) ShouldRedact(fieldName string, value any, tag string) bool

type Filter

type Filter struct {
	// contains filtered or unexported fields
}

type Option

type Option func(m *masq)

func WithAllowedType

func WithAllowedType(types ...reflect.Type) Option

WithAllowedType is an option to allow the type to be redacted. If the field is matched with the target type, the field will not be redacted.

func WithCensor added in v0.1.4

func WithCensor(censor Censor, redactors ...Redactor) Option

WithCensor is an option to add a censor function to masq. If the censor function returns true, the field will be redacted. The redactor functions will be applied to the field. If the redactor functions return true, the redaction will be stopped. If the all redactor functions return false, the default redactor will be applied. The default redactor redacts the field with the redact message.

func WithContain added in v0.1.4

func WithContain(target string, redactors ...Redactor) Option

WithContain is an option to check if the field contains the target string. If the field contains the target string, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	const issuedToken = "abcd1234"
	authHeader := "Authorization: Bearer " + issuedToken

	logger := newLogger(out, masq.New(masq.WithContain("abcd1234")))

	logger.With("auth", authHeader).Info("send header")
	out.Flush()
}
Output:

{"auth":"[REDACTED]","level":"INFO","msg":"send header","time":"2022-12-25T09:00:00.123456789"}

func WithFieldName

func WithFieldName(fieldName string, redactors ...Redactor) Option

WithFieldName is an option to check if the field name is matched with the target field name. If the field name is the target field name, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID    string
		Phone string
	}
	record := myRecord{
		ID:    "m-mizutani",
		Phone: "090-0000-0000",
	}
	logger := newLogger(out, masq.New(masq.WithFieldName("Phone")))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[REDACTED]"},"time":"2022-12-25T09:00:00.123456789"}

func WithFieldPrefix

func WithFieldPrefix(fieldName string, redactors ...Redactor) Option

WithFieldPrefix is an option to check if the field name has the target prefix. If the field name has the target prefix, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID          string
		SecurePhone string
	}
	record := myRecord{
		ID:          "m-mizutani",
		SecurePhone: "090-0000-0000",
	}

	logger := newLogger(out, masq.New(masq.WithFieldPrefix("Secure")))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","SecurePhone":"[REDACTED]"},"time":"2022-12-25T09:00:00.123456789"}

func WithRedactMessage added in v0.1.1

func WithRedactMessage(message string) Option

WithRedactMessage is an option to set the redact message. The default redact message is `[REDACTED]`.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID    string
		Phone string
	}
	record := myRecord{
		ID:    "m-mizutani",
		Phone: "090-0000-0000",
	}

	logger := newLogger(out, masq.New(
		masq.WithFieldName("Phone"),
		masq.WithRedactMessage("****"),
	))
	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"****"},"time":"2022-12-25T09:00:00.123456789"}

func WithRegex

func WithRegex(target *regexp.Regexp, redactors ...Redactor) Option

WithRegex is an option to check if the field matches the target regex. If the field matches the target regex, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"
	"regexp"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID    string
		Phone string
	}
	record := myRecord{
		ID:    "m-mizutani",
		Phone: "090-0000-0000",
	}

	logger := newLogger(out, masq.New(
		masq.WithRegex(regexp.MustCompile(`^\d{3}-\d{4}-\d{4}$`)),
	))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[REDACTED]"},"time":"2022-12-25T09:00:00.123456789"}

func WithTag

func WithTag(tag string, redactors ...Redactor) Option

WithTag is an option to check if the field is matched with the target struct tag in `masq:"xxx"`. If the field has the target tag, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID    string
		EMail string `masq:"secret"`
	}
	record := myRecord{
		ID:    "m-mizutani",
		EMail: "mizutani@hey.com",
	}

	logger := newLogger(out, masq.New(masq.WithTag("secret")))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"EMail":"[REDACTED]","ID":"m-mizutani"},"time":"2022-12-25T09:00:00.123456789"}

func WithType

func WithType[T any](redactors ...Redactor) Option

WithType is an option to check if the field is matched with the target type. If the field is the target type, the field will be redacted.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type password string
	type myRecord struct {
		ID       string
		Password password
	}
	record := myRecord{
		ID:       "m-mizutani",
		Password: "abcd1234",
	}

	logger := newLogger(out, masq.New(masq.WithType[password]()))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Password":"[REDACTED]"},"time":"2022-12-25T09:00:00.123456789"}

type Redactor added in v0.1.4

type Redactor func(src, dst reflect.Value) bool

Redactor is a function to redact value. It receives source and destination value. If the redaction is done, it must return true. If the redaction is not done, it must return false. If the redaction is not done, the next redactor will be applied. If all redactors are not done, the default redactor will be applied.

func MaskWithSymbol added in v0.1.4

func MaskWithSymbol(symbol rune, max int) Redactor

MaskWithSymbol is a redactor to redact string value with masked string that have the same length as the source string value. It can help the developer to know the length of the string value. The returned Redact function always returns true if the source value is string. Otherwise, it returns false.

Example
out := &fixedTimeWriter{}

type myRecord struct {
	ID    string
	Phone string
	Email string
}
record := myRecord{
	ID:    "m-mizutani",
	Phone: "090-0000-0000",
	// too long email address
	Email: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@example.com",
}

logger := newLogger(out, masq.New(
	masq.WithFieldName("Phone", masq.MaskWithSymbol('*', 32)),
	masq.WithFieldName("Email", masq.MaskWithSymbol('*', 12)),
))
logger.With("record", record).Info("Got record")
out.Flush()
Output:

{"level":"INFO","msg":"Got record","record":{"Email":"************ (remained 36 chars)","ID":"m-mizutani","Phone":"*************"},"time":"2022-12-25T09:00:00.123456789"}

func RedactString added in v0.1.4

func RedactString(redact func(s string) string) Redactor

RedactString is a redactor to redact string value. It receives a function to redact string. The function receives the string value and returns the redacted string value. The returned Redact function always returns true if the source value is string. Otherwise, it returns false.

Example
package main

import (
	"encoding/json"
	"io"
	"os"

	"log/slog"

	"github.com/m-mizutani/masq"
)

func newLogger(w io.Writer, f func(groups []string, a slog.Attr) slog.Attr) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: f,
	}))
}

type fixedTimeWriter struct {
	buf []byte
}

func (x *fixedTimeWriter) Write(p []byte) (n int, err error) {
	x.buf = append(x.buf, p...)
	return len(p), nil
}

func (x *fixedTimeWriter) Flush() {
	var m map[string]any
	if err := json.Unmarshal(x.buf, &m); err != nil {
		panic("failed to unmarshal")
	}
	m["time"] = "2022-12-25T09:00:00.123456789"

	raw, err := json.Marshal(m)
	if err != nil {
		panic("failed to marshal")
	}

	if _, err := os.Stdout.Write(raw); err != nil {
		panic("can not output")
	}
}

func main() {
	out := &fixedTimeWriter{}

	type myRecord struct {
		ID    string
		Phone string
		Email string
	}
	record := myRecord{
		ID:    "m-mizutani",
		Phone: "090-0000-1234",
		Email: "mizutani@hey.com",
	}

	logger := newLogger(out, masq.New(
		// custom redactor
		masq.WithFieldName("Phone",
			masq.RedactString(func(s string) string {
				return "****-" + s[len(s)-4:]
			}),
		),
		// default redactor
		masq.WithFieldName("Email"),
	))

	logger.With("record", record).Info("Got record")
	out.Flush()
}
Output:

{"level":"INFO","msg":"Got record","record":{"Email":"[REDACTED]","ID":"m-mizutani","Phone":"****-1234"},"time":"2022-12-25T09:00:00.123456789"}

type Redactors added in v0.1.4

type Redactors []Redactor

func (Redactors) Redact added in v0.1.4

func (x Redactors) Redact(src, dst reflect.Value) bool

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL