Skip to content
Open
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
120 changes: 112 additions & 8 deletions src/ModularPipelines/Engine/Executors/PipelineInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Collections;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ModularPipelines.Constants;
using ModularPipelines.Enums;
using ModularPipelines.Helpers;
using ModularPipelines.Models;
using ModularPipelines.Options;
using Spectre.Console;

namespace ModularPipelines.Engine.Executors;
Expand All @@ -18,8 +21,41 @@ internal class PipelineInitializer(
IPipelineFileWriter pipelineFileWriter,
ILogger<PipelineInitializer> logger,
IConsoleWriter consoleWriter,
ISecretObfuscator secretObfuscator) : IPipelineInitializer
ISecretObfuscator secretObfuscator,
IOptions<SecretMaskingOptions> secretMaskingOptions) : IPipelineInitializer
{
// Two outer borders, one separator, and one space of padding on each side of both columns.
private const int TableDecorationWidth = 7;

private static readonly string[] SensitiveEnvironmentVariableNameParts =
[
"TOKEN",
"SECRET",
"PASSWORD",
Comment thread
thomhurst marked this conversation as resolved.
"PASSPHRASE",
"KEY",
"PWD",
Comment thread
thomhurst marked this conversation as resolved.
"CREDENTIAL",
"AUTH",
Comment thread
thomhurst marked this conversation as resolved.
Comment thread
thomhurst marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exempt the standard XAUTHORITY path

When trace logging runs in an X11 desktop or SSH X-forwarding session, the standard XAUTHORITY variable contains the path to the X authentication file, but the undelimited AUTH match replaces that non-secret path with the secret mask. This removes useful environment diagnostics in the same way as the exempted SSH_AUTH_SOCK; exempt XAUTHORITY or match authentication markers at name boundaries.

Useful? React with 👍 / 👎.

"CONNECTION_STRING",
"CONNECTIONSTRING",
"CONNSTR",
Comment thread
thomhurst marked this conversation as resolved.
];
Comment thread
thomhurst marked this conversation as resolved.
Comment thread
thomhurst marked this conversation as resolved.

private static readonly string[] SensitiveEnvironmentVariableNames =
Comment thread
thomhurst marked this conversation as resolved.
[
"AzureWebJobsStorage",
"ALL_PROXY",
"DATABASE_URL",
Comment thread
thomhurst marked this conversation as resolved.
"HTTP_PROXY",
"HTTPS_PROXY",
"MONGODB_URI",
"PIP_EXTRA_INDEX_URL",
"PIP_INDEX_URL",
"REDIS_URL",
Comment thread
thomhurst marked this conversation as resolved.
"VSS_NUGET_EXTERNAL_FEED_ENDPOINTS",
];
Comment thread
thomhurst marked this conversation as resolved.

private static readonly Action<ILogger, BuildSystem, string, Exception?> LogDetectedBuildSystem =
LoggerMessage.Define<BuildSystem, string>(
LogLevel.Information,
Expand All @@ -43,6 +79,7 @@ internal class PipelineInitializer(
private readonly ILogger<PipelineInitializer> _logger = logger;
private readonly IConsoleWriter _consoleWriter = consoleWriter;
private readonly ISecretObfuscator _secretObfuscator = secretObfuscator;
private readonly IOptions<SecretMaskingOptions> _secretMaskingOptions = secretMaskingOptions;
private OrganizedModules? _organizedModules;

public async Task<OrganizedModules> Initialize(CancellationToken cancellationToken = default)
Expand All @@ -52,8 +89,26 @@ public async Task<OrganizedModules> Initialize(CancellationToken cancellationTok

internal static Table CreateEnvironmentVariablesTable(
IDictionary variables,
Func<string, string> obfuscate)
Func<string, string> obfuscate,
string maskValue = LoggingConstants.SecretMask,
int? consoleWidth = null)
{
var effectiveMaskValue = string.IsNullOrWhiteSpace(maskValue)
? LoggingConstants.SecretMask
: maskValue;
var entries = variables
.Cast<DictionaryEntry>()
.OrderBy(entry => entry.Key?.ToString(), StringComparer.OrdinalIgnoreCase)
.ToArray();
var maximumNameWidth = Math.Max(
GetMaximumCellWidth("Name"),
entries
.Select(entry => GetMaximumCellWidth(entry.Key?.ToString() ?? string.Empty))
.DefaultIfEmpty()
.Max());
var maximumValueWidth = Math.Max(
0,
(consoleWidth ?? AnsiConsole.Console.Profile.Width) - maximumNameWidth - TableDecorationWidth);
Comment thread
thomhurst marked this conversation as resolved.
var table = new Table
{
Border = TableBorder.Rounded,
Expand All @@ -64,18 +119,66 @@ internal static Table CreateEnvironmentVariablesTable(
table.AddColumn(new TableColumn("[bold]Name[/]").LeftAligned());
table.AddColumn(new TableColumn("[bold]Value[/]").LeftAligned());

foreach (var environmentVariable in variables
.Cast<DictionaryEntry>()
.OrderBy(entry => entry.Key?.ToString(), StringComparer.OrdinalIgnoreCase))
foreach (var environmentVariable in entries)
{
var name = environmentVariable.Key?.ToString() ?? string.Empty;
var value = environmentVariable.Value?.ToString() ?? string.Empty;
var obfuscatedValue = obfuscate(value);
var displayValue = IsSensitiveEnvironmentVariableName(name)
|| RequiresUnsafeRendering(
value,
obfuscatedValue,
effectiveMaskValue,
maximumValueWidth)
? effectiveMaskValue
: MakeSingleLine(obfuscatedValue);

table.AddRow(
Markup.Escape(environmentVariable.Key?.ToString() ?? string.Empty),
Markup.Escape(obfuscate(environmentVariable.Value?.ToString() ?? string.Empty)));
new Text(name),
new Text(displayValue).Ellipsis());
}

return table;
}

private static bool RequiresUnsafeRendering(
string value,
string obfuscatedValue,
string maskValue,
int maximumValueWidth)
{
if (!value.Equals(obfuscatedValue, StringComparison.Ordinal))
{
return !obfuscatedValue.Equals(maskValue, StringComparison.Ordinal);
}

return GetMaximumCellWidth(value) > maximumValueWidth
|| value.Any(char.IsControl);
}

private static int GetMaximumCellWidth(string value)
{
return value.Sum(character => char.IsAscii(character) ? 1 : 2);
}

private static bool IsSensitiveEnvironmentVariableName(string name)
{
return !name.Equals("PWD", StringComparison.OrdinalIgnoreCase)
&& !name.Equals("OLDPWD", StringComparison.OrdinalIgnoreCase)
&& !name.Equals("SSH_AUTH_SOCK", StringComparison.OrdinalIgnoreCase)
&& !name.StartsWith("GIT_AUTHOR_", StringComparison.OrdinalIgnoreCase)
&& (SensitiveEnvironmentVariableNames.Contains(name, StringComparer.OrdinalIgnoreCase)
|| SensitiveEnvironmentVariableNameParts.Any(
part => name.Contains(part, StringComparison.OrdinalIgnoreCase)));
}

private static string MakeSingleLine(string value)
{
return value
.Replace("\r", "\\r", StringComparison.Ordinal)
.Replace("\n", "\\n", StringComparison.Ordinal);
}

private async Task<OrganizedModules> InitializeInternal(CancellationToken cancellationToken)
{
_consolePrinter.PrintLogo();
Expand Down Expand Up @@ -114,6 +217,7 @@ private void PrintEnvironmentVariables()

_consoleWriter.Write(CreateEnvironmentVariablesTable(
Environment.GetEnvironmentVariables(),
value => _secretObfuscator.Obfuscate(value, null)));
value => _secretObfuscator.Obfuscate(value, null),
_secretMaskingOptions.Value.MaskValue));
}
}
Loading
Loading