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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Redact credential-bearing flag values (e.g. `--password`, `--token`) from `Exec` command debug logs
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user