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); + } + } +}