Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secrets search and bump DotUtils.MsBuild.BinlogRedactor.SensitiveDataDetector version #829

2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PackageVersion Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageVersion Include="DiffPlex" Version="1.7.2" />
<PackageVersion Include="DotUtils.MsBuild.BinlogRedactor.SensitiveDataDetector" Version="0.0.8" />
<PackageVersion Include="DotUtils.MsBuild.BinlogRedactor.SensitiveDataDetector" Version="0.0.9" />
<PackageVersion Include="DotUtils.StreamUtils.Sources" Version="0.0.8" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="GuiLabs.Language.Xml" Version="1.2.93" />
Expand Down
29 changes: 24 additions & 5 deletions src/StructuredLogViewer/Controls/BuildControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public partial class BuildControl : UserControl

private PropertiesAndItemsSearch propertiesAndItemsSearch;

private SecretsSearch secretsSearch;

public BuildControl(Build build, string logFilePath)
{
InitializeComponent();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -643,6 +646,7 @@ private void PopulateProjectGraph()
"$task $time",
"$message CompilerServer failed",
"will be compiled because",
"$secret"
};

private static string[] nodeKinds = new[]
Expand All @@ -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")
Expand Down Expand Up @@ -1017,18 +1022,32 @@ private object FindInFiles(string searchText, int maxResults, CancellationToken
{
var results = new List<(string, IEnumerable<(int, string)>)>();

NodeQueryMatcher notQueryMatcher = new NodeQueryMatcher(searchText);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean nodeQueryMatcher?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it in main

bool isSecretsSearch = !string.IsNullOrEmpty(searchText) && searchText.StartsWith("$secret");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also check for nodeQueryMatcher.TypeKeyword == "secret"


foreach (var file in archiveFile.Files)
{
if (cancellationToken.IsCancellationRequested)
{
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)))));
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/StructuredLogViewer/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/StructuredLogger.Utils/BinlogRedactor.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -61,7 +60,7 @@ public static void RedactSecrets(
sensitiveDataKind |= SensitiveDataKind.Username;
}

ISensitiveDataRedactor sensitiveDataRedactor = SensitiveDataDetectorFactory.GetSecretsDetector(
ISensitiveDataRedactor sensitiveDataRedactor = SensitiveDataDetectorFactory.GetSecretsRedactor(
sensitiveDataKind,
redactorOptions.IdentifyReplacemenets,
redactorOptions.TokensToRedact);
Expand Down
137 changes: 137 additions & 0 deletions src/StructuredLogger.Utils/SecretsSearch.cs
Original file line number Diff line number Diff line change
@@ -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<SensitiveDataKind, ISensitiveDataDetector> _detectors;
private readonly Dictionary<string, Dictionary<SensitiveDataKind, List<SecretDescriptor>>> _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<SearchResult> 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<SecretDescriptor> SearchSecrets(string text, IList<NodeQueryMatcher> matcher, int maxResults)
{
var activeDetectors = GetActiveDetectors(matcher);

var secrets = DetectSecrets(text, activeDetectors);

return secrets.Take(maxResults).ToList();
}

private IEnumerable<SearchResult> ScanForSecrets(IEnumerable<string> stringsPool, Dictionary<SensitiveDataKind, ISensitiveDataDetector> detectors, int maxResults)
{
var secretsSet = new HashSet<string>();
foreach (var text in stringsPool)
{
var secretResults = DetectSecrets(text, detectors);
foreach (SecretDescriptor secretDescriptor in secretResults)
{
secretsSet.Add(secretDescriptor.Secret);
}
}

var results = new List<SearchResult>();
if (_build.SearchIndex is { } index)
{
foreach (var text in secretsSet)
{
index.MaxResults = maxResults;
index.MarkResultsInTree = false;
IEnumerable<SearchResult> indexResults = index.FindNodes(text, CancellationToken.None);
if (indexResults.Any())
{
results.AddRange(indexResults);
}
}
}

return results;
}

private List<SecretDescriptor> DetectSecrets(string text, Dictionary<SensitiveDataKind, ISensitiveDataDetector> 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<SensitiveDataKind, List<SecretDescriptor>>();

foreach (var detector in detectors)
{
Dictionary<SensitiveDataKind, List<SecretDescriptor>> detectedSecrets = detector.Value.Detect(text);
foreach (KeyValuePair<SensitiveDataKind, List<SecretDescriptor>> kv in detectedSecrets)
{
if (kv.Value.Any())
{
results[kv.Key] = kv.Value;
}
}
}

if (results.Any())
{
_secretCache[text] = results;
YuliiaKovalova marked this conversation as resolved.
Show resolved Hide resolved
}

return results.Values.SelectMany(v => v).ToList();
}

private Dictionary<SensitiveDataKind, ISensitiveDataDetector> GetActiveDetectors(IList<NodeQueryMatcher> notMatchers)
{
if (!notMatchers.Any())
{
return new Dictionary<SensitiveDataKind, ISensitiveDataDetector>(_detectors);
}

return _detectors
.Where(d => !notMatchers.Any(m => Enum.TryParse<SensitiveDataKind>(m.Query, true, out var kind) && kind == d.Key))
.ToDictionary(k => k.Key, v => v.Value);
}
}
}