mirror of
https://github.com/actions/runner.git
synced 2025-12-15 06:26:46 +00:00
GitHub Actions Runner
This commit is contained in:
13
src/Sdk/DTLogging/Logging/ISecret.cs
Normal file
13
src/Sdk/DTLogging/Logging/ISecret.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
internal interface ISecret
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns one item (start, length) for each match found in the input string.
|
||||
/// </summary>
|
||||
IEnumerable<ReplacementPosition> GetPositions(String input);
|
||||
}
|
||||
}
|
||||
15
src/Sdk/DTLogging/Logging/ISecretMasker.cs
Normal file
15
src/Sdk/DTLogging/Logging/ISecretMasker.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ISecretMasker
|
||||
{
|
||||
void AddRegex(String pattern);
|
||||
void AddValue(String value);
|
||||
void AddValueEncoder(ValueEncoder encoder);
|
||||
ISecretMasker Clone();
|
||||
String MaskSecrets(String input);
|
||||
}
|
||||
}
|
||||
50
src/Sdk/DTLogging/Logging/RegexSecret.cs
Normal file
50
src/Sdk/DTLogging/Logging/RegexSecret.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using GitHub.Services.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
internal sealed class RegexSecret : ISecret
|
||||
{
|
||||
public RegexSecret(String pattern)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(pattern, nameof(pattern));
|
||||
m_pattern = pattern;
|
||||
m_regex = new Regex(pattern);
|
||||
}
|
||||
|
||||
public override Boolean Equals(Object obj)
|
||||
{
|
||||
var item = obj as RegexSecret;
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return String.Equals(m_pattern, item.m_pattern, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => m_pattern.GetHashCode();
|
||||
|
||||
public IEnumerable<ReplacementPosition> GetPositions(String input)
|
||||
{
|
||||
Int32 startIndex = 0;
|
||||
while (startIndex < input.Length)
|
||||
{
|
||||
var match = m_regex.Match(input, startIndex);
|
||||
if (match.Success)
|
||||
{
|
||||
startIndex = match.Index + 1;
|
||||
yield return new ReplacementPosition(match.Index, match.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly String m_pattern;
|
||||
private readonly Regex m_regex;
|
||||
}
|
||||
}
|
||||
29
src/Sdk/DTLogging/Logging/ReplacementPosition.cs
Normal file
29
src/Sdk/DTLogging/Logging/ReplacementPosition.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
internal sealed class ReplacementPosition
|
||||
{
|
||||
public ReplacementPosition(Int32 start, Int32 length)
|
||||
{
|
||||
Start = start;
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public ReplacementPosition(ReplacementPosition copy)
|
||||
{
|
||||
Start = copy.Start;
|
||||
Length = copy.Length;
|
||||
}
|
||||
|
||||
public Int32 Start { get; set; }
|
||||
public Int32 Length { get; set; }
|
||||
public Int32 End
|
||||
{
|
||||
get
|
||||
{
|
||||
return Start + Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
298
src/Sdk/DTLogging/Logging/SecretMasker.cs
Normal file
298
src/Sdk/DTLogging/Logging/SecretMasker.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class SecretMasker : ISecretMasker, IDisposable
|
||||
{
|
||||
public SecretMasker()
|
||||
{
|
||||
m_originalValueSecrets = new HashSet<ValueSecret>();
|
||||
m_regexSecrets = new HashSet<RegexSecret>();
|
||||
m_valueEncoders = new HashSet<ValueEncoder>();
|
||||
m_valueSecrets = new HashSet<ValueSecret>();
|
||||
}
|
||||
|
||||
private SecretMasker(SecretMasker copy)
|
||||
{
|
||||
// Read section.
|
||||
try
|
||||
{
|
||||
copy.m_lock.EnterReadLock();
|
||||
|
||||
// Copy the hash sets.
|
||||
m_originalValueSecrets = new HashSet<ValueSecret>(copy.m_originalValueSecrets);
|
||||
m_regexSecrets = new HashSet<RegexSecret>(copy.m_regexSecrets);
|
||||
m_valueEncoders = new HashSet<ValueEncoder>(copy.m_valueEncoders);
|
||||
m_valueSecrets = new HashSet<ValueSecret>(copy.m_valueSecrets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (copy.m_lock.IsReadLockHeld)
|
||||
{
|
||||
copy.m_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
|
||||
/// </summary>
|
||||
public void AddRegex(String pattern)
|
||||
{
|
||||
// Test for empty.
|
||||
if (String.IsNullOrEmpty(pattern))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Write section.
|
||||
try
|
||||
{
|
||||
m_lock.EnterWriteLock();
|
||||
|
||||
// Add the value.
|
||||
m_regexSecrets.Add(new RegexSecret(pattern));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsWriteLockHeld)
|
||||
{
|
||||
m_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
|
||||
/// </summary>
|
||||
public void AddValue(String value)
|
||||
{
|
||||
// Test for empty.
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var valueSecrets = new List<ValueSecret>(new[] { new ValueSecret(value) });
|
||||
|
||||
// Read section.
|
||||
ValueEncoder[] valueEncoders;
|
||||
try
|
||||
{
|
||||
m_lock.EnterReadLock();
|
||||
|
||||
// Test whether already added.
|
||||
if (m_originalValueSecrets.Contains(valueSecrets[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the value encoders.
|
||||
valueEncoders = m_valueEncoders.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsReadLockHeld)
|
||||
{
|
||||
m_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the encoded values.
|
||||
foreach (ValueEncoder valueEncoder in valueEncoders)
|
||||
{
|
||||
String encodedValue = valueEncoder(value);
|
||||
if (!String.IsNullOrEmpty(encodedValue))
|
||||
{
|
||||
valueSecrets.Add(new ValueSecret(encodedValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Write section.
|
||||
try
|
||||
{
|
||||
m_lock.EnterWriteLock();
|
||||
|
||||
// Add the values.
|
||||
m_originalValueSecrets.Add(valueSecrets[0]);
|
||||
foreach (ValueSecret valueSecret in valueSecrets)
|
||||
{
|
||||
m_valueSecrets.Add(valueSecret);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsWriteLockHeld)
|
||||
{
|
||||
m_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
|
||||
/// </summary>
|
||||
public void AddValueEncoder(ValueEncoder encoder)
|
||||
{
|
||||
ValueSecret[] originalSecrets;
|
||||
|
||||
// Read section.
|
||||
try
|
||||
{
|
||||
m_lock.EnterReadLock();
|
||||
|
||||
// Test whether already added.
|
||||
if (m_valueEncoders.Contains(encoder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the original value secrets.
|
||||
originalSecrets = m_originalValueSecrets.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsReadLockHeld)
|
||||
{
|
||||
m_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the encoded values.
|
||||
var encodedSecrets = new List<ValueSecret>();
|
||||
foreach (ValueSecret originalSecret in originalSecrets)
|
||||
{
|
||||
String encodedValue = encoder(originalSecret.m_value);
|
||||
if (!String.IsNullOrEmpty(encodedValue))
|
||||
{
|
||||
encodedSecrets.Add(new ValueSecret(encodedValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Write section.
|
||||
try
|
||||
{
|
||||
m_lock.EnterWriteLock();
|
||||
|
||||
// Add the encoder.
|
||||
m_valueEncoders.Add(encoder);
|
||||
|
||||
// Add the values.
|
||||
foreach (ValueSecret encodedSecret in encodedSecrets)
|
||||
{
|
||||
m_valueSecrets.Add(encodedSecret);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsWriteLockHeld)
|
||||
{
|
||||
m_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ISecretMasker Clone() => new SecretMasker(this);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_lock?.Dispose();
|
||||
m_lock = null;
|
||||
}
|
||||
|
||||
public String MaskSecrets(String input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(input))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
var secretPositions = new List<ReplacementPosition>();
|
||||
|
||||
// Read section.
|
||||
try
|
||||
{
|
||||
m_lock.EnterReadLock();
|
||||
|
||||
// Get indexes and lengths of all substrings that will be replaced.
|
||||
foreach (RegexSecret regexSecret in m_regexSecrets)
|
||||
{
|
||||
secretPositions.AddRange(regexSecret.GetPositions(input));
|
||||
}
|
||||
|
||||
foreach (ValueSecret valueSecret in m_valueSecrets)
|
||||
{
|
||||
secretPositions.AddRange(valueSecret.GetPositions(input));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (m_lock.IsReadLockHeld)
|
||||
{
|
||||
m_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Short-circuit if nothing to replace.
|
||||
if (secretPositions.Count == 0)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
// Merge positions into ranges of characters to replace.
|
||||
List<ReplacementPosition> replacementPositions = new List<ReplacementPosition>();
|
||||
ReplacementPosition currentReplacement = null;
|
||||
foreach (ReplacementPosition secretPosition in secretPositions.OrderBy(x => x.Start))
|
||||
{
|
||||
if (currentReplacement == null)
|
||||
{
|
||||
currentReplacement = new ReplacementPosition(copy: secretPosition);
|
||||
replacementPositions.Add(currentReplacement);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (secretPosition.Start <= currentReplacement.End)
|
||||
{
|
||||
// Overlap
|
||||
currentReplacement.Length = Math.Max(currentReplacement.End, secretPosition.End) - currentReplacement.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No overlap
|
||||
currentReplacement = new ReplacementPosition(copy: secretPosition);
|
||||
replacementPositions.Add(currentReplacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace
|
||||
var stringBuilder = new StringBuilder();
|
||||
Int32 startIndex = 0;
|
||||
foreach (var replacement in replacementPositions)
|
||||
{
|
||||
stringBuilder.Append(input.Substring(startIndex, replacement.Start - startIndex));
|
||||
stringBuilder.Append("***");
|
||||
startIndex = replacement.Start + replacement.Length;
|
||||
}
|
||||
|
||||
if (startIndex < input.Length)
|
||||
{
|
||||
stringBuilder.Append(input.Substring(startIndex));
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private readonly HashSet<ValueSecret> m_originalValueSecrets;
|
||||
private readonly HashSet<RegexSecret> m_regexSecrets;
|
||||
private readonly HashSet<ValueEncoder> m_valueEncoders;
|
||||
private readonly HashSet<ValueSecret> m_valueSecrets;
|
||||
private ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||
}
|
||||
}
|
||||
116
src/Sdk/DTLogging/Logging/ValueEncoders.cs
Normal file
116
src/Sdk/DTLogging/Logging/ValueEncoders.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public delegate String ValueEncoder(String value);
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ValueEncoders
|
||||
{
|
||||
public static String Base64StringEscape(String value)
|
||||
{
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
// Base64 is 6 bytes -> char
|
||||
// When end user doing somthing like base64(user:password)
|
||||
// The length of the leading content will cause different base64 encoding result on the password
|
||||
// So we add base64(value - 1/2/3/4/5 bytes) as secret as well.
|
||||
public static String Base64StringEscapeShift1(String value)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 1);
|
||||
}
|
||||
|
||||
public static String Base64StringEscapeShift2(String value)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 2);
|
||||
}
|
||||
|
||||
public static String Base64StringEscapeShift3(String value)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 3);
|
||||
}
|
||||
|
||||
public static String Base64StringEscapeShift4(String value)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 4);
|
||||
}
|
||||
|
||||
public static String Base64StringEscapeShift5(String value)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 5);
|
||||
}
|
||||
|
||||
public static String ExpressionStringEscape(String value)
|
||||
{
|
||||
return Expressions.ExpressionUtil.StringEscape(value);
|
||||
}
|
||||
|
||||
public static String JsonStringEscape(String value)
|
||||
{
|
||||
// Convert to a JSON string and then remove the leading/trailing double-quote.
|
||||
String jsonString = JsonConvert.ToString(value);
|
||||
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
|
||||
return jsonEscapedValue;
|
||||
}
|
||||
|
||||
public static String UriDataEscape(String value)
|
||||
{
|
||||
return UriDataEscape(value, 65519);
|
||||
}
|
||||
|
||||
public static String XmlDataEscape(String value)
|
||||
{
|
||||
return SecurityElement.Escape(value);
|
||||
}
|
||||
|
||||
private static string Base64StringEscapeShift(String value, int shift)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
if (bytes.Length > shift)
|
||||
{
|
||||
var shiftArray = new byte[bytes.Length - shift];
|
||||
Array.Copy(bytes, shift, shiftArray, 0, bytes.Length - shift);
|
||||
return Convert.ToBase64String(shiftArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static String UriDataEscape(
|
||||
String value,
|
||||
Int32 maxSegmentSize)
|
||||
{
|
||||
if (value.Length <= maxSegmentSize)
|
||||
{
|
||||
return Uri.EscapeDataString(value);
|
||||
}
|
||||
|
||||
// Workaround size limitation in Uri.EscapeDataString
|
||||
var result = new StringBuilder();
|
||||
var i = 0;
|
||||
do
|
||||
{
|
||||
var length = Math.Min(value.Length - i, maxSegmentSize);
|
||||
|
||||
if (Char.IsHighSurrogate(value[i + length - 1]) && length > 1)
|
||||
{
|
||||
length--;
|
||||
}
|
||||
|
||||
result.Append(Uri.EscapeDataString(value.Substring(i, length)));
|
||||
i += length;
|
||||
}
|
||||
while (i < value.Length);
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Sdk/DTLogging/Logging/ValueSecret.cs
Normal file
48
src/Sdk/DTLogging/Logging/ValueSecret.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.Services.Common;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
internal sealed class ValueSecret : ISecret
|
||||
{
|
||||
public ValueSecret(String value)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(value, nameof(value));
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
public override Boolean Equals(Object obj)
|
||||
{
|
||||
var item = obj as ValueSecret;
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return String.Equals(m_value, item.m_value, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override Int32 GetHashCode() => m_value.GetHashCode();
|
||||
|
||||
public IEnumerable<ReplacementPosition> GetPositions(String input)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(input) && !String.IsNullOrEmpty(m_value))
|
||||
{
|
||||
Int32 startIndex = 0;
|
||||
while (startIndex > -1 &&
|
||||
startIndex < input.Length &&
|
||||
input.Length - startIndex >= m_value.Length) // remaining substring longer than secret value
|
||||
{
|
||||
startIndex = input.IndexOf(m_value, startIndex, StringComparison.Ordinal);
|
||||
if (startIndex > -1)
|
||||
{
|
||||
yield return new ReplacementPosition(startIndex, m_value.Length);
|
||||
++startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly String m_value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user