diff --git a/redact_test.go b/redact_test.go index e29f8e5..027e321 100644 --- a/redact_test.go +++ b/redact_test.go @@ -1,13 +1,29 @@ package logger_test import ( + "reflect" "strings" + "testing" "github.com/gildas/go-core" "github.com/gildas/go-logger" + "github.com/stretchr/testify/suite" ) -func (suite *LoggerSuite) TestCanRedactSensitiveStruct() { +type RedactSuite struct { + LoggerSuite + Name string +} + +func TestRedactSuite(t *testing.T) { + suite.Run(t, new(RedactSuite)) +} + +func (suite *RedactSuite) SetupSuite() { + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") +} + +func (suite *RedactSuite) TestCanRedactSensitiveStruct() { customer := User{"12345678", "John Doe", nil} output := CaptureStdout(func() { log := logger.Create("test", &logger.StdoutStream{Unbuffered: true}) @@ -28,7 +44,7 @@ func (suite *LoggerSuite) TestCanRedactSensitiveStruct() { }) } -func (suite *LoggerSuite) TestCanRedactMessage() { +func (suite *RedactSuite) TestCanRedactMessage() { redactor := core.Must(logger.NewRedactor(`\+[0-9]{11}`)) suite.Require().NotEmpty(redactor.String()) output := CaptureStdout(func() { @@ -53,7 +69,7 @@ func (suite *LoggerSuite) TestCanRedactMessage() { }) } -func (suite *LoggerSuite) TestShouldNotRedactMessageWithNoMatch() { +func (suite *RedactSuite) TestShouldNotRedactMessageWithNoMatch() { redactor := core.Must(logger.NewRedactor(`\+[0-9]{23}`)) suite.Require().NotEmpty(redactor.String()) output := CaptureStdout(func() { @@ -78,7 +94,7 @@ func (suite *LoggerSuite) TestShouldNotRedactMessageWithNoMatch() { }) } -func (suite *LoggerSuite) TestCanRedactMessageWithSeveralRedactors() { +func (suite *RedactSuite) TestCanRedactMessageWithSeveralRedactors() { output := CaptureStdout(func() { log := logger.Create( "test", @@ -116,7 +132,7 @@ func (suite *LoggerSuite) TestCanRedactMessageWithSeveralRedactors() { }) } -func (suite *LoggerSuite) TestCanRedactAsString() { +func (suite *RedactSuite) TestCanRedactAsString() { suite.Assert().Equal("", logger.Redact(nil)) suite.Assert().Equal("", logger.Redact("")) redacted := logger.Redact("John Doe") @@ -128,67 +144,113 @@ func (suite *LoggerSuite) TestCanRedactAsString() { suite.Assert().Equal("Name-6cea57c2fb", logger.RedactWithPrefixedHash("Name", "John Doe")) } -func (suite *LoggerSuite) TestCanRedactCreditCardCard() { +func (suite *RedactSuite) TestCanRedactCreditCardCard() { redactor := logger.CreditCardRedactor redacted, ok := redactor.Redact("message with 30569309025904") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3056 9309 0259 04") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 6011 0009 9013 9424") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 5105 1051 0510 5100") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3569 9900 1009 5841") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactAMEXCard() { +func (suite *RedactSuite) TestCanRedactAMEXCard() { redactor := logger.AMEXRedactor redacted, ok := redactor.Redact("message with 370000000100018") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactVISACard() { +func (suite *RedactSuite) TestCanRedactVISACard() { redactor := logger.VISARedactor redacted, ok := redactor.Redact("message with 4111111111111111") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactDinersClubCard() { +func (suite *RedactSuite) TestCanRedactDinersClubCard() { redactor := logger.DinersClubRedactor redacted, ok := redactor.Redact("message with 30569309025904") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3056 9309 0259 04") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactDiscoverCard() { +func (suite *RedactSuite) TestCanRedactDiscoverCard() { redactor := logger.DiscoverRedactor redacted, ok := redactor.Redact("message with 6011000990139424") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 6011 0009 9013 9424") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactMasterCardCard() { +func (suite *RedactSuite) TestCanRedactMasterCardCard() { redactor := logger.MasterCardRedactor redacted, ok := redactor.Redact("message with 5105105105105100") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 5105 1051 0510 5100") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with nothing") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with nothing", redacted) +} + +func (suite *RedactSuite) TestCanRedactJCBCard() { + redactor := logger.JCBRedactor + redacted, ok := redactor.Redact("message with 3569990010095841") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 3569 9900 1009 5841") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) redacted, ok = redactor.Redact("message with nothing") suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactPhone() { +func (suite *RedactSuite) TestCanRedactPhone() { redactor := logger.PhoneRedactor redacted, ok := redactor.Redact("message with +13178723000") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -201,7 +263,7 @@ func (suite *LoggerSuite) TestCanRedactPhone() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactEmail() { +func (suite *RedactSuite) TestCanRedactEmail() { redactor := logger.EmailRedactor redacted, ok := redactor.Redact("message with john.doe@acme.com") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -211,7 +273,7 @@ func (suite *LoggerSuite) TestCanRedactEmail() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactIP() { +func (suite *RedactSuite) TestCanRedactIP() { redactor := logger.IPRedactor redacted, ok := redactor.Redact("message with 192.168.1.1") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -224,7 +286,7 @@ func (suite *LoggerSuite) TestCanRedactIP() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactIPV4() { +func (suite *RedactSuite) TestCanRedactIPV4() { redactor := logger.IPV4Redactor redacted, ok := redactor.Redact("message with 192.168.1.1") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -234,7 +296,7 @@ func (suite *LoggerSuite) TestCanRedactIPV4() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactIPV6() { +func (suite *RedactSuite) TestCanRedactIPV6() { redactor := logger.IPV6Redactor redacted, ok := redactor.Redact("message with 2600:::") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -250,7 +312,7 @@ func (suite *LoggerSuite) TestCanRedactIPV6() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactMAC() { +func (suite *RedactSuite) TestCanRedactMAC() { redactor := logger.MACRedactor redacted, ok := redactor.Redact("message with 2C:54:91:88:C9:E3") suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) @@ -260,7 +322,139 @@ func (suite *LoggerSuite) TestCanRedactMAC() { suite.Assert().Equal("message with nothing", redacted) } -func (suite *LoggerSuite) TestCanRedactWithKeysToRedact() { +func (suite *RedactSuite) TestCanMergeEmpty() { + suite.Assert().Equal(logger.VISARedactor.String(), logger.VISARedactor.Merge().String()) +} + +func (suite *RedactSuite) TestCanMergeWithRedactor() { + expected := `3[47]\d{2}[- ]*\d{6}[- ]*\d{5}|4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}` + redactor := logger.AMEXRedactor.Merge(logger.VISARedactor) + suite.Assert().Equal(expected, redactor.String()) + redacted, ok := redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 5105 1051 0510 5100") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with 5105 1051 0510 5100", redacted) + redacted, ok = redactor.Redact("message with nothing") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with nothing", redacted) +} + +func (suite *RedactSuite) TestCanMergeWithRedactors() { + expected := `3[47]\d{2}[- ]*\d{6}[- ]*\d{5}|4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}` + redactor := logger.AMEXRedactor.Merge(logger.VISARedactor, logger.MasterCardRedactor) + suite.Assert().Equal(expected, redactor.String()) + redacted, ok := redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 5105 1051 0510 5100") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with nothing") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with nothing", redacted) +} + +func (suite *RedactSuite) TestCanMergeWithPipedRedactors() { + expected := `3[47]\d{2}[- ]*\d{6}[- ]*\d{5}|4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}` + redactor := logger.AMEXRedactor.Merge(logger.VISARedactor).Merge(logger.MasterCardRedactor) + suite.Assert().Equal(expected, redactor.String()) + redacted, ok := redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 5105 1051 0510 5100") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with nothing") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with nothing", redacted) +} + +func (suite *RedactSuite) TestCanMergeWithString() { + expected := `3[47]\d{2}[- ]*\d{6}[- ]*\d{5}|4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}` + redactor := logger.AMEXRedactor.Merge(logger.VISARedactor.String()) + suite.Assert().Equal(expected, redactor.String()) + redacted, ok := redactor.Redact("message with 3700 000001 00018") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with 4111 1111 1111 1111") + suite.Assert().Truef(ok, "Redactor %T should have matched", redactor) + suite.Assert().Equal("message with REDACTED", redacted) + redacted, ok = redactor.Redact("message with nothing") + suite.Assert().Falsef(ok, "Redactor %T should not have matched", redactor) + suite.Assert().Equal("message with nothing", redacted) +} + + type stringer logger.Redactor + func (s stringer) String() string { + return logger.Redactor(s).String() + } + +func (suite *RedactSuite) TestCanMergeWithStringer() { + expected := `3[47]\d{2}[- ]*\d{6}[- ]*\d{5}|4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}` + redactor := logger.AMEXRedactor.Merge(stringer(*logger.VISARedactor)) + suite.Assert().Equal(expected, redactor.String()) +} + +func (suite *RedactSuite) TestMergeShouldPanicWithWrongType() { + suite.Require().Panics(func() { + _ = logger.VISARedactor.Merge(42) + }) +} + +func (suite *RedactSuite) TestCanUseSeveralRedactors() { + output := CaptureStdout(func() { + mainLog := logger.Create( + "test", + &logger.StdoutStream{Unbuffered: true}, + ) + log := mainLog.Child( + nil, + nil, + logger.PhoneRedactor, + logger.EmailRedactor, + ) + log.Infof("message with sensitive (+13178723000) data") + log.Infof("message with sensitive (john.doe@acme.com) data") + }) + lines := strings.Split(output, "\n")[0:2] + suite.LogLineEqual(lines[0], map[string]string{ + "hostname": `[a-zA-Z_0-9\-\.]+`, + "level": "30", + "msg": `message with sensitive \(REDACTED\) data`, + "name": "test", + "pid": "[0-9]+", + "scope": "main", + "tid": "[0-9]+", + "time": `[0-9]+-[0-9]+-[0-9]+T[0-9]+:[0-9]+:[0-9]+Z`, + "topic": "main", + "v": "0", + }) + suite.LogLineEqual(lines[1], map[string]string{ + "hostname": `[a-zA-Z_0-9\-\.]+`, + "level": "30", + "msg": `message with sensitive \(REDACTED\) data`, + "name": "test", + "pid": "[0-9]+", + "scope": "main", + "tid": "[0-9]+", + "time": `[0-9]+-[0-9]+-[0-9]+T[0-9]+:[0-9]+:[0-9]+Z`, + "topic": "main", + "v": "0", + }) +} + +func (suite *RedactSuite) TestCanRedactWithKeysToRedact() { metadata := Metadata{"12345678", "Taro Yamamoto", "Tokyo"} output := CaptureStdout(func() { log := logger.Create("test", &logger.StdoutStream{Unbuffered: true}) diff --git a/redactor.go b/redactor.go index 7677caf..34c847c 100644 --- a/redactor.go +++ b/redactor.go @@ -1,7 +1,9 @@ package logger import ( + "fmt" "regexp" + "strings" "github.com/gildas/go-core" ) @@ -9,26 +11,26 @@ import ( type Redactor regexp.Regexp var ( - // CreditCardRedactor is a Redactor that will redact Credit Card Numbers - CreditCardRedactor = core.Must(NewRedactor(`(4[0-9]{12}(?:[0-9]{3})?)|((?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|(6(?:011|5[0-9]{2})[0-9]{12})|((?:2131|1800|35\d{3})\d{11})`)) - // AMEXRedactor is a Redactor that will redact AMEX Numbers - AMEXRedactor = core.Must(NewRedactor(`3[47][0-9]{13}`)) + AMEXRedactor = core.Must(NewRedactor(`3[47]\d{2}[- ]*\d{6}[- ]*\d{5}`)) // DinersClubRedactor is a Redactor that will redact Diners Club Numbers - DinersClubRedactor = core.Must(NewRedactor(`3(?:0[0-5]|[68][0-9])[0-9]{11}`)) + DinersClubRedactor = core.Must(NewRedactor(`3(?:0[0-5]|[68][0-9])\d[- ]*\d{4}[- ]*\d{4}[- ]*\d{2}`)) // DiscoverRedactor is a Redactor that will redact Discover Numbers - DiscoverRedactor = core.Must(NewRedactor(`6(?:011|5[0-9]{2})[0-9]{12}`)) + DiscoverRedactor = core.Must(NewRedactor(`6(?:011|5[0-9]{2})[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}`)) // JCBRedactor is a Redactor that will redact JCB Numbers - JCBRedactor = core.Must(NewRedactor(`(?:2131|1800|35\d{3})\d{11}`)) + JCBRedactor = core.Must(NewRedactor(`(?:2131|1800|35\d{2})[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}`)) // MasterCardRedactor is a Redactor that will redact MasterCard Numbers - MasterCardRedactor = core.Must(NewRedactor(`(5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}`)) + MasterCardRedactor = core.Must(NewRedactor(`(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}`)) // VISARedactor is a Redactor that will redact VISA Credit Card Numbers - VISARedactor = core.Must(NewRedactor(`4[0-9]{12}(?:[0-9]{3})?`)) + VISARedactor = core.Must(NewRedactor(`4\d{3}[- ]*\d{4}[- ]*\d{4}[- ]*\d{4}`)) + + // CreditCardRedactor is a Redactor that will redact Credit Card Numbers + CreditCardRedactor = AMEXRedactor.Merge(DinersClubRedactor, DiscoverRedactor, JCBRedactor, MasterCardRedactor, VISARedactor) // PhoneRedactor is a Redactor that will redact Phone Numbers PhoneRedactor = core.Must(NewRedactor(`(\+?[0-9]{1,3}[-. ]?[0-9]{3}[-. ]?[0-9]{3}[-. ]?[0-9]{4})|(\+?[0-9]{1,3}[-. ]?[0-9.\-]+)`)) @@ -53,6 +55,36 @@ func NewRedactor(regex string) (*Redactor, error) { return (*Redactor)(r), err } +// Merge merges the Redactor with other Redactors +// +// The other Redactors can be strings, Redactor, *Redactor or fmt.Stringers +func (redactor Redactor) Merge(other ...any) Redactor { + if len(other) == 0 { + copy := redactor + return copy + } + var text strings.Builder + text.WriteString(redactor.String()) + for _, r := range other { + text.WriteString("|") + switch actual := r.(type) { + case *Redactor: + text.WriteString(actual.String()) + case Redactor: + text.WriteString(actual.String()) + case string: + text.WriteString(actual) + default: + if stringer, ok := actual.(fmt.Stringer); ok { + text.WriteString(stringer.String()) + } else { + panic("Invalid Redactor type") + } + } + } + return *core.Must(NewRedactor(text.String())) +} + func (redactor Redactor) Redact(value string) (string, bool) { // TODO: Find a way to not run the regex twice if (*regexp.Regexp)(&redactor).MatchString(value) {