fix(cli): redact encryption_key diff in stdout by default (#9347)

* fix(cli): redact encryption_key diff in stdout by default

Sync diff output previously printed the full encryption_key contents on
stdout whenever the workspace key changed locally or on the remote, which
made it easy to leak the key via shell history, CI logs, etc. Now the
diff is replaced with a redacted notice for any encryption_key change in
both prettyChanges and showConflict. Pass --show-encryption-key-diff
(also configurable via wmill.yaml's showEncryptionKeyDiff) to opt back
into the full diff.

Fixes WIN-1992

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): redact encryption_key diff with fixed-length mask

Drop the --show-encryption-key-diff opt-in and always redact: the diff
now keeps the first 5 chars of the key so rotations are still visible
(different prefixes), then replaces every remaining char with `*` so the
length of the key is preserved without leaking it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-27 11:28:11 +00:00
committed by GitHub
parent ae2222febf
commit 88056f8d4c
2 changed files with 45 additions and 3 deletions
+9 -2
View File
@@ -22,6 +22,7 @@ import {
showConflict,
showDiff,
extractNativeTriggerInfo,
redactEncryptionKey,
} from "../../types.ts";
import { downloadZip } from "./pull.ts";
import { runLint, printReport, checkMissingLocks } from "../lint/lint.ts";
@@ -3095,16 +3096,22 @@ function prettyChanges(
),
);
} else if (change.name === "edited") {
const changeType = getTypeStrFromPath(change.path);
log.info(
colors.yellow(
`~ ${getTypeStrFromPath(change.path)} ` +
`~ ${changeType} ` +
displayPath +
colors.gray(wsNote) +
(change.codebase ? ` (codebase changed)` : ""),
),
);
if (change.before != change.after) {
if (change.path.endsWith(".yaml")) {
if (changeType === "encryption_key") {
showDiff(
redactEncryptionKey(change.before),
redactEncryptionKey(change.after),
);
} else if (change.path.endsWith(".yaml")) {
try {
showDiff(
yamlStringify(
+36 -1
View File
@@ -129,11 +129,46 @@ export function showDiff(local: string, remote: string) {
export function showConflict(path: string, local: string, remote: string) {
log.info(colors.yellow(`- ${path}`));
showDiff(local, remote);
let isEncryptionKey = false;
try {
isEncryptionKey = getTypeStrFromPath(path) === "encryption_key";
} catch {
// ignore
}
if (isEncryptionKey) {
showDiff(redactEncryptionKey(local), redactEncryptionKey(remote));
} else {
showDiff(local, remote);
}
log.info("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
log.info("\n");
}
// Reveal only the first 5 chars of the key so a rotation is still visible in
// the diff (different prefixes), without leaking the whole secret to stdout.
// The remaining chars are replaced with `*`, preserving length so the diff
// keeps showing whether the key length changed.
export function redactEncryptionKey(content: string): string {
if (!content) return content;
// The encryption_key payload is JSON-encoded (a quoted string). Parse it so
// we redact the key value itself, then re-serialize to JSON to preserve the
// file's shape; fall back to raw redaction if parsing fails.
try {
const parsed = JSON.parse(content);
if (typeof parsed === "string") {
return JSON.stringify(redactString(parsed));
}
} catch {
// not JSON — treat content as the raw key
}
return redactString(content);
}
function redactString(s: string): string {
if (s.length <= 5) return s;
return s.slice(0, 5) + "*".repeat(s.length - 5);
}
/**
* Pushes an object to the workspace server based on its type
* @param workspace - The workspace ID to push to