[object Object]

← back to Cli Printing Press

fix(cli): redact shipcheck api key in json (#1673)

4093ef3eef08ad8a88b6ebf16b42ce817a16e113 · 2026-05-19 15:28:19 -0700 · Trevin Chow

* fix(cli): redact shipcheck api key in json

* fix(cli): address shipcheck redaction review

Files touched

Diff

commit 4093ef3eef08ad8a88b6ebf16b42ce817a16e113
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 19 15:28:19 2026 -0700

    fix(cli): redact shipcheck api key in json (#1673)
    
    * fix(cli): redact shipcheck api key in json
    
    * fix(cli): address shipcheck redaction review
---
 internal/cli/shipcheck.go      | 23 ++++++++++++++++++++---
 internal/cli/shipcheck_test.go | 37 +++++++++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/internal/cli/shipcheck.go b/internal/cli/shipcheck.go
index 2dc5fb0c..3c5a577b 100644
--- a/internal/cli/shipcheck.go
+++ b/internal/cli/shipcheck.go
@@ -318,8 +318,7 @@ type shipcheckJSONEnvelope struct {
 }
 
 // renderShipcheckJSON marshals the envelope to w. Each leg's `command`
-// field shows the full argv as it would be invoked at the shell so an
-// operator can copy-paste-rerun a specific leg from the JSON output.
+// field shows the argv used for that leg with sensitive flag values redacted.
 func renderShipcheckJSON(w *os.File, binPath string, results []shipcheckLegResult, runStartedAt time.Time, runElapsed time.Duration) error {
 	env := shipcheckJSONEnvelope{
 		Passed:    shipcheckUmbrellaCode(results) == 0,
@@ -335,7 +334,7 @@ func renderShipcheckJSON(w *os.File, binPath string, results []shipcheckLegResul
 			Passed:    r.Passed(),
 			StartedAt: r.StartedAt.UTC().Format(time.RFC3339),
 			ElapsedMS: r.Elapsed.Milliseconds(),
-			Command:   strings.Join(append([]string{binPath}, r.Argv...), " "),
+			Command:   renderShipcheckCommand(binPath, r.Argv),
 		})
 	}
 	enc := json.NewEncoder(w)
@@ -343,6 +342,24 @@ func renderShipcheckJSON(w *os.File, binPath string, results []shipcheckLegResul
 	return enc.Encode(env)
 }
 
+func renderShipcheckCommand(binPath string, argv []string) string {
+	args := append([]string{binPath}, redactShipcheckCommandArgv(argv)...)
+	return strings.Join(args, " ")
+}
+
+func redactShipcheckCommandArgv(argv []string) []string {
+	redacted := make([]string, len(argv))
+	copy(redacted, argv)
+	for i, arg := range redacted {
+		if arg == "--api-key" {
+			if i+1 < len(redacted) {
+				redacted[i+1] = "<redacted>"
+			}
+		}
+	}
+	return redacted
+}
+
 // validateShipcheckDir confirms --dir points at something that looks
 // like a built printing-press CLI: a directory containing go.mod and
 // either an internal/cli/ tree or a cmd/<name>-pp-cli/ tree. We are
diff --git a/internal/cli/shipcheck_test.go b/internal/cli/shipcheck_test.go
index ab958b19..db13d6f3 100644
--- a/internal/cli/shipcheck_test.go
+++ b/internal/cli/shipcheck_test.go
@@ -474,6 +474,43 @@ func TestShipcheck_JSONEnvelope_AllPass(t *testing.T) {
 	}
 }
 
+func TestShipcheck_JSONEnvelope_RedactsAPIKeyFromCommands(t *testing.T) {
+	h := newShipcheckHarness(t)
+	const secret = "secret-token"
+
+	out := captureStdout(t, func() {
+		if err := runShipcheckCmd(t, "--dir", h.dir, "--json", "--api-key", secret); err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+	})
+
+	var env shipcheckJSONEnvelope
+	if err := json.Unmarshal([]byte(extractFinalJSONObject(t, out)), &env); err != nil {
+		t.Fatalf("envelope is not valid JSON: %v", err)
+	}
+
+	var verifyCommand string
+	for _, leg := range env.Legs {
+		if strings.Contains(leg.Command, secret) {
+			t.Fatalf("leg %s command leaked API key: %q", leg.Name, leg.Command)
+		}
+		if leg.Name == "verify" {
+			verifyCommand = leg.Command
+		}
+	}
+	if verifyCommand == "" {
+		t.Fatal("envelope missing verify command")
+	}
+	if !strings.Contains(verifyCommand, "--api-key <redacted>") {
+		t.Fatalf("verify command should include redacted API key flag; got %q", verifyCommand)
+	}
+
+	verifyArgs := findInvocation(readStubLog(t, h.logFile), "verify")
+	if !argvHas(verifyArgs, secret) {
+		t.Fatalf("verify subprocess argv should still receive the raw API key; got %v", verifyArgs)
+	}
+}
+
 // TestShipcheck_JSONEnvelope_OneFailure: --json envelope reflects a
 // failing leg with passed=false at the leg and envelope level.
 func TestShipcheck_JSONEnvelope_OneFailure(t *testing.T) {

← d8178077 fix(cli): handle binary-only response endpoints (Accept + ba  ·  back to Cli Printing Press  ·  fix(cli): return failure exit from verify json (#1693) 5261ae73 →