Skip to content

Commit 8bb4237

Browse files
authored
Mask feed credentials for CLI commands + remove superfluous risky logs (#1627)
* Mask feed credentials for CLI commands + remove superfluous risky logs
1 parent 48571b6 commit 8bb4237

File tree

7 files changed

+351
-14
lines changed

7 files changed

+351
-14
lines changed

src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,15 @@ public async Task<CommandLineExecutionResult> ExecuteCommandAsync(
7878

7979
var pathToRun = this.commandLocatableCache[command];
8080
var joinedParameters = string.Join(" ", parameters);
81-
var commandForLogging = joinedParameters.RemoveSensitiveInformation();
8281
try
8382
{
8483
var result = await RunProcessAsync(pathToRun, joinedParameters, workingDirectory, cancellationToken);
85-
record.Track(result, pathToRun, commandForLogging);
84+
record.Track(result, pathToRun, joinedParameters);
8685
return result;
8786
}
8887
catch (Exception ex)
8988
{
90-
record.Track(ex, pathToRun, commandForLogging);
89+
record.Track(ex, pathToRun, joinedParameters);
9190
throw;
9291
}
9392
}

src/Microsoft.ComponentDetection.Common/Telemetry/CommandLineTelemetryService.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Microsoft.ComponentDetection.Common.Telemetry;
33

44
using System;
55
using System.Collections.Concurrent;
6+
using System.Linq;
67
using System.Text.Json;
78
using System.Text.Json.Nodes;
89
using Microsoft.ComponentDetection.Common.Telemetry.Records;
@@ -45,6 +46,9 @@ public void PostRecord(IDetectionTelemetryRecord record)
4546
jsonRecord["Timestamp"] = DateTime.UtcNow;
4647
jsonRecord["CorrelationId"] = TelemetryConstants.CorrelationId;
4748

49+
// Mask sensitive information in all string values before storing/logging
50+
MaskSensitiveInformation(jsonRecord);
51+
4852
this.records.Enqueue(jsonRecord);
4953

5054
if (this.telemetryMode == TelemetryMode.Debug)
@@ -55,4 +59,51 @@ public void PostRecord(IDetectionTelemetryRecord record)
5559

5660
/// <inheritdoc/>
5761
public void SetMode(TelemetryMode mode) => this.telemetryMode = mode;
62+
63+
/// <summary>
64+
/// Recursively masks sensitive information in all string values within a JSON node.
65+
/// </summary>
66+
/// <param name="node">The JSON node to process.</param>
67+
private static void MaskSensitiveInformation(JsonNode node)
68+
{
69+
if (node == null)
70+
{
71+
return;
72+
}
73+
74+
if (node is JsonObject jsonObject)
75+
{
76+
// Get keys first to avoid collection modified during enumeration
77+
var keys = jsonObject.Select(p => p.Key).ToList();
78+
foreach (var key in keys)
79+
{
80+
var value = jsonObject[key];
81+
if (value is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var stringValue))
82+
{
83+
// Mask sensitive info in string values
84+
jsonObject[key] = stringValue.RemoveSensitiveInformation();
85+
}
86+
else
87+
{
88+
// Recurse into nested objects and arrays
89+
MaskSensitiveInformation(value);
90+
}
91+
}
92+
}
93+
else if (node is JsonArray jsonArray)
94+
{
95+
for (var index = 0; index < jsonArray.Count; index++)
96+
{
97+
var item = jsonArray[index];
98+
if (item is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var stringValue))
99+
{
100+
jsonArray[index] = stringValue.RemoveSensitiveInformation();
101+
}
102+
else
103+
{
104+
MaskSensitiveInformation(item);
105+
}
106+
}
107+
}
108+
}
58109
}

src/Microsoft.ComponentDetection.Common/Telemetry/Records/CommandLineInvocationTelemetryRecord.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@ public class CommandLineInvocationTelemetryRecord : BaseDetectionTelemetryRecord
2020
internal void Track(CommandLineExecutionResult result, string path, string parameters)
2121
{
2222
this.ExitCode = result.ExitCode;
23-
this.StandardError = result.StdErr;
23+
this.StandardError = result.StdErr?.RemoveSensitiveInformation();
2424
this.TrackCommon(path, parameters);
2525
}
2626

