diff --git a/backend/internal/audit/record.go b/backend/internal/audit/record.go index 0f353fe..1b02543 100644 --- a/backend/internal/audit/record.go +++ b/backend/internal/audit/record.go @@ -11,6 +11,7 @@ const ( StatusUpstreamRejected Status = "upstream_rejected" StatusUpstreamTimeout Status = "upstream_timeout" StatusUpstreamUnavailable Status = "upstream_unavailable" + StatusUpstreamInvalid Status = "upstream_invalid_response" StatusInternalError Status = "internal_error" ) diff --git a/backend/internal/audit/sanitize.go b/backend/internal/audit/sanitize.go new file mode 100644 index 0000000..a26a9fe --- /dev/null +++ b/backend/internal/audit/sanitize.go @@ -0,0 +1,48 @@ +package audit + +import ( + "encoding/json" + "strings" +) + +func SanitizeResponse(raw []byte, maskedCard string) []byte { + if len(raw) == 0 { + return nil + } + + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return nil + } + + if maskedCard == "" { + maskedCard = "[redacted]" + } + + redactCardFields(value, maskedCard) + + sanitized, err := json.Marshal(value) + if err != nil { + return nil + } + + return sanitized +} + +func redactCardFields(value any, maskedCard string) { + switch current := value.(type) { + case map[string]any: + for key, child := range current { + if strings.EqualFold(key, "card") { + current[key] = maskedCard + continue + } + + redactCardFields(child, maskedCard) + } + case []any: + for _, child := range current { + redactCardFields(child, maskedCard) + } + } +} diff --git a/backend/internal/audit/sanitize_test.go b/backend/internal/audit/sanitize_test.go new file mode 100644 index 0000000..7bec67d --- /dev/null +++ b/backend/internal/audit/sanitize_test.go @@ -0,0 +1,56 @@ +package audit + +import ( + "encoding/json" + "testing" +) + +func TestSanitizeResponse(t *testing.T) { + raw := []byte(`{ + "data": { + "card": "6037997512345678", + "iban": "IR820540102680020817909002" + }, + "meta": {"isSuccess": true} + }`) + + got := SanitizeResponse(raw, "603799******5678") + if len(got) == 0 { + t.Fatal("SanitizeResponse() returned an empty response") + } + + var decoded map[string]any + if err := json.Unmarshal(got, &decoded); err != nil { + t.Fatalf("unmarshal sanitized response: %v", err) + } + + data, ok := decoded["data"].(map[string]any) + if !ok { + t.Fatal("sanitized response does not contain data object") + } + + if gotCard := data["card"]; gotCard != "603799******5678" { + t.Errorf("sanitized card = %v, want masked card", gotCard) + } + + if gotIBAN := data["iban"]; gotIBAN != "IR820540102680020817909002" { + t.Errorf("sanitized iban = %v, want original iban", gotIBAN) + } +} + +func TestSanitizeResponseRejectsInvalidJSON(t *testing.T) { + if got := SanitizeResponse([]byte(`not-json`), "603799******5678"); got != nil { + t.Errorf("SanitizeResponse() = %q, want nil", got) + } +} + +func TestSanitizeResponseRedactsCardWhenMaskIsEmpty(t *testing.T) { + got := SanitizeResponse( + []byte(`{"card":"6037997512345678"}`), + "", + ) + + if string(got) != `{"card":"[redacted]"}` { + t.Errorf("SanitizeResponse() = %s, want redacted card", got) + } +} diff --git a/backend/internal/card/validation.go b/backend/internal/card/validation.go index a750bc6..c97e3bf 100644 --- a/backend/internal/card/validation.go +++ b/backend/internal/card/validation.go @@ -25,3 +25,11 @@ func Validate(number string) error { return nil } + +func Mask(number string) string { + if Validate(number) != nil { + return "" + } + + return number[:6] + "******" + number[12:] +} diff --git a/backend/internal/card/validation_test.go b/backend/internal/card/validation_test.go index 418eb2e..97ebf06 100644 --- a/backend/internal/card/validation_test.go +++ b/backend/internal/card/validation_test.go @@ -53,3 +53,35 @@ func TestValidate(t *testing.T) { }) } } + +func TestMask(t *testing.T) { + tests := []struct { + name string + number string + want string + }{ + { + name: "valid card number", + number: "6037997512345678", + want: "603799******5678", + }, + { + name: "invalid card number", + number: "603799", + want: "", + }, + { + name: "non numeric card number", + number: "603799ABC2345678", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Mask(tt.number); got != tt.want { + t.Errorf("Mask() = %q, want %q", got, tt.want) + } + }) + } +}