Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,9 @@
},
{
Name: SanitizeForLogging
},
{
Name: SanitizeSecrets
}
]
},
Expand Down
7 changes: 1 addition & 6 deletions DecSm.Atom/Logging/MaskingAnsiConsoleOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,7 @@ public override void Write(char value) =>
public override void Write(string? value)
{
if (value is { Length: > 0 })
{
var masker = ServiceStaticAccessor<IParamService>.Service;

if (masker is not null)
value = masker.MaskMatchingSecrets(value);
}
value = ServiceStaticAccessor<IParamService>.Service?.MaskMatchingSecrets(value) ?? value;

_innerWriter.Write(value);
}
Expand Down
6 changes: 4 additions & 2 deletions DecSm.Atom/Logging/SpectreLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public IDisposable BeginScope<TState>(TState state)
/// <remarks>
/// This method formats log messages based on their level, with colors and prefixes. It filters out Trace and Debug
/// messages unless verbose logging is enabled. It also handles special formatting for process output and exceptions.
/// Secrets are not masked here; the custom Spectre console output handles masking.
/// </remarks>
public void Log<TState>(
LogLevel logLevel,
Expand Down Expand Up @@ -121,6 +120,9 @@ public void Log<TState>(
if (message is "(null)")
return;

// If the message contains any secrets, we want to mask them
message = ServiceStaticAccessor<IParamService>.Service?.MaskMatchingSecrets(message) ?? message;

message = message.EscapeMarkup();

if (processOutput)
Expand All @@ -131,7 +133,7 @@ public void Log<TState>(
: "dim";

var columns = new Columns(new Text(" "),
new Markup($"[{messageStyle}]{message.EscapeMarkup()}[/]").LeftJustified()).Collapse();
new Markup($"[{messageStyle}]{message}[/]").LeftJustified()).Collapse();

ServiceStaticAccessor<IAnsiConsole>.Service?.Write(columns);

Expand Down
7 changes: 2 additions & 5 deletions DecSm.Atom/Params/ParamService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public interface IParamService
GetParam<string>(paramName, defaultValue);

/// <summary>
/// Replaces known secret values in the provided text with a mask ("*****").
/// Replaces known secret values in the provided text with a mask (e.g. "*****").
/// </summary>
/// <param name="text">The text to scan and mask.</param>
/// <returns>The text with any resolved secret values replaced by a mask.</returns>
Expand Down Expand Up @@ -157,10 +157,7 @@ public IDisposable CreateOverrideSourcesScope(ParamSource sources) =>

/// <inheritdoc />
public string MaskMatchingSecrets(string text) =>
_knownSecrets.Aggregate(text,
(current, knownSecret) => knownSecret is { Length: > 0 }
? current.Replace(knownSecret, "*****", StringComparison.OrdinalIgnoreCase)
: current);
text.SanitizeSecrets(_knownSecrets);

/// <inheritdoc />
public T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
Expand Down
30 changes: 30 additions & 0 deletions DecSm.Atom/Util/StringUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,35 @@ public int GetLevenshteinDistance(string? compareTo)

return @string[..maxLength] + "...";
}

/// <summary>
/// Sanitizes a string by replacing specified secret substrings with asterisks or a fixed mask, ensuring that the
/// secrets are not exposed in logs or outputs.
/// </summary>
/// <param name="secrets">
/// A list of secret substrings to be sanitized from the input string. Secrets that are null or empty
/// will be ignored.
/// </param>
/// <returns>
/// The sanitized string with secrets replaced, or <c>null</c> if the input string was <c>null</c> or empty, or if no
/// valid secrets were provided. If the input string is shorter than the shortest valid secret, it will be returned
/// unchanged.
/// </returns>
[return: NotNullIfNotNull(nameof(@string))]
public string? SanitizeSecrets(List<string> secrets)
{
var validSecrets = secrets
.Where(s => !string.IsNullOrEmpty(s))
.ToList();

return @string is null or "" || validSecrets.Count is 0 || @string.Length < validSecrets.Min(s => s.Length)
? @string
: validSecrets.Aggregate(@string,
static (current, secret) => current.Replace(secret,
secret.Length < 5
? new('*', secret.Length)
: "*****",
StringComparison.OrdinalIgnoreCase));
}
}
}
Loading