Repository: lindell/multi-gitter
Pull request #185 (2021-09-23 18:43:49)

fix: censor http authentication header
...even if the token is encoded (basic auth)



Patch:
diff --git a/cmd/logging.go b/cmd/logging.go
index 82a2efe..7f30bcd 100644
--- a/cmd/logging.go
+++ b/cmd/logging.go
@@ -62,10 +62,7 @@ func logFlagInit(cmd *cobra.Command, args []string) error {
 		})
 	}
 
-	log.SetFormatter(&internallog.CensorFormatter{
-		CensorItems:         censorItems,
-		UnderlyingFormatter: formatter,
-	})
+	log.SetFormatter(internallog.NewCensorFormatter(formatter, censorItems...))
 
 	// Set the output (file)
 	strFile, _ := cmd.Flags().GetString("log-file")
diff --git a/internal/log/censor-formatter.go b/internal/log/censor-formatter.go
index 6c9667c..09d53b1 100644
--- a/internal/log/censor-formatter.go
+++ b/internal/log/censor-formatter.go
@@ -2,11 +2,21 @@ package log
 
 import (
 	"bytes"
+	"regexp"
 	"strings"
 
 	log "github.com/sirupsen/logrus"
 )
 
+// NewCensorFormatter creates a new formater that censors sensitive logs.
+// It contains some default censoring rules, but additional items may be used
+func NewCensorFormatter(underlyingFormater log.Formatter, additionalCensoring ...CensorItem) *CensorFormatter {
+	return &CensorFormatter{
+		CensorItems:         append(defaultCensorItems, additionalCensoring...),
+		UnderlyingFormatter: underlyingFormater,
+	}
+}
+
 // CensorFormatter makes sure sensitive data is not logged.
 // It works as a middleware and sensors the data before sending it to an underlying formatter
 type CensorFormatter struct {
@@ -16,21 +26,42 @@ type CensorFormatter struct {
 
 // CensorItem is something that should be censored, Sensitive will be replaced with Replacement
 type CensorItem struct {
-	Sensitive   string
-	Replacement string
+	Sensitive       string
+	SensitiveRegexp *regexp.Regexp
+	Replacement     string
+}
+
+func (c CensorItem) stringReplace(str string) string {
+	if c.Sensitive != "" {
+		return strings.ReplaceAll(str, c.Sensitive, c.Replacement)
+	}
+	if c.SensitiveRegexp != nil {
+		return c.SensitiveRegexp.ReplaceAllString(str, c.Replacement)
+	}
+	return str
+}
+
+func (c CensorItem) byteReplace(bb []byte) []byte {
+	if c.Sensitive != "" {
+		return bytes.ReplaceAll(bb, []byte(c.Sensitive), []byte(c.Replacement))
+	}
+	if c.SensitiveRegexp != nil {
+		return c.SensitiveRegexp.ReplaceAll(bb, []byte(c.Replacement))
+	}
+	return bb
 }
 
 // Format censors some data and sends the entry to the underlying formatter
 func (f *CensorFormatter) Format(entry *log.Entry) ([]byte, error) {
 	for _, s := range f.CensorItems {
-		entry.Message = strings.ReplaceAll(entry.Message, s.Sensitive, s.Replacement)
+		entry.Message = s.stringReplace(entry.Message)
 
 		for key := range entry.Data {
 			if str, ok := entry.Data[key].(string); ok {
-				entry.Data[key] = strings.ReplaceAll(str, s.Sensitive, s.Replacement)
+				entry.Data[key] = s.stringReplace(str)
 			}
 			if bb, ok := entry.Data[key].([]byte); ok {
-				entry.Data[key] = bytes.ReplaceAll(bb, []byte(s.Sensitive), []byte(s.Replacement))
+				entry.Data[key] = s.byteReplace(bb)
 			}
 		}
 	}
diff --git a/internal/log/default-censors.go b/internal/log/default-censors.go
new file mode 100644
index 0000000..e5f39c3
--- /dev/null
+++ b/internal/log/default-censors.go
@@ -0,0 +1,10 @@
+package log
+
+import "regexp"
+
+var defaultCensorItems = []CensorItem{
+	{
+		SensitiveRegexp: regexp.MustCompile(`(?i)\n(Authorization: [a-z]+) ([^ ]+)\n`),
+		Replacement:     "\n$1 <CENSORED>\n",
+	},
+}


Test patch:
diff --git a/internal/log/censor-formatter_test.go b/internal/log/censor-formatter_test.go
new file mode 100644
index 0000000..6b42347
--- /dev/null
+++ b/internal/log/censor-formatter_test.go
@@ -0,0 +1,134 @@
+package log
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+type dummyFormater struct {
+	entry logrus.Entry
+}
+
+func (f *dummyFormater) Format(entry *logrus.Entry) ([]byte, error) {
+	f.entry = *entry
+	return []byte{}, nil
+}
+
+func TestFormat(t *testing.T) {
+	tests := []struct {
+		name  string
+		items []CensorItem
+		entry logrus.Entry
+
+		expectedMessage string
+		expectedData    logrus.Fields
+	}{
+		{
+			name: "simple",
+			items: []CensorItem{
+				{
+					Sensitive:   "password",
+					Replacement: "<censored>",
+				},
+			},
+			entry: logrus.Entry{
+				Message: "this is the password",
+				Data: logrus.Fields{
+					"string-field": "something password something",
+					"bytes-field":  []byte("something password something"),
+				},
+			},
+			expectedMessage: "this is the <censored>",
+			expectedData: logrus.Fields{
+				"string-field": "something <censored> something",
+				"bytes-field":  []byte("something <censored> something"),
+			},
+		},
+		{
+			name: "multiple items",
+			items: []CensorItem{
+				{
+					Sensitive:   "password",
+					Replacement: "<censored-password>",
+				},
+				{
+					Sensitive:   "token",
+					Replacement: "<censored-token>",
+				},
+			},
+			entry: logrus.Entry{
+				Message: "a password and a token",
+				Data: logrus.Fields{
+					"string-field": "a password and a token",
+					"bytes-field":  []byte("a password and a token"),
+				},
+			},
+			expectedMessage: "a <censored-password> and a <censored-token>",
+			expectedData: logrus.Fields{
+				"string-field": "a <censored-password> and a <censored-token>",
+				"bytes-field":  []byte("a <censored-password> and a <censored-token>"),
+			},
+		},
+		{
+			name: "http authorization",
+			items: []CensorItem{
+				{
+					SensitiveRegexp: regexp.MustCompile(`(?i)\n(Authorization: [a-z]+) ([^ ]+)\n`),
+					Replacement:     "\n$1 <censored>\n",
+				},
+			},
+			entry: logrus.Entry{
+				Data: logrus.Fields{
+					"request": `GET /api/v4/ HTTP/1.1
+Host: gitlab.com
+User-Agent: Go-http-client/1.1
+Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+Accept-Encoding: gzip
+
+Some Data`,
+					"request-as-byte-slice": []byte(`GET /api/v4/ HTTP/1.1
+Host: gitlab.com
+User-Agent: Go-http-client/1.1
+Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+Accept-Encoding: gzip
+
+Some Data`),
+				},
+			},
+			expectedData: logrus.Fields{
+				"request": `GET /api/v4/ HTTP/1.1
+Host: gitlab.com
+User-Agent: Go-http-client/1.1
+Authorization: Basic <censored>
+Accept-Encoding: gzip
+
+Some Data`,
+				"request-as-byte-slice": []byte(`GET /api/v4/ HTTP/1.1
+Host: gitlab.com
+User-Agent: Go-http-client/1.1
+Authorization: Basic <censored>
+Accept-Encoding: gzip
+
+Some Data`),
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			dummyFormater := &dummyFormater{}
+			formatter := CensorFormatter{
+				CensorItems:         test.items,
+				UnderlyingFormatter: dummyFormater,
+			}
+			_, err := formatter.Format(&test.entry)
+			assert.NoError(t, err)
+
+			assert.Equal(t, test.expectedData, dummyFormater.entry.Data)
+			assert.Equal(t, test.expectedMessage, dummyFormater.entry.Message)
+		})
+	}
+}