Skip to content

Commit 686c912

Browse files
committed
redact secrets when echoing commands
1 parent adab915 commit 686c912

File tree

4 files changed

+103
-17
lines changed

4 files changed

+103
-17
lines changed

SimpleExec/Command.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static class Command
2020
/// <param name="name">The name of the command. This can be a path to an executable file.</param>
2121
/// <param name="args">The arguments to pass to the command.</param>
2222
/// <param name="workingDirectory">The working directory in which to run the command.</param>
23+
/// <param name="secrets">A list of secrets that are redacted by replacement with "***" when echoing the resulting command line and the working directory (if specified) to standard output (stdout).</param>
2324
/// <param name="noEcho">Whether to echo the resulting command line and working directory (if specified) to standard output (stdout).</param>
2425
/// <param name="echoPrefix">The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout).</param>
2526
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
@@ -44,6 +45,7 @@ public static void Run(
4445
string name,
4546
string args = "",
4647
string workingDirectory = "",
48+
IEnumerable<string>? secrets = null,
4749
bool noEcho = false,
4850
string? echoPrefix = null,
4951
Action<IDictionary<string, string?>>? configureEnvironment = null,
@@ -60,7 +62,7 @@ public static void Run(
6062
false,
6163
configureEnvironment ?? DefaultAction,
6264
createNoWindow)
63-
.Run(noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
65+
.Run(secrets ?? [], noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
6466

6567
/// <summary>
6668
/// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin).
@@ -72,6 +74,7 @@ public static void Run(
7274
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
7375
/// </param>
7476
/// <param name="workingDirectory">The working directory in which to run the command.</param>
77+
/// <param name="secrets">A list of secrets that are redacted by replacement with "***" when echoing the resulting command line and the working directory (if specified) to standard output (stdout).</param>
7578
/// <param name="noEcho">Whether to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout).</param>
7679
/// <param name="echoPrefix">The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout).</param>
7780
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
@@ -92,6 +95,7 @@ public static void Run(
9295
string name,
9396
IEnumerable<string> args,
9497
string workingDirectory = "",
98+
IEnumerable<string>? secrets = null,
9599
bool noEcho = false,
96100
string? echoPrefix = null,
97101
Action<IDictionary<string, string?>>? configureEnvironment = null,
@@ -108,10 +112,11 @@ public static void Run(
108112
false,
109113
configureEnvironment ?? DefaultAction,
110114
createNoWindow)
111-
.Run(noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
115+
.Run(secrets ?? [], noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
112116

113117
private static void Run(
114118
this System.Diagnostics.ProcessStartInfo startInfo,
119+
IEnumerable<string> secrets,
115120
bool noEcho,
116121
string echoPrefix,
117122
Func<int, bool>? handleExitCode,
@@ -121,7 +126,7 @@ private static void Run(
121126
using var process = new Process();
122127
process.StartInfo = startInfo;
123128

124-
process.Run(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken);
129+
process.Run(secrets, noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken);
125130

126131
if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
127132
{
@@ -136,6 +141,7 @@ private static void Run(
136141
/// <param name="name">The name of the command. This can be a path to an executable file.</param>
137142
/// <param name="args">The arguments to pass to the command.</param>
138143
/// <param name="workingDirectory">The working directory in which to run the command.</param>
144+
/// <param name="secrets">A list of secrets that are redacted by replacement with "***" when echoing the resulting command line and the working directory (if specified) to standard output (stdout).</param>
139145
/// <param name="noEcho">Whether to echo the resulting command line and working directory (if specified) to standard output (stdout).</param>
140146
/// <param name="echoPrefix">The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout).</param>
141147
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
@@ -161,6 +167,7 @@ public static Task RunAsync(
161167
string name,
162168
string args = "",
163169
string workingDirectory = "",
170+
IEnumerable<string>? secrets = null,
164171
bool noEcho = false,
165172
string? echoPrefix = null,
166173
Action<IDictionary<string, string?>>? configureEnvironment = null,
@@ -177,7 +184,7 @@ public static Task RunAsync(
177184
false,
178185
configureEnvironment ?? DefaultAction,
179186
createNoWindow)
180-
.RunAsync(noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
187+
.RunAsync(secrets ?? [], noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
181188

182189
/// <summary>
183190
/// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin).
@@ -189,6 +196,7 @@ public static Task RunAsync(
189196
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
190197
/// </param>
191198
/// <param name="workingDirectory">The working directory in which to run the command.</param>
199+
/// <param name="secrets">A list of secrets that are redacted by replacement with "***" when echoing the resulting command line and the working directory (if specified) to standard output (stdout).</param>
192200
/// <param name="noEcho">Whether to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout).</param>
193201
/// <param name="echoPrefix">The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout).</param>
194202
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
@@ -210,6 +218,7 @@ public static Task RunAsync(
210218
string name,
211219
IEnumerable<string> args,
212220
string workingDirectory = "",
221+
IEnumerable<string>? secrets = null,
213222
bool noEcho = false,
214223
string? echoPrefix = null,
215224
Action<IDictionary<string, string?>>? configureEnvironment = null,
@@ -226,10 +235,11 @@ public static Task RunAsync(
226235
false,
227236
configureEnvironment ?? DefaultAction,
228237
createNoWindow)
229-
.RunAsync(noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
238+
.RunAsync(secrets ?? [], noEcho, echoPrefix ?? DefaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
230239

231240
private static async Task RunAsync(
232241
this System.Diagnostics.ProcessStartInfo startInfo,
242+
IEnumerable<string> secrets,
233243
bool noEcho,
234244
string echoPrefix,
235245
Func<int, bool>? handleExitCode,
@@ -239,7 +249,7 @@ private static async Task RunAsync(
239249
using var process = new Process();
240250
process.StartInfo = startInfo;
241251

242-
await process.RunAsync(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken).ConfigureAwait(false);
252+
await process.RunAsync(secrets, noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken).ConfigureAwait(false);
243253

244254
if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
245255
{
@@ -367,7 +377,7 @@ private static async Task RunAsync(
367377
process.StartInfo = startInfo;
368378

369379
#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
370-
var runProcess = process.RunAsync(true, "", cancellationIgnoresProcessTree, cancellationToken);
380+
var runProcess = process.RunAsync([], true, "", cancellationIgnoresProcessTree, cancellationToken);
371381
#pragma warning restore CA2025
372382

373383
Task<string> readOutput;

SimpleExec/ProcessExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ namespace SimpleExec;
77

88
internal static class ProcessExtensions
99
{
10-
public static void Run(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
10+
public static void Run(this Process process, IEnumerable<string> secrets, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
1111
{
1212
var cancelled = new StrongBox<long>(0);
1313

1414
if (!noEcho)
1515
{
16-
Console.Out.Write(process.StartInfo.GetEchoLines(echoPrefix));
16+
Console.Out.Write(process.StartInfo.GetEchoLines(secrets, echoPrefix));
1717
}
1818

1919
_ = process.Start();
@@ -36,7 +36,7 @@ public static void Run(this Process process, bool noEcho, string echoPrefix, boo
3636
}
3737
}
3838

39-
public static async Task RunAsync(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
39+
public static async Task RunAsync(this Process process, IEnumerable<string> secrets, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
4040
{
4141
using var sync = new SemaphoreSlim(1, 1);
4242
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -46,7 +46,7 @@ public static async Task RunAsync(this Process process, bool noEcho, string echo
4646

4747
if (!noEcho)
4848
{
49-
await Console.Out.WriteAsync(process.StartInfo.GetEchoLines(echoPrefix)).ConfigureAwait(false);
49+
await Console.Out.WriteAsync(process.StartInfo.GetEchoLines(secrets, echoPrefix)).ConfigureAwait(false);
5050
}
5151

5252
_ = process.Start();
@@ -66,7 +66,7 @@ public static async Task RunAsync(this Process process, bool noEcho, string echo
6666
await tcs.Task.ConfigureAwait(false);
6767
}
6868

69-
private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info, string echoPrefix)
69+
private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info, IEnumerable<string> secrets, string echoPrefix)
7070
{
7171
var builder = new StringBuilder();
7272

@@ -89,9 +89,12 @@ private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info
8989
_ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {info.FileName}{(string.IsNullOrEmpty(info.Arguments) ? "" : $" {info.Arguments}")}");
9090
}
9191

92-
return builder.ToString();
92+
return builder.ToString().Redact(secrets);
9393
}
9494

95+
private static string Redact(this string value, IEnumerable<string> secrets) =>
96+
secrets.Aggregate(value, (current, secret) => current.Replace(secret, "***", StringComparison.OrdinalIgnoreCase));
97+
9598
private static bool TryKill(this Process process, bool ignoreProcessTree)
9699
{
97100
// exceptions may be thrown for all kinds of reasons

SimpleExec/PublicAPI.Shipped.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ SimpleExec.ExitCodeReadException.StandardError.get -> string!
1111
SimpleExec.ExitCodeReadException.StandardOutput.get -> string!
1212
static SimpleExec.Command.ReadAsync(string! name, string! args = "", string! workingDirectory = "", System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, System.Text.Encoding? encoding = null, System.Func<int, bool>? handleExitCode = null, string? standardInput = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(string! StandardOutput, string! StandardError)>!
1313
static SimpleExec.Command.ReadAsync(string! name, System.Collections.Generic.IEnumerable<string!>! args, string! workingDirectory = "", System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, System.Text.Encoding? encoding = null, System.Func<int, bool>? handleExitCode = null, string? standardInput = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(string! StandardOutput, string! StandardError)>!
14-
static SimpleExec.Command.Run(string! name, string! args = "", string! workingDirectory = "", bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
15-
static SimpleExec.Command.Run(string! name, System.Collections.Generic.IEnumerable<string!>! args, string! workingDirectory = "", bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
16-
static SimpleExec.Command.RunAsync(string! name, string! args = "", string! workingDirectory = "", bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
17-
static SimpleExec.Command.RunAsync(string! name, System.Collections.Generic.IEnumerable<string!>! args, string! workingDirectory = "", bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
14+
static SimpleExec.Command.Run(string! name, string! args = "", string! workingDirectory = "", System.Collections.Generic.IEnumerable<string!>? secrets = null, bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
15+
static SimpleExec.Command.Run(string! name, System.Collections.Generic.IEnumerable<string!>! args, string! workingDirectory = "", System.Collections.Generic.IEnumerable<string!>? secrets = null, bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
16+
static SimpleExec.Command.RunAsync(string! name, string! args = "", string! workingDirectory = "", System.Collections.Generic.IEnumerable<string!>? secrets = null, bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
17+
static SimpleExec.Command.RunAsync(string! name, System.Collections.Generic.IEnumerable<string!>! args, string! workingDirectory = "", System.Collections.Generic.IEnumerable<string!>? secrets = null, bool noEcho = false, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string?>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, bool cancellationIgnoresProcessTree = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!

0 commit comments

Comments
 (0)