diff --git a/Directory.Packages.props b/Directory.Packages.props
index b035ee9ce..7abe6d757 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,7 +17,7 @@
-
+
diff --git a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
index 9c6960988..b39c03766 100644
--- a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
+++ b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
@@ -79,6 +79,8 @@ public partial class BuildControl : UserControl
private PropertiesAndItemsSearch propertiesAndItemsSearch;
+ private SecretsSearch secretsSearch;
+
public BuildControl(Build build, string logFilePath)
{
InitializeComponent();
@@ -137,6 +139,7 @@ public BuildControl(Build build, string logFilePath)
propertiesAndItemsControl.WatermarkDisplayed += UpdatePropertiesAndItemsWatermark;
propertiesAndItemsControl.RecentItemsCategory = "PropertiesAndItems";
+ secretsSearch = (SecretsSearch)build.SearchExtensions.FirstOrDefault(se => se is SecretsSearch);
SetProjectContext(null);
VirtualizingPanel.SetIsVirtualizing(treeView, SettingsService.EnableTreeViewVirtualization);
@@ -643,6 +646,7 @@ private void PopulateProjectGraph()
"$task $time",
"$message CompilerServer failed",
"will be compiled because",
+ "$secret"
};
private static string[] nodeKinds = new[]
@@ -662,7 +666,8 @@ private void PopulateProjectGraph()
"$csc",
"$rar",
"$import",
- "$noimport"
+ "$noimport",
+ "$secret"
};
private static Inline MakeLink(string query, SearchAndResultsControl searchControl, string before = " \u2022 ", string after = "\r\n")
@@ -1017,6 +1022,9 @@ private object FindInFiles(string searchText, int maxResults, CancellationToken
{
var results = new List<(string, IEnumerable<(int, string)>)>();
+ NodeQueryMatcher notQueryMatcher = new NodeQueryMatcher(searchText);
+ bool isSecretsSearch = !string.IsNullOrEmpty(searchText) && searchText.StartsWith("$secret");
+
foreach (var file in archiveFile.Files)
{
if (cancellationToken.IsCancellationRequested)
@@ -1024,11 +1032,22 @@ private object FindInFiles(string searchText, int maxResults, CancellationToken
return null;
}
- var haystack = file.Value;
- var resultsInFile = haystack.Find(searchText);
- if (resultsInFile.Count > 0)
+ if (isSecretsSearch)
{
- results.Add((file.Key, resultsInFile.Select(lineNumber => (lineNumber, haystack.GetLineText(lineNumber)))));
+ var searchResults = secretsSearch.SearchSecrets(file.Value.Text, notQueryMatcher.NotMatchers, maxResults);
+ if (searchResults.Count > 0)
+ {
+ results.Add((file.Key, searchResults.Select(sr => (sr.Line - 1, sr.Secret))));
+ }
+ }
+ else
+ {
+ var haystack = file.Value;
+ var resultsInFile = haystack.Find(searchText);
+ if (resultsInFile.Count > 0)
+ {
+ results.Add((file.Key, resultsInFile.Select(lineNumber => (lineNumber, haystack.GetLineText(lineNumber)))));
+ }
}
}
diff --git a/src/StructuredLogViewer/MainWindow.xaml.cs b/src/StructuredLogViewer/MainWindow.xaml.cs
index 27a0e9217..c90565e9d 100644
--- a/src/StructuredLogViewer/MainWindow.xaml.cs
+++ b/src/StructuredLogViewer/MainWindow.xaml.cs
@@ -692,6 +692,7 @@ await System.Threading.Tasks.Task.Run(() =>
try
{
BuildAnalyzer.AnalyzeBuild(build);
+ build.SearchExtensions.Add(new SecretsSearch(build));
build.SearchExtensions.Add(new NuGetSearch(build));
}
catch (Exception ex)
diff --git a/src/StructuredLogger.Utils/BinlogRedactor.cs b/src/StructuredLogger.Utils/BinlogRedactor.cs
index f6a1adbce..37d8a4226 100644
--- a/src/StructuredLogger.Utils/BinlogRedactor.cs
+++ b/src/StructuredLogger.Utils/BinlogRedactor.cs
@@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Threading;
-using Microsoft.Build.Logging;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.Build.SensitiveDataDetector;
@@ -61,7 +60,7 @@ public static void RedactSecrets(
sensitiveDataKind |= SensitiveDataKind.Username;
}
- ISensitiveDataRedactor sensitiveDataRedactor = SensitiveDataDetectorFactory.GetSecretsDetector(
+ ISensitiveDataRedactor sensitiveDataRedactor = SensitiveDataDetectorFactory.GetSecretsRedactor(
sensitiveDataKind,
redactorOptions.IdentifyReplacemenets,
redactorOptions.TokensToRedact);
diff --git a/src/StructuredLogger.Utils/SecretsSearch.cs b/src/StructuredLogger.Utils/SecretsSearch.cs
new file mode 100644
index 000000000..005bf3f64
--- /dev/null
+++ b/src/StructuredLogger.Utils/SecretsSearch.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using DotUtils.MsBuild.SensitiveDataDetector;
+using Microsoft.Build.SensitiveDataDetector;
+using StructuredLogViewer;
+
+namespace Microsoft.Build.Logging.StructuredLogger
+{
+ public class SecretsSearch : ISearchExtension
+ {
+ private readonly Build _build;
+ private readonly Dictionary _detectors;
+ private readonly Dictionary>> _secretCache = new();
+
+ public SecretsSearch(Build build)
+ {
+ _build = build ?? throw new ArgumentNullException(nameof(build));
+
+ _detectors = new()
+ {
+ { SensitiveDataKind.CommonSecrets, SensitiveDataDetectorFactory.GetSecretsDetector(SensitiveDataKind.CommonSecrets, false) },
+ { SensitiveDataKind.ExplicitSecrets, SensitiveDataDetectorFactory.GetSecretsDetector(SensitiveDataKind.ExplicitSecrets, false) },
+ { SensitiveDataKind.Username, SensitiveDataDetectorFactory.GetSecretsDetector(SensitiveDataKind.Username, false) }
+ };
+ }
+
+ public bool TryGetResults(NodeQueryMatcher matcher, IList results, int maxResults)
+ {
+ if (string.Equals(matcher.TypeKeyword, "secret", StringComparison.OrdinalIgnoreCase))
+ {
+ var activeDetectors = GetActiveDetectors(matcher.NotMatchers);
+ var foundResults = ScanForSecrets(_build.StringTable.Instances, activeDetectors, maxResults);
+
+ if (foundResults.Any())
+ {
+ foreach (var result in foundResults)
+ {
+ results.Add(result);
+ }
+ }
+ else
+ {
+ results.Add(new SearchResult(new Message { Text = "No secret(s) were detected in the tree." }));
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public List SearchSecrets(string text, IList matcher, int maxResults)
+ {
+ var activeDetectors = GetActiveDetectors(matcher);
+
+ var secrets = DetectSecrets(text, activeDetectors);
+
+ return secrets.Take(maxResults).ToList();
+ }
+
+ private IEnumerable ScanForSecrets(IEnumerable stringsPool, Dictionary detectors, int maxResults)
+ {
+ var secretsSet = new HashSet();
+ foreach (var text in stringsPool)
+ {
+ var secretResults = DetectSecrets(text, detectors);
+ foreach (SecretDescriptor secretDescriptor in secretResults)
+ {
+ secretsSet.Add(secretDescriptor.Secret);
+ }
+ }
+
+ var results = new List();
+ if (_build.SearchIndex is { } index)
+ {
+ foreach (var text in secretsSet)
+ {
+ index.MaxResults = maxResults;
+ index.MarkResultsInTree = false;
+ IEnumerable indexResults = index.FindNodes(text, CancellationToken.None);
+ if (indexResults.Any())
+ {
+ results.AddRange(indexResults);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private List DetectSecrets(string text, Dictionary detectors)
+ {
+ if (_secretCache.TryGetValue(text, out var cachedSecrets))
+ {
+ return cachedSecrets
+ .Where(kv => detectors.Any(d => d.Key == kv.Key))
+ .SelectMany(kv => kv.Value)
+ .ToList();
+ }
+
+ var results = new Dictionary>();
+
+ foreach (var detector in detectors)
+ {
+ Dictionary> detectedSecrets = detector.Value.Detect(text);
+ foreach (KeyValuePair> kv in detectedSecrets)
+ {
+ if (kv.Value.Any())
+ {
+ results[kv.Key] = kv.Value;
+ }
+ }
+ }
+
+ if (results.Any())
+ {
+ _secretCache[text] = results;
+ }
+
+ return results.Values.SelectMany(v => v).ToList();
+ }
+
+ private Dictionary GetActiveDetectors(IList notMatchers)
+ {
+ if (!notMatchers.Any())
+ {
+ return new Dictionary(_detectors);
+ }
+
+ return _detectors
+ .Where(d => !notMatchers.Any(m => Enum.TryParse(m.Query, true, out var kind) && kind == d.Key))
+ .ToDictionary(k => k.Key, v => v.Value);
+ }
+ }
+}