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