Skip to content

Commit

Permalink
Add log.MaskingLogger and log.Masker (#14)
Browse files Browse the repository at this point in the history
Add log.MaskingLogger and log.Masker

---------

Co-authored-by: Sergey Bolzhatov <[email protected]>
  • Loading branch information
lVlayhem and Sergey Bolzhatov authored Oct 21, 2024
1 parent e03f3db commit d4c7a80
Show file tree
Hide file tree
Showing 8 changed files with 928 additions and 2 deletions.
55 changes: 54 additions & 1 deletion log/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
cfgKeyAddCaller = "log.addCaller"
cfgKeyErrorNoVerbose = "log.error.noVerbose"
cfgKeyErrorVerboseSuffix = "log.error.verboseSuffix"
cfgKeyMasking = "log.masking"
)

// Default and restriction values.
Expand Down Expand Up @@ -61,6 +62,8 @@ type Config struct {
// {"level":"info","time":"...","msg":"starting application HTTP server...","caller":"httpserver/http_server.go:98","address":":8888"}
AddCaller bool

Masking MaskingConfig

keyPrefix string
}

Expand Down Expand Up @@ -139,6 +142,7 @@ func (c *Config) SetProviderDefaults(dp config.DataProvider) {
dp.SetDefault(cfgKeyFileRotationMaxBackups, DefaultFileRotationMaxBackups)
dp.SetDefault(cfgKeyErrorNoVerbose, false)
dp.SetDefault(cfgKeyErrorVerboseSuffix, "_verbose")
c.Masking.SetProviderDefaults(config.NewKeyPrefixedDataProvider(dp, cfgKeyMasking))
}

var (
Expand Down Expand Up @@ -187,7 +191,9 @@ func (c *Config) Set(dp config.DataProvider) error {
if c.ErrorVerboseSuffix, err = dp.GetString(cfgKeyErrorVerboseSuffix); err != nil {
return err
}

if err := c.Masking.Set(config.NewKeyPrefixedDataProvider(dp, cfgKeyMasking)); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -235,3 +241,50 @@ func (c *Config) setFileOutputConfig(dp config.DataProvider) error {

return nil
}

type MaskingConfig struct {
Enabled bool
UseDefaultRules bool
Rules []MaskingRuleConfig
}

type MaskingRuleConfig struct {
Field string `mapstructure:"field"`
Formats []FieldMaskFormat `mapstructure:"formats"`
Masks []MaskConfig `mapstructure:"masks"`
}

type MaskConfig struct {
RegExp string `mapstructure:"regexp"`
Mask string `mapstructure:"mask"`
}

type FieldMaskFormat string

const (
FieldMaskFormatHTTPHeader FieldMaskFormat = "http_header"
FieldMaskFormatJSON FieldMaskFormat = "json"
FieldMaskFormatURLEncoded FieldMaskFormat = "urlencoded"

cfgKeyMaskingEnabled = "enabled"
cfgKeyMaskingUseDefaultRules = "useDefaultRules"
cfgKeyMaskingRules = "rules"
)

func (c *MaskingConfig) SetProviderDefaults(dp config.DataProvider) {
dp.SetDefault(cfgKeyMaskingUseDefaultRules, true)
}

// Set sets logger configuration values from config.DataProvider.
func (c *MaskingConfig) Set(dp config.DataProvider) (err error) {
if c.Enabled, err = dp.GetBool(cfgKeyMaskingEnabled); err != nil {
return err
}
if c.UseDefaultRules, err = dp.GetBool(cfgKeyMaskingUseDefaultRules); err != nil {
return err
}
if err := dp.UnmarshalKey(cfgKeyMaskingRules, &c.Rules); err != nil {
return err
}
return nil
}
69 changes: 69 additions & 0 deletions log/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ log:
require.Equal(t, 42, cfg.File.Rotation.MaxBackups)
require.True(t, cfg.File.Rotation.Compress)
require.True(t, cfg.AddCaller)
require.False(t, cfg.Masking.Enabled)
require.True(t, cfg.Masking.UseDefaultRules)
require.Nil(t, cfg.Masking.Rules)
})

t.Run("errors", func(t *testing.T) {
Expand Down Expand Up @@ -102,3 +105,69 @@ log:
require.EqualError(t, err, `log.file.path: cannot be empty when "file" output is used`)
})
}