2727
internal void Track(Exception ex, string path, string parameters)
2828
{
2929
this.ExitCode = -1;
30-
this.UnhandledException = ex.ToString();
30+
this.UnhandledException = ex.ToString().RemoveSensitiveInformation();
3131
this.TrackCommon(path, parameters);
3232
}
3333

3434
private void TrackCommon(string path, string parameters)
3535
{
3636
this.PathThatWasRan = path;
37-
this.Parameters = parameters;
37+
this.Parameters = parameters?.RemoveSensitiveInformation();
3838
this.StopExecutionTimer();
3939
}
4040
}

src/Microsoft.ComponentDetection.Common/Utilities/StringUtilities.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ namespace Microsoft.ComponentDetection.Common;
66

77
public static class StringUtilities
88
{
9-
private static readonly Regex SensitiveInfoRegex = new Regex(@"(?<=https://)(.+)(?=@)", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5));
9+
// Matches credentials in URLs like http://user:password@host or https://user:password@host
10+
private static readonly Regex UrlCredentialsRegex = new(@"(?<=https?://)(.+?)(?=@)", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5));
11+
12+
// Matches SAS tokens in query strings (e.g., ?sv=...&sig=... or ?se=...&sig=...)
13+
// SAS tokens contain a 'sig' parameter which is the signature - we mask the entire query string
14+
private static readonly Regex SasTokenRegex = new(@"\?[^""'\s]*sig=[^""'\s]*", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5));
15+
1016
public const string SensitivePlaceholder = "******";
1117

1218
/// <summary>
13-
/// Utility method to remove sensitive information from a string, currently focused on removing on the credentials placed within URL which can be part of CLI commands.
19+
/// Utility method to remove sensitive information from a string, including:
20+
/// - Credentials in URLs (e.g., https://user:password@host)
21+
/// - SAS tokens in query strings (e.g., ?sv=...&amp;sig=...).
1422
/// </summary>
1523
/// <param name="inputString">String with possible credentials.</param>
16-
/// <returns>New string identical to original string, except credentials in URL are replaced with placeholders.</returns>
24+
/// <returns>New string identical to original string, except sensitive information is replaced with placeholders.</returns>
1725
public static string RemoveSensitiveInformation(this string inputString)
1826
{
1927
if (string.IsNullOrWhiteSpace(inputString))
@@ -23,7 +31,9 @@ public static string RemoveSensitiveInformation(this string inputString)
2331

2432
try
2533
{
26-
return SensitiveInfoRegex.Replace(inputString, SensitivePlaceholder);
34+
var result = UrlCredentialsRegex.Replace(inputString, SensitivePlaceholder);
35+
result = SasTokenRegex.Replace(result, $"?{SensitivePlaceholder}");
36+
return result;
2737
}
2838
catch (Exception)
2939
{

src/Microsoft.ComponentDetection.Detectors/pip/PipCommandService.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,10 @@ private async Task<CommandLineExecutionResult> ExecuteCommandAsync(
192192

193193
if (command.ExitCode == -1 && cancellationToken.IsCancellationRequested)
194194
{
195-
var errorMessage = $"PipReport: Cancelled for file '{formattedPath}' with command '{pipReportCommand.RemoveSensitiveInformation()}'.";
195+
var errorMessage = $"PipReport: Cancelled for file '{formattedPath}'.";
196196
using var failureRecord = new PipReportFailureTelemetryRecord
197197
{
198198
ExitCode = command.ExitCode,
199-
StdErr = $"{errorMessage} {command.StdErr}",
200199
};
201200

202201
this.logger.LogWarning("{Error}", errorMessage);
@@ -207,10 +206,9 @@ private async Task<CommandLineExecutionResult> ExecuteCommandAsync(
207206
using var failureRecord = new PipReportFailureTelemetryRecord
208207
{
209208
ExitCode = command.ExitCode,
210-
StdErr = command.StdErr,
211209
};
212210

213-
this.logger.LogDebug("PipReport: Pip installation report error: {StdErr}", command.StdErr);
211+
this.logger.LogDebug("PipReport: Pip installation report failed with exit code {ExitCode} for {Path}", command.ExitCode, formattedPath);
214212
throw new InvalidOperationException($"PipReport: Failed to generate pip installation report for file {path} with exit code {command.ExitCode}");
215213
}
216214

0 commit comments

Comments
 (0)