From 549b46d72cea096389e9554ed17b9065e20064e0 Mon Sep 17 00:00:00 2001 From: Fred Hatfull <82189157+fhats-stripe@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:40:32 -0500 Subject: [PATCH] Adds sanitization of request log data on output (#1280) * Adds sanitization of request log data on output * Appease goimports * appease go-mod-tidy --- go.mod | 1 + go.sum | 2 + pkg/cmd/logs/tail.go | 28 +++++++++ pkg/cmd/logs/tail_test.go | 123 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 pkg/cmd/logs/tail_test.go diff --git a/go.mod b/go.mod index 0548fb71..2428909c 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( ) require ( + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/go-git/go-git/v5 v5.12.0 github.com/hashicorp/go-hclog v1.2.2 github.com/hashicorp/go-plugin v1.4.4 diff --git a/go.sum b/go.sum index 5748617a..ce61eb83 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= diff --git a/pkg/cmd/logs/tail.go b/pkg/cmd/logs/tail.go index 9501a202..aaffc65c 100644 --- a/pkg/cmd/logs/tail.go +++ b/pkg/cmd/logs/tail.go @@ -6,10 +6,12 @@ import ( "os" "os/signal" "reflect" + "regexp" "strings" "syscall" "time" + "github.com/acarl005/stripansi" "github.com/briandowns/spinner" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -28,6 +30,8 @@ import ( const outputFormatJSON = "JSON" +var newlineRegex = regexp.MustCompile("[\r\n]") + // TailCmd wraps the configuration for the tail command type TailCmd struct { apiBaseURL string @@ -289,6 +293,8 @@ func createVisitor(logger *log.Logger, format string) *websocket.Visitor { return fmt.Errorf("VisitData received unexpected type for DataElement, got %T expected %T", de, logtailing.EventPayload{}) } + sanitizePayload(&log) + if strings.ToUpper(format) == outputFormatJSON { fmt.Println(ansi.ColorizeJSON(de.Marshaled, false, os.Stdout)) return nil @@ -337,3 +343,25 @@ func urlForRequestID(payload *logtailing.EventPayload) string { return fmt.Sprintf("https://dashboard.stripe.com%s/logs/%s", maybeTest, payload.RequestID) } + +func sanitize(str string) string { + withoutAnsi := stripansi.Strip(str) + withoutNewlines := newlineRegex.ReplaceAllLiteralString(withoutAnsi, "") + return strings.TrimSpace(withoutNewlines) +} + +func sanitizePayload(payload *logtailing.EventPayload) { + payload.Error.Charge = sanitize(payload.Error.Charge) + payload.Error.Code = sanitize(payload.Error.Code) + payload.Error.DeclineCode = sanitize(payload.Error.DeclineCode) + payload.Error.ErrorInsight = sanitize(payload.Error.ErrorInsight) + payload.Error.Message = sanitize(payload.Error.Message) + payload.Error.Param = sanitize(payload.Error.Param) + payload.Error.Type = sanitize(payload.Error.Type) + + payload.Method = sanitize(payload.Method) + + payload.RequestID = sanitize(payload.RequestID) + + payload.URL = sanitize(payload.URL) +} diff --git a/pkg/cmd/logs/tail_test.go b/pkg/cmd/logs/tail_test.go new file mode 100644 index 00000000..1f1fb01b --- /dev/null +++ b/pkg/cmd/logs/tail_test.go @@ -0,0 +1,123 @@ +package logs + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stripe/stripe-cli/pkg/logtailing" +) + +func hasZeroValueString(v reflect.Value) bool { + switch v.Kind() { + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if hasZeroValueString(v.Field(i)) { + return true + } + } + return false + case reflect.String: + return v.IsZero() + default: + return false + } +} + +func containsZeroValueStrings(x interface{}) bool { + v := reflect.ValueOf(x) + + // If it's a pointer, dereference it + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if !v.IsValid() { + return true + } + + return hasZeroValueString(v) +} + +func TestSanitize(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "does not change basic strings", + input: "GET", + expected: "GET", + }, + { + name: "removes ansi escape codes", + input: "\x0d\x0a\x1b[90mvery cool\x0d\x0a\x1b[32m and very legal", + expected: "very cool and very legal", + }, + { + name: "removes newlines", + input: "\x0d\x0a\x1b[90mvery cool", + expected: "very cool", + }, + { + name: "removes both ansi escape codes and newlines", + input: "\x0d\x0a\x1b[90ma horse\r\n a dog\n a cat\x0d\x0a\x1b[32m and a bird", + expected: "a horse a dog a cat and a bird", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := sanitize(tt.input) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestSanitizePayload(t *testing.T) { + withAnsi := func(s string) string { + return fmt.Sprintf("\x1b[90m%s\x1b[0m", s) + } + + payload := logtailing.EventPayload{ + Error: logtailing.RedactedError{ + Charge: withAnsi("ch_123"), + Code: withAnsi("invlaid_argument"), + DeclineCode: withAnsi("card_declined"), + ErrorInsight: withAnsi("make fewer errors"), + Message: withAnsi("an error occurred"), + Param: withAnsi("card"), + Type: withAnsi("invalid_request"), + }, + Method: withAnsi("POST"), + RequestID: withAnsi("req_123"), + URL: withAnsi("https://example.com"), + } + + expected := logtailing.EventPayload{ + Error: logtailing.RedactedError{ + Charge: "ch_123", + Code: "invlaid_argument", + DeclineCode: "card_declined", + ErrorInsight: "make fewer errors", + Message: "an error occurred", + Param: "card", + Type: "invalid_request", + }, + Method: "POST", + RequestID: "req_123", + URL: "https://example.com", + } + + // Ensures that we're testing/covering the entire payload in case + // any new fields are added + require.Equal(t, containsZeroValueStrings(payload), false) + + sanitizePayload(&payload) + + assert.Equal(t, expected, payload) +}