func TestMaskingConfig(t *testing.T) {
for _, tc := range []struct {
name string
cfg string
masking MaskingConfig
}{
{
name: "enable default",
cfg: `
log:
masking:
enabled: true`,
masking: MaskingConfig{Enabled: true, UseDefaultRules: true, Rules: nil},
},
{
name: "default with custom rules",
cfg: `
log:
masking:
enabled: true
useDefaultRules: true
rules:
- field: "api_key"
formats: ["http_header", "json", "urlencoded"]`,
masking: MaskingConfig{
Enabled: true, UseDefaultRules: true, Rules: []MaskingRuleConfig{
{
Field: "api_key",
Formats: []FieldMaskFormat{FieldMaskFormatHTTPHeader, FieldMaskFormatJSON, FieldMaskFormatURLEncoded},
},
},
},
},
{
name: "ultimate",
cfg: `
log:
masking:
enabled: true
useDefaultRules: false
rules:
- field: "api_key"
formats: ["http_header", "json", "urlencoded"]
masks:
- regexp: "<api_key>.+?</api_key>"
mask: "<api_key>***</api_key>"`,
masking: MaskingConfig{
Enabled: true, UseDefaultRules: false, Rules: []MaskingRuleConfig{
{
Field: "api_key",
Formats: []FieldMaskFormat{FieldMaskFormatHTTPHeader, FieldMaskFormatJSON, FieldMaskFormatURLEncoded},
Masks: []MaskConfig{{RegExp: "<api_key>.+?</api_key>", Mask: "<api_key>***</api_key>"}},
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := Config{}
err := config.NewDefaultLoader("").LoadFromReader(bytes.NewBuffer([]byte(tc.cfg)), config.DataTypeYAML, &cfg)
require.NoError(t, err)
require.Equal(t, tc.masking, cfg.Masking)
})
}
}
11 changes: 10 additions & 1 deletion log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,16 @@ func NewLogger(cfg *Config) (FieldLogger, CloseFunc) {
// to receive log line not in this file
logfLogger = logfLogger.WithCaller().WithCallerSkip(1)
}
return &LogfAdapter{logfLogger}, CloseFunc(closeFunc)
var logger FieldLogger = &LogfAdapter{logfLogger}

if cfg.Masking.Enabled {
rules := cfg.Masking.Rules
if cfg.Masking.UseDefaultRules {
rules = append(rules, DefaultMasks...)
}
logger = NewMaskingLogger(logger, NewMasker(rules))
}
return logger, CloseFunc(closeFunc)
}

// With returns a new logger with the given additional fields.
Expand Down
136 changes: 136 additions & 0 deletions log/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"fmt"
"io"
"os"
"regexp"
"testing"

"github.com/ssgreg/logf"
Expand Down Expand Up @@ -124,3 +125,138 @@ func TestTextFormat(t *testing.T) {
require.Contains(t, buf.String(), `error="some error"`)
require.Contains(t, buf.String(), fmt.Sprintf(`pid=%d`, os.Getpid()))
}

func TestLoggerWithMasking(t *testing.T) {
logger, closer := NewLogger(&Config{
Masking: MaskingConfig{
Enabled: true, UseDefaultRules: true, Rules: []MaskingRuleConfig{
{
Field: "api_key",
Formats: []FieldMaskFormat{FieldMaskFormatHTTPHeader, FieldMaskFormatJSON, FieldMaskFormatURLEncoded},
Masks: []MaskConfig{{RegExp: "<api_key>.+?</api_key>", Mask: "<api_key>***</api_key>"}},
},
},
},
})
defer closer()

mLogger, ok := logger.(MaskingLogger)
require.True(t, ok)

require.IsType(t, &LogfAdapter{}, mLogger.log)

masker, ok := mLogger.masker.(*Masker)
require.True(t, ok)

expectedMasks := []FieldMasker{
{
Field: "api_key",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`<api_key>.+?</api_key>`),
Mask: "<api_key>***</api_key>",
},
{
RegExp: regexp.MustCompile(`(?i)api_key: .+?\r\n`),
Mask: "api_key: ***\r\n",
},
{
RegExp: regexp.MustCompile(`(?i)"api_key"\s*:\s*".*?[^\\]"`),
Mask: `"api_key": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)api_key\s*=\s*[^&\s]+`),
Mask: "api_key=***",
},
},
},
{
Field: "authorization",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)Authorization: .+?\r\n`),
Mask: "Authorization: ***\r\n",
},
},
},
{
Field: "password",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"password"\s*:\s*".*?[^\\]"`),
Mask: `"password": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)password\s*=\s*[^&\s]+`),
Mask: "password=***",
},
},
},
{
Field: "client_secret",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"client_secret"\s*:\s*".*?[^\\]"`),
Mask: `"client_secret": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)client_secret\s*=\s*[^&\s]+`),
Mask: "client_secret=***",
},
},
},
{
Field: "access_token",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"access_token"\s*:\s*".*?[^\\]"`),
Mask: `"access_token": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)access_token\s*=\s*[^&\s]+`),
Mask: "access_token=***",
},
},
},
{
Field: "refresh_token",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"refresh_token"\s*:\s*".*?[^\\]"`),
Mask: `"refresh_token": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)refresh_token\s*=\s*[^&\s]+`),
Mask: "refresh_token=***",
},
},
},
{
Field: "id_token",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"id_token"\s*:\s*".*?[^\\]"`),
Mask: `"id_token": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)id_token\s*=\s*[^&\s]+`),
Mask: "id_token=***",
},
},
},
{
Field: "assertion",
Masks: []Mask{
{
RegExp: regexp.MustCompile(`(?i)"assertion"\s*:\s*".*?[^\\]"`),
Mask: `"assertion": "***"`,
},
{
RegExp: regexp.MustCompile(`(?i)assertion\s*=\s*[^&\s]+`),
Mask: "assertion=***",
},
},
},
}
require.Equal(t, expectedMasks, masker.FieldMasks)
}
Loading

0 comments on commit d4c7a80

Please sign in to comment.