diff --git a/src/Shared.CLI/Shared.CLI.csproj b/src/Shared.CLI/Shared.CLI.csproj
index 8727b516..a898f423 100644
--- a/src/Shared.CLI/Shared.CLI.csproj
+++ b/src/Shared.CLI/Shared.CLI.csproj
@@ -133,6 +133,9 @@
Always
+
+
+
PreserveNewest
@@ -164,4 +167,38 @@
PreserveNewest
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
\ No newline at end of file
diff --git a/src/Shared.CLI/Tools/CharacteristicTool.cs b/src/Shared.CLI/Tools/CharacteristicTool.cs
index 6c381d2f..a70fcb8f 100644
--- a/src/Shared.CLI/Tools/CharacteristicTool.cs
+++ b/src/Shared.CLI/Tools/CharacteristicTool.cs
@@ -6,6 +6,7 @@
using Microsoft.ApplicationInspector.RulesEngine;
using Microsoft.CodeAnalysis.Sarif;
using Microsoft.CST.OpenSource.Shared;
+using Microsoft.CST.RecursiveExtractor;
using System;
using System.Collections.Generic;
using System.IO;
@@ -31,49 +32,405 @@ public CharacteristicTool() : this(new ProjectManagerFactory())
{
}
- public async Task AnalyzeFile(CharacteristicToolOptions options, string file)
+ public async Task AnalyzeFile(CharacteristicToolOptions options, string file, RuleSet? embeddedRules = null)
{
Logger.Trace("AnalyzeFile({0})", file);
- return await AnalyzeDirectory(options, file);
+ return await AnalyzeDirectory(options, file, embeddedRules);
}
///
/// Analyzes a directory of files.
///
/// directory to analyze.
+ /// Optional embedded rules to use instead of file-based rules.
/// List of tags identified
- public async Task AnalyzeDirectory(CharacteristicToolOptions options, string directory)
+ public async Task AnalyzeDirectory(CharacteristicToolOptions options, string directory, RuleSet? embeddedRules = null)
{
Logger.Trace("AnalyzeDirectory({0})", directory);
AnalyzeResult? analysisResult = null;
- // Call Application Inspector using the NuGet package
- AnalyzeOptions? analyzeOptions = new AnalyzeOptions()
+ try
+ {
+ // Build the RuleSet
+ RuleSet rules = new RuleSet();
+
+ if (embeddedRules != null && embeddedRules.Any())
+ {
+ // Use the embedded rules directly
+ rules.AddRange(embeddedRules);
+ Logger.Debug("Using {0} embedded rules", rules.Count());
+ }
+ else
+ {
+ // Load from custom directory or use defaults
+ if (!options.DisableDefaultRules)
+ {
+ // Load default ApplicationInspector rules
+ var aiAssembly = typeof(AnalyzeCommand).Assembly;
+ foreach (string? resourceName in aiAssembly.GetManifestResourceNames())
+ {
+ if (resourceName.EndsWith(".json"))
+ {
+ try
+ {
+ using var stream = aiAssembly.GetManifestResourceStream(resourceName);
+ using var reader = new StreamReader(stream ?? new MemoryStream());
+ rules.AddString(reader.ReadToEnd(), resourceName);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Error loading default rule {0}: {1}", resourceName, ex.Message);
+ }
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(options.CustomRuleDirectory))
+ {
+ rules.AddDirectory(options.CustomRuleDirectory);
+ }
+ }
+
+ if (!rules.Any())
+ {
+ Logger.Error("No rules were loaded, unable to continue.");
+ return null;
+ }
+
+ Logger.Debug("Loaded {0} total rules for analysis", rules.Count());
+
+ // Create RuleProcessor with empty options (like DetectCryptographyTool does)
+ RuleProcessor processor = new RuleProcessor(rules, new RuleProcessorOptions());
+
+ // Get list of files to analyze
+ string[] fileList;
+ string[] exclusions = options.FilePathExclusions?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty();
+
+ if (System.IO.Directory.Exists(directory))
+ {
+ fileList = System.IO.Directory.GetFiles(directory, "*", SearchOption.AllDirectories);
+ }
+ else if (File.Exists(directory))
+ {
+ fileList = new string[] { directory };
+ }
+ else
+ {
+ Logger.Warn("{0} is neither a directory nor a file.", directory);
+ return null;
+ }
+
+ // Filter out excluded files
+ if (exclusions.Any())
+ {
+ fileList = fileList.Where(f => !exclusions.Any(exc => f.Contains(exc, StringComparison.OrdinalIgnoreCase))).ToArray();
+ }
+
+ Logger.Debug("Analyzing {0} files in {1}", fileList.Length, directory);
+
+ // Analyze files
+ List allMatches = new List();
+ Dictionary languageCounts = new Dictionary();
+
+ foreach (string filename in fileList)
+ {
+ try
+ {
+ Logger.Trace("Processing {0}", filename);
+
+ byte[] fileContents = File.ReadAllBytes(filename);
+
+ // Create a FileEntry for the processor
+ FileEntry fileEntry = new FileEntry(filename, new MemoryStream(fileContents));
+
+ // Determine language
+ LanguageInfo languageInfo = new LanguageInfo();
+ var languages = new Languages();
+ languages.FromFileName(filename, ref languageInfo);
+
+ // DEBUG: Log file processing details
+ Logger.Debug("File: {0}, Language: {1}, Size: {2} bytes",
+ Path.GetFileName(filename),
+ languageInfo.Name ?? "unknown",
+ fileContents.Length);
+
+ // Track language statistics
+ if (!string.IsNullOrEmpty(languageInfo.Name))
+ {
+ if (languageCounts.ContainsKey(languageInfo.Name))
+ languageCounts[languageInfo.Name]++;
+ else
+ languageCounts[languageInfo.Name] = 1;
+ }
+
+ // Analyze the file
+ List fileMatches;
+
+ if (options.SingleThread)
+ {
+ fileMatches = processor.AnalyzeFile(fileEntry, languageInfo);
+ }
+ else
+ {
+ // Run with timeout for safety
+ var task = Task.Run(() => processor.AnalyzeFile(fileEntry, languageInfo));
+ if (task.Wait(TimeSpan.FromSeconds(30)))
+ {
+ fileMatches = task.Result;
+ }
+ else
+ {
+ Logger.Warn("Analysis timed out for {0}", filename);
+ continue;
+ }
+ }
+
+ // DEBUG: Log match results
+ Logger.Debug("File {0} produced {1} matches",
+ Path.GetFileName(filename),
+ fileMatches?.Count ?? 0);
+
+ if (fileMatches != null && fileMatches.Any())
+ {
+ allMatches.AddRange(fileMatches);
+ Logger.Trace("Found {0} matches in {1}", fileMatches.Count, filename);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Error analyzing file {0}: {1}", filename, ex.Message);
+ }
+ }
+
+ // Build the AnalyzeResult
+ // Note: We can't use MetaData directly because many properties are read-only
+ // and Languages dictionary might be null. We'll just pass the matches to AnalyzeResult
+ // and let ApplicationInspector handle the metadata construction.
+
+ // Create a simple AnalyzeResult without complex MetaData manipulation
+ analysisResult = new AnalyzeResult()
+ {
+ ResultCode = AnalyzeResult.ExitCode.Success
+ };
+
+ // Try to set basic metadata if the Metadata property allows it
+ try
+ {
+ MetaData metadata = new MetaData(directory, directory);
+
+ // Try to set matches via reflection
+ var matchesField = typeof(MetaData).GetField("_matches", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (matchesField != null)
+ {
+ matchesField.SetValue(metadata, allMatches);
+ }
+
+ // Try to set properties via reflection on backing fields
+ var metadataType = typeof(MetaData);
+
+ metadataType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(metadata, fileList.Length);
+
+ metadataType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(metadata, fileList.Length);
+
+ metadataType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(metadata, allMatches.Count);
+
+ metadataType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(metadata, allMatches.Select(m => m.RuleId).Distinct().Count());
+
+ // Try to initialize and set Languages dictionary
+ if (languageCounts.Any())
+ {
+ var languagesField = metadataType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (languagesField != null)
+ {
+ var existingDict = languagesField.GetValue(metadata) as System.Collections.Concurrent.ConcurrentDictionary;
+ if (existingDict == null)
+ {
+ // Create new dictionary and set it
+ existingDict = new System.Collections.Concurrent.ConcurrentDictionary();
+ languagesField.SetValue(metadata, existingDict);
+ }
+
+ // Add language counts
+ foreach (var kvp in languageCounts)
+ {
+ existingDict.TryAdd(kvp.Key, kvp.Value);
+ }
+ }
+ }
+
+ // Set the metadata on the result
+ var metadataProperty = typeof(AnalyzeResult).GetProperty("Metadata");
+ if (metadataProperty != null && metadataProperty.CanWrite)
+ {
+ metadataProperty.SetValue(analysisResult, metadata);
+ }
+ }
+ catch (Exception metadataEx)
+ {
+ Logger.Warn(metadataEx, "Unable to set full metadata, continuing with basic result: {0}", metadataEx.Message);
+ }
+
+ Logger.Debug("Operation Complete: {0} files analyzed, {1} matches found.", fileList.Length, allMatches.Count);
+ }
+ catch (Exception ex)
{
- SourcePath = new[] { directory },
- IgnoreDefaultRules = options.DisableDefaultRules,
- CustomRulesPath = options.CustomRuleDirectory,
- ConfidenceFilters = new [] { Confidence.High | Confidence.Medium | Confidence.Low },
- ScanUnknownTypes = true,
- AllowAllTagsInBuildFiles = options.AllowTagsInBuildFiles,
- SingleThread = options.SingleThread,
- FilePathExclusions = options.FilePathExclusions?.Split(',') ?? Array.Empty(),
- EnableNonBacktrackingRegex = !options.EnableBacktracking
- };
+ Logger.Warn("Error analyzing {0}: {1}", directory, ex.Message);
+ }
+
+ return analysisResult;
+ }
+
+ ///
+ /// Analyzes a directory and returns raw match records (like DetectCryptographyTool).
+ ///
+ /// Analysis options
+ /// Directory to analyze
+ /// Optional embedded rules
+ /// List of MatchRecord objects found during analysis
+ public async Task> AnalyzeDirectoryRaw(CharacteristicToolOptions options, string directory, RuleSet? embeddedRules = null)
+ {
+ Logger.Trace("AnalyzeDirectoryRaw({0})", directory);
+
+ List allMatches = new List();
try
{
- AnalyzeCommand? analyzeCommand = new AnalyzeCommand(analyzeOptions);
- analysisResult = analyzeCommand.GetResult();
- Logger.Debug("Operation Complete: {0} files analyzed.", analysisResult?.Metadata?.TotalFiles);
+ // Build the RuleSet
+ RuleSet rules = new RuleSet();
+
+ if (embeddedRules != null && embeddedRules.Any())
+ {
+ rules.AddRange(embeddedRules);
+ Logger.Debug("Using {0} embedded rules", rules.Count());
+ }
+ else
+ {
+ // Load from custom directory or use defaults
+ if (!options.DisableDefaultRules)
+ {
+ var aiAssembly = typeof(AnalyzeCommand).Assembly;
+ foreach (string? resourceName in aiAssembly.GetManifestResourceNames())
+ {
+ if (resourceName.EndsWith(".json"))
+ {
+ try
+ {
+ using var stream = aiAssembly.GetManifestResourceStream(resourceName);
+ using var reader = new StreamReader(stream ?? new MemoryStream());
+ rules.AddString(reader.ReadToEnd(), resourceName);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Error loading default rule {0}: {1}", resourceName, ex.Message);
+ }
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(options.CustomRuleDirectory))
+ {
+ rules.AddDirectory(options.CustomRuleDirectory);
+ }
+ }
+
+ if (!rules.Any())
+ {
+ Logger.Error("No rules were loaded, unable to continue.");
+ return allMatches;
+ }
+
+ Logger.Debug("Loaded {0} total rules for analysis", rules.Count());
+
+ // Create RuleProcessor
+ RuleProcessor processor = new RuleProcessor(rules, new RuleProcessorOptions());
+
+ // Get list of files to analyze
+ string[] fileList;
+ string[] exclusions = options.FilePathExclusions?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty();
+
+ if (System.IO.Directory.Exists(directory))
+ {
+ fileList = System.IO.Directory.GetFiles(directory, "*", SearchOption.AllDirectories);
+ }
+ else if (File.Exists(directory))
+ {
+ fileList = new string[] { directory };
+ }
+ else
+ {
+ Logger.Warn("{0} is neither a directory nor a file.", directory);
+ return allMatches;
+ }
+
+ // Filter out excluded files
+ if (exclusions.Any())
+ {
+ fileList = fileList.Where(f => !exclusions.Any(exc => f.Contains(exc, StringComparison.OrdinalIgnoreCase))).ToArray();
+ }
+
+ Logger.Debug("Analyzing {0} files in {1}", fileList.Length, directory);
+
+ // Analyze files
+ foreach (string filename in fileList)
+ {
+ try
+ {
+ Logger.Trace("Processing {0}", filename);
+
+ byte[] fileContents = File.ReadAllBytes(filename);
+ FileEntry fileEntry = new FileEntry(filename, new MemoryStream(fileContents));
+
+ // Determine language
+ LanguageInfo languageInfo = new LanguageInfo();
+ var languages = new Languages();
+ languages.FromFileName(filename, ref languageInfo);
+
+ // Analyze the file
+ List fileMatches;
+
+ if (options.SingleThread)
+ {
+ fileMatches = processor.AnalyzeFile(fileEntry, languageInfo);
+ }
+ else
+ {
+ var task = Task.Run(() => processor.AnalyzeFile(fileEntry, languageInfo));
+ if (task.Wait(TimeSpan.FromSeconds(30)))
+ {
+ fileMatches = task.Result;
+ }
+ else
+ {
+ Logger.Warn("Analysis timed out for {0}", filename);
+ continue;
+ }
+ }
+
+ if (fileMatches != null && fileMatches.Any())
+ {
+ allMatches.AddRange(fileMatches);
+ Logger.Debug("Found {0} matches in {1}", fileMatches.Count, filename);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Error analyzing file {0}: {1}", filename, ex.Message);
+ }
+ }
+
+ Logger.Debug("Operation Complete: {0} files analyzed, {1} matches found.", fileList.Length, allMatches.Count);
}
catch (Exception ex)
{
Logger.Warn("Error analyzing {0}: {1}", directory, ex.Message);
}
- return analysisResult;
+ return allMatches;
}
///
@@ -83,7 +440,8 @@ public CharacteristicTool() : this(new ProjectManagerFactory())
/// List of tags identified
public async Task> AnalyzePackage(CharacteristicToolOptions options, PackageURL purl,
string? targetDirectoryName,
- bool doCaching = false)
+ bool doCaching = false,
+ RuleSet? embeddedRules = null)
{
Logger.Trace("AnalyzePackage({0})", purl.ToString());
@@ -98,7 +456,7 @@ public CharacteristicTool() : this(new ProjectManagerFactory())
{
foreach (string? directoryName in directoryNames)
{
- AnalyzeResult? singleResult = await AnalyzeDirectory(options, directoryName);
+ AnalyzeResult? singleResult = await AnalyzeDirectory(options, directoryName, embeddedRules);
analysisResults[directoryName] = singleResult;
}
}
@@ -195,7 +553,7 @@ private static List GetTextResults(PackageURL purl, Dictionary>? dict = new Dictionary>();
- foreach ((string[]? tags, Confidence confidence) in metadata?.Matches?.Where(x => x is not null).Select(x => (x.Tags, x.Confidence)) ?? Array.Empty<(string[], Confidence)>())
+ foreach ((string[]? tags, Confidence confidence) in metadata?.Matches?.Where(x => x is not null).Select(x => (x.Tags ?? Array.Empty(), x.Confidence)) ?? Array.Empty<(string[], Confidence)>())
{
foreach (string? tag in tags)
{
@@ -260,7 +618,7 @@ public override async Task RunAsync(CharacteristicToolOptions options
return ErrorCode.Ok;
}
- public async Task>> LegacyRunAsync(CharacteristicToolOptions options)
+ public async Task>> LegacyRunAsync(CharacteristicToolOptions options, RuleSet? embeddedRules = null)
{
// select output destination and format
SelectOutput(options.OutputFile);
@@ -280,14 +638,15 @@ public override async Task RunAsync(CharacteristicToolOptions options
string downloadDirectory = options.DownloadDirectory == "." ? System.IO.Directory.GetCurrentDirectory() : options.DownloadDirectory;
Dictionary? analysisResult = await AnalyzePackage(options, purl,
downloadDirectory,
- options.UseCache == true);
+ options.UseCache == true,
+ embeddedRules);
AppendOutput(outputBuilder, purl, analysisResult, options);
finalResults.Add(analysisResult);
}
else if (System.IO.Directory.Exists(target))
{
- AnalyzeResult? analysisResult = await AnalyzeDirectory(options, target);
+ AnalyzeResult? analysisResult = await AnalyzeDirectory(options, target, embeddedRules);
if (analysisResult != null)
{
Dictionary? analysisResults = new Dictionary()
@@ -302,7 +661,7 @@ public override async Task RunAsync(CharacteristicToolOptions options
}
else if (File.Exists(target))
{
- AnalyzeResult? analysisResult = await AnalyzeFile(options, target);
+ AnalyzeResult? analysisResult = await AnalyzeFile(options, target, embeddedRules);
if (analysisResult != null)
{
Dictionary? analysisResults = new Dictionary()
diff --git a/src/Shared.CLI/Tools/DetectBackdoorTool.cs b/src/Shared.CLI/Tools/DetectBackdoorTool.cs
index 9b54b403..a7520688 100644
--- a/src/Shared.CLI/Tools/DetectBackdoorTool.cs
+++ b/src/Shared.CLI/Tools/DetectBackdoorTool.cs
@@ -16,143 +16,232 @@ namespace Microsoft.CST.OpenSource
using OssGadget.Options;
using OssGadget.Tools;
using PackageManagers;
+ using PackageUrl;
public class DetectBackdoorTool : BaseTool
{
public DetectBackdoorTool(ProjectManagerFactory projectManagerFactory) : base(projectManagerFactory)
{
- RULE_DIRECTORY = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "BackdoorRules");
}
public DetectBackdoorTool() : this(new ProjectManagerFactory())
{
}
- ///
- /// Location of the backdoor detection rules.
- ///
- private string RULE_DIRECTORY { get; set; }
-
- ///
- /// Main entrypoint for the download program.
- ///
- /// parameters passed in from the user
public override async Task RunAsync(DetectBackdoorToolOptions options)
{
- List>? detectionResults = await LegacyRunAsync(options);
+ if (options?.Targets is not IList targetList || targetList.Count == 0)
+ {
+ Logger.Warn("No target provided; nothing to analyze.");
+ return ErrorCode.NoTargets;
+ }
+
+ // Load embedded backdoor detection rules
+ RuleSet? embeddedRules = LoadEmbeddedRules();
+
+ if (embeddedRules == null || !embeddedRules.Any())
+ {
+ Logger.Error("Failed to load embedded backdoor detection rules. Cannot proceed.");
+ return ErrorCode.ProcessingException;
+ }
- foreach (Dictionary? result in detectionResults)
+ CharacteristicTool characteristicTool = new CharacteristicTool(ProjectManagerFactory);
+
+ foreach (string target in targetList)
{
- foreach (KeyValuePair entry in result)
+ try
{
- if (entry.Value == null || entry.Value.Metadata == null || entry.Value.Metadata.Matches == null)
+ List results = new List();
+
+ if (target.StartsWith("pkg:", StringComparison.InvariantCulture))
+ {
+ PackageURL purl = new PackageURL(target);
+ results = await AnalyzePackage(characteristicTool, options, purl, embeddedRules);
+ }
+ else if (System.IO.Directory.Exists(target))
+ {
+ results = await AnalyzeDirectory(characteristicTool, options, target, embeddedRules);
+ }
+ else if (File.Exists(target))
{
+ results = await AnalyzeDirectory(characteristicTool, options, target, embeddedRules);
+ }
+ else
+ {
+ Logger.Warn("{0} was neither a Package URL, directory, nor a file.", target);
continue;
}
- if (options.Format == "text")
+ // Display results
+ if (results == null || !results.Any())
{
- IOrderedEnumerable? matchEntries = entry.Value.Metadata.Matches.OrderByDescending(x => x.Confidence);
- int matchEntriesCount = matchEntries.Count();
- int matchIndex = 1;
-
- foreach (MatchRecord? match in matchEntries)
- {
- WriteMatch(match, matchIndex, matchEntriesCount);
- matchIndex++;
- }
- Console.WriteLine($"{entry.Value.Metadata.TotalMatchesCount} matches found.");
+ Console.WriteLine("0 matches found.");
}
+ else
+ {
+ DisplayResults(target, results, options);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Error processing {0}: {1}", target, ex.Message);
+ }
+ }
- void WriteMatch(MatchRecord match, int index, int matchCount)
+ return ErrorCode.Ok;
+ }
+
+ private async Task> AnalyzePackage(CharacteristicTool tool, DetectBackdoorToolOptions options, PackageURL purl, RuleSet embeddedRules)
+ {
+ Logger.Trace("AnalyzePackage({0})", purl.ToString());
+
+ List allMatches = new List();
+
+ PackageDownloader packageDownloader = new PackageDownloader(purl, ProjectManagerFactory, options.DownloadDirectory, options.UseCache);
+ List directoryNames = await packageDownloader.DownloadPackageLocalCopy(purl, false, true);
+
+ if (directoryNames.Count > 0)
+ {
+ foreach (string directoryName in directoryNames)
+ {
+ Logger.Trace("Analyzing directory {0}", directoryName);
+ List singleResult = await AnalyzeDirectory(tool, options, directoryName, embeddedRules);
+ if (singleResult != null)
{
- string? filename = match.FileName;
- if (filename == null)
- {
- return;
- }
- int? sourcePathLength = entry.Value.Metadata.SourcePath?.Length;
- if (sourcePathLength.HasValue)
- {
- if (entry.Value.Metadata.SourcePath != null && filename.StartsWith(entry.Value.Metadata.SourcePath))
- {
- filename = filename[sourcePathLength.Value..];
- }
- }
- Console.WriteLine(Red($"--[ ") + Blue("Match #") + Yellow(index.ToString()) + Blue(" of ") + Yellow(matchCount.ToString()) + Red(" ]--"));
- Console.WriteLine(" Rule Id: " + Blue(match.Rule.Id));
- Console.WriteLine(" Tag: " + Blue(match.Tags?.First()));
- Console.WriteLine(" Severity: " + Cyan(match.Severity.ToString()) + ", Confidence: " + Cyan(match.Confidence.ToString()));
- Console.WriteLine(" Filename: " + Yellow(filename));
- Console.WriteLine(" Pattern: " + Green(match.MatchingPattern.Pattern));
+ allMatches.AddRange(singleResult);
+ }
+ }
+ }
+ else
+ {
+ Logger.Warn("Error downloading {0}.", purl.ToString());
+ }
+
+ packageDownloader.ClearPackageLocalCopyIfNoCaching();
+ return allMatches;
+ }
- string[] FullTextLines = match.FullTextContainer.FullContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
- string[] ExcerptsLines = match.Excerpt.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ private async Task> AnalyzeDirectory(CharacteristicTool tool, DetectBackdoorToolOptions options, string directory, RuleSet embeddedRules)
+ {
+ CharacteristicToolOptions cOptions = new CharacteristicToolOptions
+ {
+ Targets = new List { directory },
+ DisableDefaultRules = true,
+ CustomRuleDirectory = null,
+ DownloadDirectory = options.DownloadDirectory,
+ UseCache = options.UseCache,
+ AllowTagsInBuildFiles = true,
+ FilePathExclusions = ".md,LICENSE,.txt",
+ AllowDupTags = true,
+ EnableBacktracking = options.EnableBacktracking,
+ SingleThread = options.SingleThread
+ };
+
+ return await tool.AnalyzeDirectoryRaw(cOptions, directory, embeddedRules);
+ }
- int ExcerptStart = -1;
+ private void DisplayResults(string target, List results, DetectBackdoorToolOptions options)
+ {
+ Console.WriteLine($"\n{target}");
+ Console.WriteLine($"{results.Count} matches found.\n");
- for (int i = 0; i < ExcerptsLines.Length; i++)
- {
- if (string.Equals(ExcerptsLines[i].TrimStart().TrimEnd(), FullTextLines[match.StartLocationLine - 1].TrimStart().TrimEnd()))
- {
- ExcerptStart = match.StartLocationLine - i;
- break;
- }
- }
+ if (options.Format == "text" && results.Any())
+ {
+ var orderedResults = results.OrderByDescending(x => x.Confidence);
+ int matchIndex = 1;
- Array.Resize(ref ExcerptsLines, ExcerptsLines.Length - 1);
- foreach (string? line in ExcerptsLines)
- {
- string? s = line;
- if (s.Length > 100)
- {
- s = s.Substring(0, 100);
- }
-
- if (ExcerptStart != -1)
- {
- Console.WriteLine(Bright.Black($"{ExcerptStart++} | ") + Magenta(s));
- }
- else
- {
- Console.WriteLine(Bright.Black(" | ") + Magenta(s));
- }
- }
- Console.WriteLine();
- }
+ foreach (MatchRecord match in orderedResults)
+ {
+ WriteMatch(match, matchIndex, results.Count, target);
+ matchIndex++;
}
}
+ }
- return ErrorCode.Ok;
+ private void WriteMatch(MatchRecord match, int index, int matchCount, string basePath)
+ {
+ string? filename = match.FileName;
+ if (filename == null)
+ {
+ return;
+ }
+
+ // Trim base path if present
+ if (filename.StartsWith(basePath))
+ {
+ filename = filename.Substring(basePath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ Console.WriteLine(Red($"--[ ") + Blue("Match #") + Yellow(index.ToString()) + Blue(" of ") + Yellow(matchCount.ToString()) + Red(" ]--"));
+ Console.WriteLine(" Rule Id: " + Blue(match.Rule.Id));
+ Console.WriteLine(" Tag: " + Blue(match.Tags?.FirstOrDefault() ?? "N/A"));
+ Console.WriteLine(" Severity: " + Cyan(match.Severity.ToString()) + ", Confidence: " + Cyan(match.Confidence.ToString()));
+ Console.WriteLine(" Filename: " + Yellow(filename));
+ Console.WriteLine(" Pattern: " + Green(match.MatchingPattern.Pattern));
+
+ // Display excerpt
+ string[] excerptLines = match.Excerpt.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ int lineNumber = match.StartLocationLine;
+
+ foreach (string line in excerptLines.Take(excerptLines.Length - 1))
+ {
+ string displayLine = line.Length > 100 ? line.Substring(0, 100) + "..." : line;
+ Console.WriteLine(Bright.Black($"{lineNumber++} | ") + Magenta(displayLine));
+ }
+ Console.WriteLine();
}
-
- private async Task>> LegacyRunAsync(DetectBackdoorToolOptions options)
+
+ private RuleSet? LoadEmbeddedRules()
{
- if (options != null && options.Targets is IList targetList && targetList.Count > 0)
+ try
{
- CharacteristicTool? characteristicTool = new CharacteristicTool(ProjectManagerFactory);
- CharacteristicToolOptions cOptions = new CharacteristicToolOptions
+ var assembly = typeof(DetectBackdoorTool).Assembly;
+ var resourceNames = assembly.GetManifestResourceNames()
+ .Where(name => name.Contains("BackdoorRules") && name.EndsWith(".json"))
+ .ToList();
+
+ if (!resourceNames.Any())
+ {
+ Logger.Warn("No embedded BackdoorRules found in assembly resources");
+ return null;
+ }
+
+ RuleSet rules = new RuleSet();
+
+ foreach (var resourceName in resourceNames)
+ {
+ try
+ {
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ Logger.Warn("Could not load resource: {0}", resourceName);
+ continue;
+ }
+
+ using var reader = new StreamReader(stream);
+ var jsonContent = reader.ReadToEnd();
+ rules.AddString(jsonContent, resourceName);
+ Logger.Debug("Loaded rules from {0}", resourceName);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, "Failed to load embedded rule from {0}: {1}", resourceName, ex.Message);
+ }
+ }
+
+ if (rules.Any())
{
- Targets = options.Targets,
- DisableDefaultRules = true,
- CustomRuleDirectory = RULE_DIRECTORY,
- DownloadDirectory = options.DownloadDirectory,
- UseCache = options.UseCache,
- Format = options.Format == "text" ? "none" : options.Format,
- OutputFile = options.OutputFile,
- AllowTagsInBuildFiles = true,
- FilePathExclusions = ".md,LICENSE,.txt",
- AllowDupTags = true,
- SarifLevel = CodeAnalysis.Sarif.FailureLevel.Warning,
- EnableBacktracking = options.EnableBacktracking,
- SingleThread = options.SingleThread
- };
-
- return await characteristicTool.LegacyRunAsync(cOptions);
+ Logger.Info("Successfully loaded {0} total backdoor detection rules from embedded resources", rules.Count());
+ return rules;
+ }
+
+ return null;
}
- else
+ catch (Exception ex)
{
- return new List>();
+ Logger.Error(ex, "Error loading embedded rules: {0}", ex.Message);
+ return null;
}
}
}