chore(core): redact sensitive flag values from exec command logs (#4087)

The `Exec` helper in `@trigger.dev/core` logs command args at debug
level (and in its output/error metadata). For commands that take a
credential directly on the command line - `--password`, `--token`,
`--secret`, etc. - that value is logged verbatim, so turning on debug
logging can surface secrets in log sinks.

This masks the value of known credential-bearing flags (both `--flag
value` and `--flag=value` forms) before the args are logged. The
executed command is untouched - only the logged copy is redacted. Added
a small unit test for the redaction helper.
This commit is contained in:
nicktrn
2026-06-30 18:59:06 +01:00
committed by GitHub
parent d5ea3dd3b7
commit baaecfcff3
3 changed files with 74 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Redact credential-bearing flag values (e.g. `--password`, `--token`) from `Exec` command debug logs
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { redactArgsForLogging } from "./exec.js";
describe("redactArgsForLogging", () => {
it("masks the value following a credential flag", () => {
expect(
redactArgsForLogging(["login", "--username", "robot", "--password", "s3cr3t", "host:80"])
).toEqual(["login", "--username", "robot", "--password", "[redacted]", "host:80"]);
});
it("masks inline --flag=value form", () => {
expect(redactArgsForLogging(["--token=abc123"])).toEqual(["--token=[redacted]"]);
});
it("leaves non-credential args untouched", () => {
expect(redactArgsForLogging(["push", "--tls-verify=false", "host:80/img"])).toEqual([
"push",
"--tls-verify=false",
"host:80/img",
]);
});
it("passes undefined through", () => {
expect(redactArgsForLogging(undefined)).toBeUndefined();
});
});
+43 -4
View File
@@ -23,6 +23,38 @@ export interface ExecOptions {
neverThrow?: boolean;
}
// Long-form flags whose value carries a credential - the following arg (or inline
// `--flag=value`) is replaced before args are logged so it never reaches log sinks.
const REDACTED_FLAGS = new Set([
"--password",
"--token",
"--secret",
"--access-token",
"--registry-token",
"--registry-password",
"--api-key",
]);
export function redactArgsForLogging(args?: string[]): string[] | undefined {
if (!args) {
return args;
}
return args.map((arg, index) => {
const previous = index > 0 ? args[index - 1]?.trim() : undefined;
if (previous && REDACTED_FLAGS.has(previous)) {
return "[redacted]";
}
const equalsIndex = arg.indexOf("=");
if (equalsIndex > 0 && REDACTED_FLAGS.has(arg.slice(0, equalsIndex).trim())) {
return `${arg.slice(0, equalsIndex)}=[redacted]`;
}
return arg;
});
}
export class Exec {
private logger: SimpleStructuredLogger;
private abortSignal: AbortSignal | undefined;
@@ -47,8 +79,15 @@ export class Exec {
): Promise<Output> {
const argsTrimmed = this.trimArgs ? args?.map((arg) => arg.trim()) : args;
const commandWithFirstArg = `${command}${argsTrimmed?.length ? ` ${argsTrimmed[0]}` : ""}`;
this.logger.debug(`exec: ${commandWithFirstArg}`, { command, args, argsTrimmed });
const argsForLogging = redactArgsForLogging(args);
const argsTrimmedForLogging = redactArgsForLogging(argsTrimmed);
const commandWithFirstArg = `${command}${argsTrimmedForLogging?.length ? ` ${argsTrimmedForLogging[0]}` : ""}`;
this.logger.debug(`exec: ${commandWithFirstArg}`, {
command,
args: argsForLogging,
argsTrimmed: argsTrimmedForLogging,
});
const result = x(command, argsTrimmed, {
signal: opts?.ignoreAbort ? undefined : this.abortSignal,
@@ -60,8 +99,8 @@ export class Exec {
const metadata = {
command,
argsRaw: args,
argsTrimmed,
argsRaw: argsForLogging,
argsTrimmed: argsTrimmedForLogging,
globalOpts: {
trimArgs: this.trimArgs,
neverThrow: this.neverThrow,