diff --git a/src/Runner.Common/HostContext.cs b/src/Runner.Common/HostContext.cs index d4ea48c39..2bdc6452d 100644 --- a/src/Runner.Common/HostContext.cs +++ b/src/Runner.Common/HostContext.cs @@ -90,6 +90,8 @@ namespace GitHub.Runner.Common this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes); + this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape); + this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape); // Create the trace manager. if (string.IsNullOrEmpty(logFile)) diff --git a/src/Sdk/DTLogging/Logging/ValueEncoders.cs b/src/Sdk/DTLogging/Logging/ValueEncoders.cs index 3f30dd540..c3c065bc2 100644 --- a/src/Sdk/DTLogging/Logging/ValueEncoders.cs +++ b/src/Sdk/DTLogging/Logging/ValueEncoders.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Security; using System.Text; +using System.Text.RegularExpressions; using Newtonsoft.Json; namespace GitHub.DistributedTask.Logging @@ -80,6 +81,65 @@ namespace GitHub.DistributedTask.Logging return trimmed; } + public static String PowerShellPreAmpersandEscape(String value) + { + // if the secret is passed to PS as a command and it causes an error, sections of it can be surrounded by color codes + // or printed individually. + + // The secret secretpart1&secretpart2&secretpart3 would be split into 2 sections: + // 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section. + + // The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections: + // 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section. + + var trimmed = string.Empty; + if (!string.IsNullOrEmpty(value) && value.Contains("&")) + { + var secretSection = string.Empty; + if (value.Contains("&+")) + { + secretSection = value.Substring(0, value.IndexOf("&+") + "&+".Length); + } + else + { + secretSection = value.Substring(0, value.LastIndexOf("&") + "&".Length); + } + + // Don't mask short secrets + if (secretSection.Length >= 6) + { + trimmed = secretSection; + } + } + + return trimmed; + } + + public static String PowerShellPostAmpersandEscape(String value) + { + var trimmed = string.Empty; + if (!string.IsNullOrEmpty(value) && value.Contains("&")) + { + var secretSection = string.Empty; + if (value.Contains("&+")) + { + // +1 to skip the letter that got colored + secretSection = value.Substring(value.IndexOf("&+") + "&+".Length + 1); + } + else + { + secretSection = value.Substring(value.LastIndexOf("&") + "&".Length); + } + + if (secretSection.Length >= 6) + { + trimmed = secretSection; + } + } + + return trimmed; + } + private static string Base64StringEscapeShift(String value, int shift) { var bytes = Encoding.UTF8.GetBytes(value); diff --git a/src/Test/L0/HostContextL0.cs b/src/Test/L0/HostContextL0.cs index 9e5c52901..488064ad8 100644 --- a/src/Test/L0/HostContextL0.cs +++ b/src/Test/L0/HostContextL0.cs @@ -112,6 +112,36 @@ namespace GitHub.Runner.Common.Tests } } + [Theory] + [InlineData("secret&secret&secret", "secret&secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")] + [InlineData("secret&secret+secret", "secret&\x0033[96msecret+secret\x0033[0m", "***\x0033[96m***\x0033[0m")] + [InlineData("secret+secret&secret", "secret+secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")] + [InlineData("secret&secret&+secretsecret", "secret&secret&+\x0033[96ms\x0033[0mecretsecret", "***\x0033[96ms\x0033[0m***")] + [InlineData("secret&+secret&secret", "secret&+\x0033[96ms\x0033[0mecret&secret", "***\x0033[96ms\x0033[0m***")] + [InlineData("secret&+secret&+secret", "secret&+\x0033[96ms\x0033[0mecret&+secret", "***\x0033[96ms\x0033[0m***")] + [InlineData("secret&+secret&secret&+secret", "secret&+\x0033[96ms\x0033[0mecret&secret&+secret", "***\x0033[96ms\x0033[0m***")] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void SecretSectionMasking(string secret, string rawOutput, string maskedOutput) + { + try + { + // Arrange. + Setup(); + + // Act. + _hc.SecretMasker.AddValue(secret); + + // Assert. + Assert.Equal(maskedOutput, _hc.SecretMasker.MaskSecrets(rawOutput)); + } + finally + { + // Cleanup. + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")]