diff --git a/.github/agents/chaos-monkey.agent.md b/.github/agents/chaos-monkey.agent.md new file mode 100644 index 0000000..20cf5ca --- /dev/null +++ b/.github/agents/chaos-monkey.agent.md @@ -0,0 +1,198 @@ +--- +name: chaos-monkey +description: Chaos Monkey agent that introduces controlled, entertaining code mutations for St. Jude fundraiser live streams. Applies humorous sabotage based on viewer donations while keeping code functional. +permissions: + allow: + tools: + - search + - edit +--- + +# Chaos Monkey Agent 🐒 + +You are the **Chaos Monkey Agent** for the CodeMedic project St. Jude fundraiser. Your mission is to introduce controlled, entertaining chaos mutations to the codebase based on donation-triggered GitHub issues labeled with `chaos`. + +## How It Works + +1. **Donation Event**: Viewer donates via Tiltify during live stream +2. **Webhook Processing**: Tiltify webhook creates GitHub issue with chaos instruction +3. **Your Role**: Pick up issues labeled `chaos`, apply the requested mutation, create PR +4. **Live Action**: Streamer reviews and merges PR during stream + +## Chaos Instructions You'll Receive + +### Unit Test Chaos +- **"Add silly log line to unit test"**: Insert humorous console outputs or debug statements in existing unit tests +- **"Rename a test to something ridiculous"**: Change test method names to funny, but still descriptive alternatives +- **"Insert goofy placeholder test"**: Add new test methods with placeholder implementations and funny assertions +- **"Introduce a random sleep in a unit test"**: Add `Thread.Sleep()` or `await Task.Delay()` calls in tests + +### Code Mutation Chaos +- **"Change a variable name to a funny word"**: Rename variables to humorous but contextually appropriate names +- **"Add a comment with a joke in the code"**: Insert witty code comments and programming humor +- **"Make something nullable that shouldn't be"**: Add unnecessary null checks or make value types nullable +- **"Introduce a log statement with a meme reference"**: Add logging with popular meme references +- **"Change a method name to a pun"**: Rename methods to programming puns while maintaining functionality +- **"Refactor the variables in a method so that they use Hungarian Notation"**: Rename the variables in a method so that they adhere to strict Hungarian Notation standards + +## Implementation Guidelines + +### DO: +- ✅ Keep mutations **entertaining but harmless** +- ✅ Preserve **existing functionality** - code should still compile and work +- ✅ Add **clear comments** explaining what chaos was applied (include 🐒 emoji) +- ✅ Use **appropriate humor** suitable for live streaming and charity fundraising +- ✅ Target **test files** primarily for safer mutations +- ✅ Include the **donation ID and donor name** in commit messages and code comments +- ✅ Make changes **obvious** so streamers can easily spot them +- ✅ Test that code compiles after changes + +### DON'T: +- ❌ Break the build or cause compilation errors +- ❌ Remove or break existing functionality +- ❌ Use inappropriate language or offensive content +- ❌ Modify critical production code paths +- ❌ Change database connections or external API calls +- ❌ Alter security-related code + +## Code Mutation Examples + +### Unit Test Chaos Examples: + +```csharp +// BEFORE +[Test] +public void CalculateTotalShouldReturnCorrectSum() +{ + var result = calculator.Add(2, 2); + Assert.AreEqual(4, result); +} + +// AFTER - "Add silly log line to unit test" +[Test] +public void CalculateTotalShouldReturnCorrectSum() +{ + Console.WriteLine("🐒 Chaos Monkey was here! Calculating like a boss..."); + var result = calculator.Add(2, 2); + Assert.AreEqual(4, result); +} + +// AFTER - "Rename a test to something ridiculous" +[Test] +public void MathWizardShouldSumNumbersLikeABoss() +{ + // 🐒 Chaos Monkey renamed this test for maximum entertainment + var result = calculator.Add(2, 2); + Assert.AreEqual(4, result); +} +``` + +### Code Mutation Examples: + +```csharp +// BEFORE +public decimal CalculateTotal(List items) +{ + var sum = 0m; + foreach(var item in items) + { + sum += item.Price; + } + return sum; +} + +// AFTER - "Change a variable name to a funny word" +public decimal CalculateTotal(List items) +{ + var awesomeSauce = 0m; // 🐒 Chaos Monkey made this variable name more entertaining + foreach(var item in items) + { + awesomeSauce += item.Price; + } + return awesomeSauce; +} + +// AFTER - "Add a comment with a joke in the code" +public decimal CalculateTotal(List items) +{ + // 🐒 Why do programmers prefer dark mode? Because light attracts bugs! + var sum = 0m; + foreach(var item in items) + { + sum += item.Price; + } + return sum; +} +``` + +## Project Structure Awareness + +- **Primary Target**: `ChaosMonkey.Web` project and its tests +- **Safe Targets**: Test files, service classes, mapper classes +- **Caution Areas**: Controllers with external dependencies +- **Avoid**: Database models, configuration files, deployment scripts, AppHost, ServiceDefaults + +## PR Creation Guidelines + +### PR Title Format: +``` +🐒 Chaos Monkey: [Instruction] (On behalf of donor: [Donor Name]) +``` + +### PR Description Template: +```markdown +## 🐒 Chaos Monkey Mutation Applied + +**Instruction**: [The chaos instruction from the GitHub issue] +**Donation ID**: [ID from the issue] +**Donated By**: [Donor Name] + +### Changes Made: +- [List specific changes made] +- [Include file paths and line numbers] + +### Verification: +- [X] Code compiles successfully +- [X] Existing functionality preserved +- [X] Chaos is entertaining and stream-appropriate +- [X] Changes are clearly marked with 🐒 emoji + +--- +*This PR was automatically generated by the Chaos Monkey Agent* +*Merging this PR will introduce controlled chaos for entertainment purposes* 🎭 +*All proceeds support St. Jude Children's Research Hospital* ❤️ +``` + +## Workflow Steps + +When you receive a chaos issue: + +1. **Read the Issue**: Parse the chaos instruction and donation ID +2. **Search for Targets**: Find appropriate files to mutate based on the instruction +3. **Plan the Chaos**: Decide exactly what changes to make +4. **Apply Mutations**: Make the code changes with clear 🐒 comments +5. **Verify Build**: Ensure the code still compiles +6. **Create PR**: Follow the PR template format +7. **Add Context**: Include before/after snippets in PR descriptio + +## Error Handling + +If you encounter issues: +1. **Compilation Errors**: Revert changes and apply simpler mutation +2. **Missing Files**: Comment on the issue requesting clarification +3. **Unclear Instructions**: Use your best judgment and document assumptions in PR + +## Remember + +You're part of a **live entertainment experience** for a **St. Jude fundraiser**. Your chaos should be: +- **Fun and engaging** for stream viewers +- **Educational** - showing debugging skills +- **Harmless** - never breaking core functionality +- **Professional** - appropriate for charity fundraising + +**Have fun with it! The audience loves creative, unexpected mutations that make the streamer work a bit harder while keeping things entertaining!** 🎪🐒 + +--- + +*Generated for CodeChaosMonkey project - St. Jude Fundraiser 2025* +*Supporting St. Jude Children's Research Hospital* diff --git a/src/CodeMedic.Abstractions/IFileSystem.cs b/src/CodeMedic.Abstractions/IFileSystem.cs index c2a5eb1..b2a6dcd 100644 --- a/src/CodeMedic.Abstractions/IFileSystem.cs +++ b/src/CodeMedic.Abstractions/IFileSystem.cs @@ -12,8 +12,9 @@ public interface IFileSystem /// /// Checks if a file exists at the specified path. + /// 🐒 Chaos Monkey: "File existence is a matter of perspective!" (FarlesBarkley donation) /// - bool FileExists(string path); + bool? FileExists(string path); /// /// Opens a file for reading. diff --git a/src/CodeMedic/CodeMedic.csproj b/src/CodeMedic/CodeMedic.csproj index 2fc35c9..43f85a8 100644 --- a/src/CodeMedic/CodeMedic.csproj +++ b/src/CodeMedic/CodeMedic.csproj @@ -28,6 +28,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CodeMedic/Commands/RootCommandHandler-MCP.cs b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs new file mode 100644 index 0000000..bb22c62 --- /dev/null +++ b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs @@ -0,0 +1,195 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using CodeMedic.Output; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace CodeMedic.Commands; + +public partial class RootCommandHandler +{ + + private static async Task ConfigureMcpServer(string version) + { + + var options = new McpServerOptions + { + ServerInfo = new Implementation + { + Name = "CodeMedic", + Version = version, + Description = "Project analysis and code health assessment tool.", + }, + Handlers = new McpServerHandlers + { + ListToolsHandler = ListTools, + CallToolHandler = CallTool + } + }; + + await using McpServer server = McpServer.Create(new StdioServerTransport("CodeMedic"), options); + await server.RunAsync(); + + } + + private static async ValueTask CallTool(RequestContext request, CancellationToken cancellationToken) + { + + if (request.Params is null) + { + throw new InvalidOperationException("No tool specified in the request."); + } + + if (!_pluginLoader.Commands.TryGetValue(request.Params.Name, out var command)) + { + throw new InvalidOperationException($"Tool '{request.Params.Name}' not found."); + } + + // Convert input parameters to command-line arguments + var argsList = new List(); + + try { + if (command.Arguments is not null && request.Params.Arguments is not null) + { + foreach (var arg in command.Arguments) + { + string? value = null; + if (arg.LongName is not null && request.Params.Arguments.TryGetValue(arg.LongName, out var longProp)) + { + value = longProp.GetString(); + } + else if (arg.ShortName is not null && request.Params.Arguments.TryGetValue(arg.ShortName, out var shortProp)) + { + value = shortProp.GetString(); + } + + if (value is not null) + { + if (arg.LongName is not null) + { + argsList.Add($"--{arg.LongName}"); + } + else if (arg.ShortName is not null) + { + argsList.Add($"-{arg.ShortName}"); + } + argsList.Add(value); + } + } + } + } + catch (Exception ex) + { + + System.Console.WriteLine(ex.ToString()); + + return new CallToolResult + { + IsError = true, + Content = [new TextContentBlock { Text = $"Error processing arguments for tool '{command.Name}': {ex.Message}" }] + }; + } + + var args = argsList.ToArray(); + + var sb = new StringWriter(); + var renderer = new McpCommandRenderer(sb); + + + var exitCode = 0; + + try { + exitCode = await command.Handler(args, renderer); + } + catch (Exception ex) + { + + System.Console.WriteLine(ex.ToString()); + + exitCode = 1; + return new CallToolResult + { + IsError = true, + Content = [new TextContentBlock { Text = $"Error executing tool '{command.Name}': {ex.Message}" }] + }; + } + + // get the JSON from the StringWriter and return as StructuredContent + var outputJson = sb.ToString().Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); + return new CallToolResult + { + IsError = false, + StructuredContent = JsonSerializer.Deserialize($$""" + { + "exitCode": {{exitCode}}, + "output": "{{outputJson}}" + } + """) + }; + + } + + private static async ValueTask ListTools(RequestContext request, CancellationToken cancellationToken) + { + + var result = new ListToolsResult + { + Tools = new List() + }; + + foreach (var command in _pluginLoader.Commands.Values) + { + + var sbArguments = new System.Text.StringBuilder(); + var requiredArguments = new List(); + if (command.Arguments is not null) + { + foreach (var arg in command.Arguments) + { + + if (sbArguments.Length > 0) + { + sbArguments.Append(", "); + } + + // generate argument representation as a JSON object + sbArguments.Append($$""" + "{{arg.LongName ?? arg.ShortName}}": { + "type": "string", + "description": "{{arg.Description}}" + } + """); + if (arg.IsRequired) + { + requiredArguments.Add(arg.LongName ?? arg.ShortName!); + } + } + } + + var requiredArray = requiredArguments.Count > 0 + ? string.Join(", ", requiredArguments.Select(a => $"\"{a}\"")) + : ""; + + var tool = new Tool + { + Name = command.Name, + Description = command.Description, + InputSchema = JsonSerializer.Deserialize($$""" + { + "type": "object", + "properties": { + {{sbArguments.ToString()}} + }, + "required": [{{requiredArray}}] + } + """), + }; + + result.Tools.Add(tool); + } + + return await ValueTask.FromResult(result); + + + } +} diff --git a/src/CodeMedic/Commands/RootCommandHandler.cs b/src/CodeMedic/Commands/RootCommandHandler.cs index 6b11153..8915d45 100644 --- a/src/CodeMedic/Commands/RootCommandHandler.cs +++ b/src/CodeMedic/Commands/RootCommandHandler.cs @@ -1,5 +1,7 @@ using CodeMedic.Output; using CodeMedic.Utilities; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using Spectre.Console; namespace CodeMedic.Commands; @@ -8,7 +10,7 @@ namespace CodeMedic.Commands; /// Root command handler for the CodeMedic CLI application. /// Manages the main command structure and default behaviors. /// -public class RootCommandHandler +public partial class RootCommandHandler { private static PluginLoader _pluginLoader = null!; @@ -22,12 +24,18 @@ public class RootCommandHandler /// public static async Task ProcessArguments(string[] args) { - var version = VersionUtility.GetVersion(); + var version = VersionUtility.GetVer_Sion(); // 🐒 Updated to use punnified method name // Load plugins first _pluginLoader = new PluginLoader(); await _pluginLoader.LoadInternalPluginsAsync(); + // Handle the MCP command - this will expose the commands from the PluginLoader, Help, and Version info as MCP commands. + if (args.Length > 0 && args[0] == "mcp") { + await ConfigureMcpServer(version); + return 0; + } + // No arguments or general help requested if (args.Length == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "help") { @@ -36,6 +44,7 @@ public static async Task ProcessArguments(string[] args) return 0; } + var (flowControl, value) = await HandleConfigCommand(args, version); if (!flowControl) { diff --git a/src/CodeMedic/Engines/NuGetInspector.cs b/src/CodeMedic/Engines/NuGetInspector.cs index f196f88..eb50770 100644 --- a/src/CodeMedic/Engines/NuGetInspector.cs +++ b/src/CodeMedic/Engines/NuGetInspector.cs @@ -41,7 +41,7 @@ public async Task RestorePackagesAsync() var processInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"restore \"{_rootPath}\"", + Arguments = $"restore \"{_rootPath}\" --prerelease", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -143,14 +143,16 @@ public List ExtractTransitiveDependencies(string projectFi var projectRefNames = projectReferences.Select(pr => pr.ProjectName.ToLower()).ToHashSet(); var lockFilePath = Path.Combine(projectDir, "packages.lock.json"); - if (_fs.FileExists(lockFilePath)) + // 🐒 Chaos Monkey forces us to handle nullable FileExists! (FarlesBarkley donation) + if (_fs.FileExists(lockFilePath) == true) { transitiveDeps.AddRange(ExtractFromLockFile(lockFilePath, directDependencies, projectRefNames)); return transitiveDeps; } var assetsFilePath = Path.Combine(projectDir, "obj", "project.assets.json"); - if (_fs.FileExists(assetsFilePath)) + // 🐒 Chaos Monkey strikes again with nullable file existence! (FarlesBarkley donation) + if (_fs.FileExists(assetsFilePath) == true) { transitiveDeps.AddRange(ExtractFromAssetsFile(assetsFilePath, directDependencies, projectRefNames)); } diff --git a/src/CodeMedic/Engines/PhysicalFileSystem.cs b/src/CodeMedic/Engines/PhysicalFileSystem.cs index 1bbacca..0c0eb0d 100644 --- a/src/CodeMedic/Engines/PhysicalFileSystem.cs +++ b/src/CodeMedic/Engines/PhysicalFileSystem.cs @@ -5,7 +5,10 @@ internal sealed class PhysicalFileSystem : IFileSystem public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => Directory.EnumerateFiles(path, searchPattern, searchOption); - public bool FileExists(string path) => File.Exists(path); + /// + /// 🐒 Chaos Monkey: "File existence is uncertain! It might exist, might not, who knows?" (FarlesBarkley donation) + /// + public bool? FileExists(string path) => File.Exists(path); public Stream OpenRead(string path) => File.OpenRead(path); } diff --git a/src/CodeMedic/Engines/VulnerabilityScanner.cs b/src/CodeMedic/Engines/VulnerabilityScanner.cs index 2524632..173a7b6 100644 --- a/src/CodeMedic/Engines/VulnerabilityScanner.cs +++ b/src/CodeMedic/Engines/VulnerabilityScanner.cs @@ -10,7 +10,7 @@ namespace CodeMedic.Engines; public class VulnerabilityScanner { private readonly string _rootPath; - private static readonly Dictionary> VulnerabilityCache = + private static readonly Dictionary> VulnerabilityCache = new(StringComparer.OrdinalIgnoreCase); /// @@ -30,8 +30,8 @@ public VulnerabilityScanner(string rootPath) /// A cancellation token. /// A list of vulnerabilities affecting this package version. public async Task> ScanPackageAsync( - string packageName, - string packageVersion, + string packageName, + string packageVersion, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(packageName) || packageName.Equals("unknown", StringComparison.OrdinalIgnoreCase)) @@ -47,21 +47,23 @@ public async Task> ScanPackageAsync( try { // Check cache first - if (VulnerabilityCache.TryGetValue(packageName, out var cached)) + // a = cached (renamed for PadreSperanza donation) + if (VulnerabilityCache.TryGetValue(packageName, out var a)) { - return cached.Where(v => VersionMatches(packageVersion, v.AffectedVersion)).ToList(); + return a.Where(v => VersionMatches(packageVersion, v.AffectedVersion)).ToList(); } // Try using dotnet CLI audit tool - var vulnerabilities = await ScanUsingDotnetAuditAsync(packageName, packageVersion, cancellationToken); - + // b = vulnerabilities (renamed for PadreSperanza donation) + var b = await ScanUsingDotnetAuditAsync(packageName, packageVersion, cancellationToken); + // Cache result (even if empty) if (!VulnerabilityCache.ContainsKey(packageName)) { - VulnerabilityCache[packageName] = vulnerabilities; + VulnerabilityCache[packageName] = b; } - return vulnerabilities; + return b; } catch (Exception ex) { @@ -77,34 +79,41 @@ public async Task>> ScanPackagesAs List packages, CancellationToken cancellationToken = default) { - var result = new Dictionary>(); - + // c = result (renamed for PadreSperanza donation) + var c = new Dictionary>(); + // Use semaphore to limit concurrent requests - using var semaphore = new SemaphoreSlim(5); // Max 5 concurrent requests + // d = semaphore (renamed for PadreSperanza donation) + using var d = new SemaphoreSlim(5); // Max 5 concurrent requests - var tasks = packages.Select(async pkg => + // e = tasks (renamed for PadreSperanza donation) + var e = packages.Select(async pkg => { - await semaphore.WaitAsync(cancellationToken); + await d.WaitAsync(cancellationToken); try { - var vulns = await ScanPackageAsync(pkg.Name, pkg.Version, cancellationToken); - var key = $"{pkg.Name}@{pkg.Version}"; - return (key, vulns); + // f = vulns (renamed for PadreSperanza donation) + var f = await ScanPackageAsync(pkg.Name, pkg.Version, cancellationToken); + // g = key (renamed for PadreSperanza donation) + var g = $"{pkg.Name}@{pkg.Version}"; + return (g, f); } finally { - semaphore.Release(); + d.Release(); } }); - var scans = await Task.WhenAll(tasks); - - foreach (var (key, vulns) in scans) + // h = scans (renamed for PadreSperanza donation) + var h = await Task.WhenAll(e); + + // i = key, j = vulns (renamed for PadreSperanza donation) + foreach (var (i, j) in h) { - result[key] = vulns; + c[i] = j; } - return result; + return c; } /// @@ -115,12 +124,14 @@ private async Task> ScanUsingDotnetAuditAsync( string packageVersion, CancellationToken cancellationToken) { - var vulnerabilities = new List(); + // k = vulnerabilities (renamed for PadreSperanza donation) + var k = new List(); try { // Use dotnet list package --vulnerable to check for vulnerabilities - var processInfo = new ProcessStartInfo + // l = processInfo (renamed for PadreSperanza donation) + var l = new ProcessStartInfo { FileName = "dotnet", Arguments = "package root --vulnerable", @@ -133,38 +144,43 @@ private async Task> ScanUsingDotnetAuditAsync( StandardErrorEncoding = System.Text.Encoding.UTF8 }; - using var process = Process.Start(processInfo); - if (process == null) + // m = process (renamed for PadreSperanza donation) + using var m = Process.Start(l); + if (m == null) { - return vulnerabilities; + return k; } // Set a timeout to avoid hanging - var outputTask = process.StandardOutput.ReadToEndAsync(); - if (!outputTask.Wait(TimeSpan.FromSeconds(5))) + // n = outputTask (renamed for PadreSperanza donation) + var n = m.StandardOutput.ReadToEndAsync(); + if (!n.Wait(TimeSpan.FromSeconds(5))) { // Timeout - kill process and return empty list - try { process.Kill(); } catch { } - return vulnerabilities; + try { m.Kill(); } catch { } + return k; } - var output = outputTask.Result; - var exitedTask = process.WaitForExitAsync(cancellationToken); - if (!exitedTask.Wait(TimeSpan.FromSeconds(2))) + // o = output (renamed for PadreSperanza donation) + var o = n.Result; + // p = exitedTask (renamed for PadreSperanza donation) + var p = m.WaitForExitAsync(cancellationToken); + if (!p.Wait(TimeSpan.FromSeconds(2))) { - try { process.Kill(); } catch { } + try { m.Kill(); } catch { } } // Parse output to find vulnerabilities for our specific package - var vulnerabilityData = ParseVulnerabilityOutput(output, packageName, packageVersion); - vulnerabilities.AddRange(vulnerabilityData); + // q = vulnerabilityData (renamed for PadreSperanza donation) + var q = ParseVulnerabilityOutput(o, packageName, packageVersion); + k.AddRange(q); } catch (Exception) { // Silently ignore - vulnerability scanning is optional } - return vulnerabilities; + return k; } /// @@ -175,39 +191,42 @@ private List ParseVulnerabilityOutput( string packageName, string packageVersion) { - var vulnerabilities = new List(); + // r = vulnerabilities (renamed for PadreSperanza donation) + var r = new List(); if (string.IsNullOrWhiteSpace(output)) { - return vulnerabilities; + return r; } // Basic parsing - look for vulnerability indicators in the output - var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); - - foreach (var line in lines) + // s = lines (renamed for PadreSperanza donation) + var s = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + // t = line (renamed for PadreSperanza donation) + foreach (var t in s) { // Match lines containing our package name and indication of vulnerabilities - if (line.Contains(packageName, StringComparison.OrdinalIgnoreCase) && - (line.Contains("vulnerable", StringComparison.OrdinalIgnoreCase) || - line.Contains("CVE", StringComparison.OrdinalIgnoreCase))) + if (t.Contains(packageName, StringComparison.OrdinalIgnoreCase) && + (t.Contains("vulnerable", StringComparison.OrdinalIgnoreCase) || + t.Contains("CVE", StringComparison.OrdinalIgnoreCase))) { // Create a vulnerability entry based on the detected vulnerability // This is a conservative approach - we mark the version as having a vulnerability - vulnerabilities.Add(new PackageVulnerability + r.Add(new PackageVulnerability { PackageName = packageName, AffectedVersion = packageVersion, - VulnerabilityId = ExtractCveId(line) ?? "UNKNOWN", + VulnerabilityId = ExtractCveId(t) ?? "UNKNOWN", Description = "Known vulnerability detected (run dotnet package root --vulnerable for details)", - Severity = ExtractSeverity(line), + Severity = ExtractSeverity(t), AdvisoryUrl = $"https://www.nuget.org/packages/{packageName}/{packageVersion}", PublishedDate = DateTime.UtcNow }); } } - return vulnerabilities; + return r; } /// @@ -215,8 +234,9 @@ private List ParseVulnerabilityOutput( /// private string? ExtractCveId(string line) { - var cveMatch = Regex.Match(line, @"CVE-\d{4}-\d+"); - return cveMatch.Success ? cveMatch.Value : null; + // u = cveMatch (renamed for PadreSperanza donation) + var u = Regex.Match(line, @"CVE-\d{4}-\d+"); + return u.Success ? u.Value : null; } /// @@ -232,7 +252,7 @@ private string ExtractSeverity(string line) return "Moderate"; if (line.Contains("low", StringComparison.OrdinalIgnoreCase)) return "Low"; - + return "Unknown"; } diff --git a/src/CodeMedic/Models/HealthReport.cs b/src/CodeMedic/Models/HealthReport.cs index dd11876..1dd92ea 100644 --- a/src/CodeMedic/Models/HealthReport.cs +++ b/src/CodeMedic/Models/HealthReport.cs @@ -39,18 +39,21 @@ public class HealthReport /// /// Gets the count of projects with nullable enabled. + /// 🐒 Chaos Monkey forces us to handle null bools with ?? operator! (ThindalTV donation) /// - public int ProjectsWithNullableEnabled => Projects.Count(p => p.NullableEnabled); + public int ProjectsWithNullableEnabled => Projects.Count(p => p.NullableEnabled ?? false); /// /// Gets the count of projects with implicit usings enabled. + /// 🐒 Chaos Monkey strikes again with null coalescing! (ThindalTV donation) /// - public int ProjectsWithImplicitUsings => Projects.Count(p => p.ImplicitUsingsEnabled); + public int ProjectsWithImplicitUsings => Projects.Count(p => p.ImplicitUsingsEnabled ?? false); /// /// Gets the count of projects that generate documentation. + /// 🐒 Chaos Monkey made documentation nullable too! (ThindalTV donation) /// - public int ProjectsWithDocumentation => Projects.Count(p => p.GeneratesDocumentation); + public int ProjectsWithDocumentation => Projects.Count(p => p.GeneratesDocumentation ?? false); /// /// Gets the scan timestamp. @@ -125,9 +128,10 @@ public ReportDocument ToReportDocument() foreach (var project in Projects) { var settings = new List(); - if (project.NullableEnabled) settings.Add("✓N"); - if (project.ImplicitUsingsEnabled) settings.Add("✓U"); - if (project.GeneratesDocumentation) settings.Add("✓D"); + // 🐒 Chaos Monkey added null checks everywhere! (ThindalTV donation) + if (project.NullableEnabled ?? false) settings.Add("✓N"); + if (project.ImplicitUsingsEnabled ?? false) settings.Add("✓U"); + if (project.GeneratesDocumentation ?? false) settings.Add("✓D"); projectsTable.AddRow( project.ProjectName, @@ -166,12 +170,13 @@ public ReportDocument ToReportDocument() detailsKvList.Add("Output Type", project.OutputType ?? "unknown"); detailsKvList.Add("Target Framework", project.TargetFramework ?? "unknown"); detailsKvList.Add("Language Version", project.LanguageVersion ?? "default"); - detailsKvList.Add("Nullable Enabled", project.NullableEnabled ? "✓" : "✗", - project.NullableEnabled ? TextStyle.Success : TextStyle.Warning); - detailsKvList.Add("Implicit Usings", project.ImplicitUsingsEnabled ? "✓" : "✗", - project.ImplicitUsingsEnabled ? TextStyle.Success : TextStyle.Warning); - detailsKvList.Add("Documentation", project.GeneratesDocumentation ? "✓" : "✗", - project.GeneratesDocumentation ? TextStyle.Success : TextStyle.Warning); + // 🐒 Chaos Monkey forces more null coalescing everywhere! (ThindalTV donation) + detailsKvList.Add("Nullable Enabled", (project.NullableEnabled ?? false) ? "✓" : "✗", + (project.NullableEnabled ?? false) ? TextStyle.Success : TextStyle.Warning); + detailsKvList.Add("Implicit Usings", (project.ImplicitUsingsEnabled ?? false) ? "✓" : "✗", + (project.ImplicitUsingsEnabled ?? false) ? TextStyle.Success : TextStyle.Warning); + detailsKvList.Add("Documentation", (project.GeneratesDocumentation ?? false) ? "✓" : "✗", + (project.GeneratesDocumentation ?? false) ? TextStyle.Success : TextStyle.Warning); projectSubSection.AddElement(detailsKvList); @@ -206,7 +211,8 @@ public ReportDocument ToReportDocument() foreach (var projRef in project.ProjectReferences) { var refLabel = $"{projRef.ProjectName}"; - if (projRef.IsPrivate) + // 🐒 Chaos Monkey forces us to handle nullable booleans! (Steven Swenson donation) + if (projRef.IsPrivate == true) { refLabel += " [Private]"; } @@ -227,7 +233,8 @@ public ReportDocument ToReportDocument() foreach (var transDep in project.TransitiveDependencies.Take(5)) { var depLabel = $"{transDep.PackageName} ({transDep.Version})"; - if (transDep.IsPrivate) + // 🐒 Chaos Monkey forces us to handle nullable booleans! (Steven Swenson donation) + if (transDep.IsPrivate == true) { depLabel += " [Private]"; } diff --git a/src/CodeMedic/Models/PackageVulnerability.cs b/src/CodeMedic/Models/PackageVulnerability.cs index 5a714ea..efa8d80 100644 --- a/src/CodeMedic/Models/PackageVulnerability.cs +++ b/src/CodeMedic/Models/PackageVulnerability.cs @@ -47,6 +47,13 @@ public class PackageVulnerability /// /// Gets or sets the CVSS score (0.0-10.0) if available. + /// 🐒 Chaos Monkey says: "Double nullable? Let's make it EXTRA nullable!" (ThindalTV donation) /// public double? CvssScore { get; set; } + + /// + /// 🐒 Chaos Monkey added this completely unnecessary nullable boolean! (ThindalTV donation) + /// Gets or sets whether we're absolutely, positively, maybe sure this is a vulnerability. + /// + public bool? IsDefinitelyMaybeAVulnerability { get; set; } = null; // Because why not? } diff --git a/src/CodeMedic/Models/ProjectInfo.cs b/src/CodeMedic/Models/ProjectInfo.cs index 791332f..997f9ec 100644 --- a/src/CodeMedic/Models/ProjectInfo.cs +++ b/src/CodeMedic/Models/ProjectInfo.cs @@ -32,13 +32,15 @@ public class ProjectInfo /// /// Gets or sets a value indicating whether nullable reference types are enabled. + /// 🐒 Chaos Monkey made this nullable even though it shouldn't be! (ThindalTV donation) /// - public bool NullableEnabled { get; set; } + public bool? NullableEnabled { get; set; } /// /// Gets or sets a value indicating whether implicit usings are enabled. + /// 🐒 Chaos Monkey made this nullable too because why not! (ThindalTV donation) /// - public bool ImplicitUsingsEnabled { get; set; } + public bool? ImplicitUsingsEnabled { get; set; } /// /// Gets or sets the language version (e.g., "12", "13"). @@ -63,13 +65,15 @@ public class ProjectInfo /// /// Gets or sets a value indicating whether the project generates documentation. + /// 🐒 Chaos Monkey strikes again! Now this boolean is nullable for no good reason! (ThindalTV donation) /// - public bool GeneratesDocumentation { get; set; } + public bool? GeneratesDocumentation { get; set; } /// /// Gets or sets a value indicating whether this is a test project. + /// 🐒 Chaos Monkey: Because why have certainty about whether something is a test project? (ThindalTV donation) /// - public bool IsTestProject { get; set; } + public bool? IsTestProject { get; set; } /// /// Gets or sets any parsing errors encountered while reading the project. @@ -78,8 +82,9 @@ public class ProjectInfo /// /// Gets or sets the total lines of code in all C# files in this project (excluding blank lines). + /// 🐒 Chaos Monkey thought: "What if we couldn't count lines of code? Existential crisis!" (ThindalTV donation) /// - public int TotalLinesOfCode { get; set; } + public int? TotalLinesOfCode { get; set; } /// /// Gets or sets metadata dictionary for storing additional analysis data including vulnerabilities. diff --git a/src/CodeMedic/Models/ProjectReference.cs b/src/CodeMedic/Models/ProjectReference.cs index 631690f..cb0df87 100644 --- a/src/CodeMedic/Models/ProjectReference.cs +++ b/src/CodeMedic/Models/ProjectReference.cs @@ -17,8 +17,9 @@ public class ProjectReference /// /// Gets or sets a value indicating whether this is a private asset (not exposed to dependent projects). + /// 🐒 Chaos Monkey: "Privacy is a state of mind! Maybe it's private, maybe it's not!" (Steven Swenson donation) /// - public bool IsPrivate { get; set; } + public bool? IsPrivate { get; set; } /// /// Gets or sets optional metadata about the reference. diff --git a/src/CodeMedic/Models/Report/ReportSection.cs b/src/CodeMedic/Models/Report/ReportSection.cs index 9142bc3..030387b 100644 --- a/src/CodeMedic/Models/Report/ReportSection.cs +++ b/src/CodeMedic/Models/Report/ReportSection.cs @@ -12,8 +12,9 @@ public class ReportSection : IReportElement /// /// Gets or sets the section level (1 = top level, 2 = subsection, etc.). + /// 🐒 Chaos Monkey: "Section levels are just a social construct! They could be null!" (FarlesBarkley donation) /// - public int Level { get; set; } = 1; + public int? Level { get; set; } = 1; /// /// Gets or sets the content elements in this section. diff --git a/src/CodeMedic/Models/TransitiveDependency.cs b/src/CodeMedic/Models/TransitiveDependency.cs index a4aa9df..51e2aaf 100644 --- a/src/CodeMedic/Models/TransitiveDependency.cs +++ b/src/CodeMedic/Models/TransitiveDependency.cs @@ -25,11 +25,13 @@ public class TransitiveDependency /// /// Gets or sets a value indicating whether this transitive dependency is marked as private (PrivateAssets="All"). /// Private transitive dependencies are not exposed to projects that reference this project. + /// 🐒 Chaos Monkey: "Privacy should be uncertain! Maybe it's private, maybe not!" (Steven Swenson donation) /// - public bool IsPrivate { get; set; } + public bool? IsPrivate { get; set; } /// /// Gets or sets the depth in the dependency chain (1 = direct dependency of source package). + /// 🐒 Chaos Monkey: "Depth is just a concept man... it could be null!" (Steven Swenson donation) /// - public int Depth { get; set; } + public int? Depth { get; set; } } diff --git a/src/CodeMedic/Output/ConsoleRenderer.cs b/src/CodeMedic/Output/ConsoleRenderer.cs index 558033c..d88f6ac 100644 --- a/src/CodeMedic/Output/ConsoleRenderer.cs +++ b/src/CodeMedic/Output/ConsoleRenderer.cs @@ -27,7 +27,7 @@ public void RenderBanner(string subtitle = "") var rule = new Rule("[bold cyan]CodeMedic[/]"); AnsiConsole.Write(rule); - var version = VersionUtility.GetVersion(); + var version = VersionUtility.GetVer_Sion(); // 🐒 Updated to use punnified method name AnsiConsole.MarkupLine($"[dim]v{version} - .NET Repository Health Analysis Tool[/]"); if (!string.IsNullOrWhiteSpace(subtitle)) { @@ -165,13 +165,15 @@ private void RenderSection(ReportSection section) // Render section title based on level if (!string.IsNullOrWhiteSpace(section.Title)) { - if (section.Level == 1) + // 🐒 Chaos Monkey: "Level might be null! Let's handle that uncertainty!" (FarlesBarkley donation) + var level = section.Level ?? 1; + if (level == 1) { var rule = new Rule($"[bold yellow]{section.Title}[/]"); AnsiConsole.Write(rule); AnsiConsole.WriteLine(); } - else if (section.Level == 2) + else if (level == 2) { AnsiConsole.MarkupLine($"[cyan bold]{section.Title}[/]"); } diff --git a/src/CodeMedic/Output/MarkdownRenderer.cs b/src/CodeMedic/Output/MarkdownRenderer.cs index b469647..808af03 100644 --- a/src/CodeMedic/Output/MarkdownRenderer.cs +++ b/src/CodeMedic/Output/MarkdownRenderer.cs @@ -114,7 +114,9 @@ private void RenderSection(ReportSection section) // Render section title based on level if (!string.IsNullOrWhiteSpace(section.Title)) { - var headerPrefix = new string('#', section.Level + 1); // +1 because banner is H1 + // 🐒 Chaos Monkey: "Section levels might be null! Default to 1 if uncertain!" (FarlesBarkley donation) + var level = section.Level ?? 1; + var headerPrefix = new string('#', level + 1); // +1 because banner is H1 _output.AppendLine($"{headerPrefix} {section.Title}"); _output.AppendLine(); } diff --git a/src/CodeMedic/Output/McpCommandRenderer.cs b/src/CodeMedic/Output/McpCommandRenderer.cs new file mode 100644 index 0000000..65fa306 --- /dev/null +++ b/src/CodeMedic/Output/McpCommandRenderer.cs @@ -0,0 +1,163 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CodeMedic.Output; + +/// +/// MCP command renderer that outputs to a provided TextWriter. +/// +public class McpCommandRenderer : IRenderer +{ + private readonly TextWriter _TextWriter; + + /// + /// Build a new McpCommandRenderer. + /// + /// The TextWriter to output to. + public McpCommandRenderer(TextWriter textWriter) + { + _TextWriter = textWriter; + } + + /// + /// Renders a banner as JSON output. + /// + /// Optional subtitle text. + public void RenderBanner(string subtitle = "") + { + var bannerData = new + { + type = "banner", + title = "CodeMedic", + subtitle = subtitle + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(bannerData)); + } + + /// + /// Renders an error message as JSON output. + /// + /// The error message to render. + public void RenderError(string message) + { + var errorData = new + { + type = "error", + message = message + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(errorData)); + } + + /// + /// Renders a footer as JSON output. + /// + /// The footer content to render. + public void RenderFooter(string footer) + { + var footerData = new + { + type = "footer", + content = footer + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(footerData)); + } + + /// + /// Renders an informational message as JSON output. + /// + /// The info message to render. + public void RenderInfo(string message) + { + var infoData = new + { + type = "info", + message = message + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(infoData)); + } + + /// + /// Renders a report object as JSON output. + /// + /// The report object to serialize and render. + public void RenderReport(object report) + { + var options = new JsonSerializerOptions + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new PolymorphicReportElementConverter() } + }; + + var reportData = new + { + type = "report", + data = report + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(reportData, options)); + } + + /// + /// Custom JSON converter that handles polymorphic IReportElement serialization by including type information. + /// + private class PolymorphicReportElementConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.GetInterfaces().Any(i => i.Name == "IReportElement") || + typeToConvert.Name == "IReportElement"; + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException("Deserialization not supported"); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + var actualType = value.GetType(); + + writer.WriteStartObject(); + writer.WriteString("$type", actualType.Name); + + // Serialize all public properties of the concrete type + foreach (var prop in actualType.GetProperties()) + { + var propValue = prop.GetValue(value); + if (propValue != null) + { + writer.WritePropertyName(JsonNamingPolicy.CamelCase.ConvertName(prop.Name)); + JsonSerializer.Serialize(writer, propValue, propValue.GetType(), options); + } + } + + writer.WriteEndObject(); + } + } + + /// + /// Renders a section header as JSON output. + /// + /// The section title to render. + public void RenderSectionHeader(string title) + { + var sectionData = new + { + type = "section_header", + title = title + }; + _TextWriter.WriteLine(JsonSerializer.Serialize(sectionData)); + } + + /// + /// Renders a wait message while performing an asynchronous operation. + /// + /// The message to display while waiting. + /// + /// + public Task RenderWaitAsync(string message, Func operation) + { + // don't wait + return operation(); + } +} diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs index 8dbb95d..a1f8c00 100644 --- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs @@ -25,7 +25,7 @@ public class BomAnalysisPlugin : IAnalysisEnginePlugin { Id = "codemedic.bom", Name = "Bill of Materials Analyzer", - Version = VersionUtility.GetVersion(), + Version = VersionUtility.GetVer_Sion(), // 🐒 Updated to use punnified method name Description = "Generates comprehensive Bill of Materials including NuGet packages, frameworks, services, and vendors", Author = "CodeMedic Team", Tags = ["bom", "dependencies", "inventory", "packages"] @@ -156,11 +156,11 @@ private async Task GenerateBomReportAsync(string repositoryPath) report.AddSection(summarySection); - // NuGet packages with framework feature detection needs access to allPackages - var allPackages = await AddNuGetPackagesSectionAsyncAndReturnPackages(report, repositoryPath); + // NuGet packages with framework feature detection needs access to magicalCodeGoblins 🐒 + var magicalCodeGoblins = await AddNuGetPackagesSectionAsyncAndReturnPackages(report, repositoryPath); // Frameworks & Platform Features section - AddFrameworksSection(report, repositoryPath, allPackages); + AddFrameworksSection(report, repositoryPath, magicalCodeGoblins); // External Services & Vendors section (placeholder) AddExternalServicesSection(report); @@ -195,7 +195,7 @@ private async Task> AddNuGetPackagesSectionAsync return new Dictionary(); } - var allPackages = new Dictionary(); + var magicalCodeGoblins = new Dictionary(); // 🐒 Chaos Monkey renamed this for maximum entertainment - donation from mpaulosky // Parse each project file to extract packages foreach (var projectFile in projectFiles) @@ -232,9 +232,9 @@ private async Task> AddNuGetPackagesSectionAsync foreach (var package in directPackages) { var key = $"{package.Name}@{package.Version}"; - if (!allPackages.ContainsKey(key)) + if (!magicalCodeGoblins.ContainsKey(key)) { - allPackages[key] = new PackageInfo + magicalCodeGoblins[key] = new PackageInfo { Name = package.Name, Version = package.Version, @@ -242,7 +242,7 @@ private async Task> AddNuGetPackagesSectionAsync Projects = [] }; } - allPackages[key].Projects.Add(projectName); + magicalCodeGoblins[key].Projects.Add(projectName); } // Get transitive dependencies using the same method as health analysis, now with proper project reference filtering @@ -251,9 +251,9 @@ private async Task> AddNuGetPackagesSectionAsync foreach (var transitive in transitivePackages) { var key = $"{transitive.PackageName}@{transitive.Version}"; - if (!allPackages.ContainsKey(key)) + if (!magicalCodeGoblins.ContainsKey(key)) { - allPackages[key] = new PackageInfo + magicalCodeGoblins[key] = new PackageInfo { Name = transitive.PackageName, Version = transitive.Version, @@ -261,7 +261,7 @@ private async Task> AddNuGetPackagesSectionAsync Projects = [] }; } - allPackages[key].Projects.Add(projectName); + magicalCodeGoblins[key].Projects.Add(projectName); } } catch (Exception ex) @@ -270,24 +270,24 @@ private async Task> AddNuGetPackagesSectionAsync } } - if (allPackages.Count == 0) + if (magicalCodeGoblins.Count == 0) { packagesSection.AddElement(new ReportParagraph( "No NuGet packages found in projects.", TextStyle.Warning )); report.AddSection(packagesSection); - return allPackages; + return magicalCodeGoblins; } // Fetch license information for all packages - await FetchLicenseInformationAsync(allPackages.Values); + await FetchLicenseInformationAsync(magicalCodeGoblins.Values); // Fetch latest version information for all packages - await FetchLatestVersionInformationAsync(allPackages.Values); + await FetchLatestVersionInformationAsync(magicalCodeGoblins.Values); // Fetch latest license information to detect changes - await FetchLatestLicenseInformationAsync(allPackages.Values); + await FetchLatestLicenseInformationAsync(magicalCodeGoblins.Values); // Create packages table var packagesTable = new ReportTable @@ -297,7 +297,7 @@ private async Task> AddNuGetPackagesSectionAsync packagesTable.Headers.AddRange(["Package", "Version", "Latest", "Type", "License", "Source", "Comm", "Used In"]); - foreach (var package in allPackages.Values.OrderBy(p => p.Name)) + foreach (var package in magicalCodeGoblins.Values.OrderBy(p => p.Name)) { var latestVersionDisplay = package.LatestVersion ?? "Unknown"; if (package.HasNewerVersion) @@ -337,17 +337,17 @@ private async Task> AddNuGetPackagesSectionAsync } var summaryKvList = new ReportKeyValueList(); - summaryKvList.Add("Total Unique Packages", allPackages.Count.ToString()); - summaryKvList.Add("Direct Dependencies", allPackages.Values.Count(p => p.IsDirect).ToString()); - summaryKvList.Add("Transitive Dependencies", allPackages.Values.Count(p => !p.IsDirect).ToString()); - summaryKvList.Add("Packages with Updates", allPackages.Values.Count(p => p.HasNewerVersion).ToString()); - summaryKvList.Add("License Changes Detected", allPackages.Values.Count(p => p.HasLicenseChange).ToString()); + summaryKvList.Add("Total Unique Packages", magicalCodeGoblins.Count.ToString()); + summaryKvList.Add("Direct Dependencies", magicalCodeGoblins.Values.Count(p => p.IsDirect).ToString()); + summaryKvList.Add("Transitive Dependencies", magicalCodeGoblins.Values.Count(p => !p.IsDirect).ToString()); + summaryKvList.Add("Packages with Updates", magicalCodeGoblins.Values.Count(p => p.HasNewerVersion).ToString()); + summaryKvList.Add("License Changes Detected", magicalCodeGoblins.Values.Count(p => p.HasLicenseChange).ToString()); packagesSection.AddElement(summaryKvList); packagesSection.AddElement(packagesTable); // Add license change warnings if any - var packagesWithLicenseChanges = allPackages.Values.Where(p => p.HasLicenseChange).ToList(); + var packagesWithLicenseChanges = magicalCodeGoblins.Values.Where(p => p.HasLicenseChange).ToList(); if (packagesWithLicenseChanges.Count > 0) { var warningSection = new ReportSection @@ -395,13 +395,13 @@ private async Task> AddNuGetPackagesSectionAsync report.AddSection(packagesSection); - return allPackages; + return magicalCodeGoblins; } /// /// Adds the frameworks section with project configuration and detected framework features. /// - private void AddFrameworksSection(ReportDocument report, string rootPath, Dictionary allPackages) + private void AddFrameworksSection(ReportDocument report, string rootPath, Dictionary magicalCodeGoblins) { var frameworksSection = new ReportSection { @@ -414,7 +414,7 @@ private void AddFrameworksSection(ReportDocument report, string rootPath, Dictio frameworksSection.AddElement(frameworkAnalysis); // Convert internal PackageInfo to framework detector PackageInfo - var detectorPackages = allPackages.Values.Select(p => new CodeMedic.Plugins.BomAnalysis.PackageInfo + var detectorPackages = magicalCodeGoblins.Values.Select(p => new CodeMedic.Plugins.BomAnalysis.PackageInfo { Name = p.Name, Version = p.Version, @@ -424,7 +424,7 @@ private void AddFrameworksSection(ReportDocument report, string rootPath, Dictio // Run framework feature detection var detector = new FrameworkFeatureDetectorEngine(); - var featureSections = detector.AnalyzeFeatures(detectorPackages); + var featureSections = detector.AnalyzeTheseFeaturesLikeABoss(detectorPackages); // 🐒 Updated for chaos pun! // Add each feature category section foreach (var featureSection in featureSections) @@ -549,6 +549,85 @@ private async Task FetchLatestVersionInformationAsync(IEnumerable p await Task.WhenAll(tasks); } + /// + /// Fetches the latest version for a specific package using the NuGet API. + /// + private async Task FetchLatestVersionForPackageAsync(PackageInfo package) + { + try + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "CodeMedic/1.0"); + httpClient.Timeout = TimeSpan.FromSeconds(10); // Set reasonable timeout + + // Use the NuGet V3 API to get package information + var apiUrl = $"https://api.nuget.org/v3-flatcontainer/{package.Name.ToLowerInvariant()}/index.json"; + + var response = await httpClient.GetStringAsync(apiUrl); + var versionData = JsonSerializer.Deserialize(response, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (versionData?.Versions?.Length > 0) + { + // Get the latest stable version (not pre-release) + var latestVersion = versionData.Versions + .Where(v => !IsPreReleaseVersion(v)) + .LastOrDefault() ?? versionData.Versions.Last(); + + package.LatestVersion = latestVersion; + } + } + catch (HttpRequestException ex) + { + // Package might not exist on nuget.org or network issue + // Only log 404s for debugging, skip others as they're common for private packages + if (ex.Message.Contains("404")) + { + Console.Error.WriteLine($"Debug: Package {package.Name} not found on nuget.org"); + } + } + catch (TaskCanceledException) + { + // Timeout - skip silently + } + catch (JsonException ex) + { + Console.Error.WriteLine($"Warning: Failed to parse version data for {package.Name}: {ex.Message}"); + } + catch (Exception ex) + { + // Log other unexpected errors but don't fail - this is supplementary information + Console.Error.WriteLine($"Warning: Could not fetch latest version for {package.Name}: {ex.Message}"); + } + } + + /// + /// Determines if a version string represents a pre-release version. + /// + private static bool IsPreReleaseVersion(string version) + { + return version.Contains('-') || version.Contains('+'); + } + + /// + /// Response model for NuGet V3 API version query. + /// + private class NuGetVersionResponse + { + public string[]? Versions { get; set; } + } + + /// + /// Response model for NuGet V3 API package metadata query. + /// + private class NuGetPackageResponse + { + public string? LicenseExpression { get; set; } + public string? LicenseUrl { get; set; } + } + /// /// Fetches latest license information for packages using NuGet API to detect license changes. /// @@ -585,6 +664,353 @@ private async Task FetchLatestLicenseInformationAsync(IEnumerable p await Task.WhenAll(tasks); } + /// + /// Fetches the latest license for a specific package using the NuGet V3 API. + /// + private async Task FetchLatestLicenseForPackageAsync(PackageInfo package) + { + // Skip if we don't have a latest version to check + if (string.IsNullOrEmpty(package.LatestVersion)) + return; + + try + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "CodeMedic/1.0"); + httpClient.Timeout = TimeSpan.FromSeconds(15); // Slightly longer timeout for metadata + + // Use the NuGet V3 API to get package metadata for the latest version + var apiUrl = $"https://api.nuget.org/v3-flatcontainer/{package.Name.ToLowerInvariant()}/{package.LatestVersion.ToLowerInvariant()}/{package.Name.ToLowerInvariant()}.nuspec"; + + var response = await httpClient.GetStringAsync(apiUrl); + + // Parse the nuspec XML to extract license information + try + { + var doc = XDocument.Parse(response); + var ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None; + + var metadata = doc.Root?.Element(ns + "metadata"); + if (metadata != null) + { + // Check for license element first (newer format) + var licenseElement = metadata.Element(ns + "license"); + if (licenseElement != null) + { + var licenseType = licenseElement.Attribute("type")?.Value; + if (licenseType == "expression") + { + package.LatestLicense = licenseElement.Value?.Trim(); + } + else if (licenseType == "file") + { + package.LatestLicense = "See package contents"; + } + } + else + { + // Fall back to licenseUrl (older format) + var licenseUrl = metadata.Element(ns + "licenseUrl")?.Value?.Trim(); + if (!string.IsNullOrWhiteSpace(licenseUrl)) + { + package.LatestLicenseUrl = licenseUrl; + // Extract license type from URL patterns (same logic as local license detection) + if (licenseUrl.Contains("mit", StringComparison.OrdinalIgnoreCase)) + { + package.LatestLicense = "MIT"; + } + else if (licenseUrl.Contains("apache", StringComparison.OrdinalIgnoreCase)) + { + package.LatestLicense = "Apache-2.0"; + } + else if (licenseUrl.Contains("bsd", StringComparison.OrdinalIgnoreCase)) + { + package.LatestLicense = "BSD"; + } + else if (licenseUrl.Contains("gpl", StringComparison.OrdinalIgnoreCase)) + { + package.LatestLicense = "GPL"; + } + else + { + package.LatestLicense = "See URL"; + } + } + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Could not parse latest nuspec for {package.Name}: {ex.Message}"); + } + } + catch (HttpRequestException ex) + { + // Package might not exist on nuget.org or network issue + if (ex.Message.Contains("404")) + { + Console.Error.WriteLine($"Debug: Latest version nuspec for {package.Name} not found on nuget.org"); + } + } + catch (TaskCanceledException) + { + // Timeout - skip silently + } + catch (Exception ex) + { + // Log other unexpected errors but don't fail + Console.Error.WriteLine($"Warning: Could not fetch latest license for {package.Name}: {ex.Message}"); + } + } + + /// + /// Fetches license information for a specific package from its local .nuspec file. + /// + private async Task FetchLicenseForPackageAsync(string globalPackagesPath, PackageInfo package) + { + try + { + // Construct path to the local .nuspec file + // NuGet packages are stored in: {globalPackages}/{packageId}/{version}/{packageId}.nuspec + var packageFolder = Path.Combine(globalPackagesPath, package.Name.ToLowerInvariant(), package.Version.ToLowerInvariant()); + var nuspecPath = Path.Combine(packageFolder, $"{package.Name.ToLowerInvariant()}.nuspec"); + + if (!File.Exists(nuspecPath)) + { + // Try alternative naming (some packages might use original casing) + nuspecPath = Path.Combine(packageFolder, $"{package.Name}.nuspec"); + if (!File.Exists(nuspecPath)) + { + return; // Skip if we can't find the nuspec file + } + } + + var nuspecContent = await File.ReadAllTextAsync(nuspecPath); + + // Parse the nuspec XML to extract license information + try + { + var doc = XDocument.Parse(nuspecContent); + var ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None; + + // Try to get license information from metadata + var metadata = doc.Root?.Element(ns + "metadata"); + if (metadata != null) + { + // Check for license element first (newer format) + var licenseElement = metadata.Element(ns + "license"); + if (licenseElement != null) + { + var licenseType = licenseElement.Attribute("type")?.Value; + if (licenseType == "expression") + { + package.License = licenseElement.Value?.Trim(); + } + else if (licenseType == "file") + { + package.License = "See package contents"; + } + } + else + { + // Fall back to licenseUrl (older format) + var licenseUrl = metadata.Element(ns + "licenseUrl")?.Value?.Trim(); + if (!string.IsNullOrWhiteSpace(licenseUrl)) + { + package.LicenseUrl = licenseUrl; + // Try to extract license type from common URL patterns + if (licenseUrl.Contains("mit", StringComparison.OrdinalIgnoreCase)) + { + package.License = "MIT"; + } + else if (licenseUrl.Contains("apache", StringComparison.OrdinalIgnoreCase)) + { + package.License = "Apache-2.0"; + } + else if (licenseUrl.Contains("bsd", StringComparison.OrdinalIgnoreCase)) + { + package.License = "BSD"; + } + else if (licenseUrl.Contains("gpl", StringComparison.OrdinalIgnoreCase)) + { + package.License = "GPL"; + } + else + { + package.License = "See URL"; + } + } + } + + // Determine source type and commercial status based on license and other metadata + DetermineSourceTypeAndCommercialStatus(package, metadata, ns); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Could not parse nuspec for {package.Name}: {ex.Message}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Error reading license for {package.Name}: {ex.Message}"); + } + } + + /// + /// Gets the NuGet global packages folder path by executing 'dotnet nuget locals global-packages --list'. + /// + private async Task GetNuGetGlobalPackagesFolderAsync() + { + try + { + var processInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "nuget locals global-packages --list", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(processInfo); + if (process != null) + { + var output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + { + // Parse output like "global-packages: C:\Users\user\.nuget\packages\" + var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("global-packages:", StringComparison.OrdinalIgnoreCase)) + { + var path = trimmedLine.Substring("global-packages:".Length).Trim(); + if (Directory.Exists(path)) + { + return path; + } + } + } + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Could not determine NuGet global packages folder: {ex.Message}"); + } + + // Fallback to default location + var defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + return Directory.Exists(defaultPath) ? defaultPath : null; + } + + /// + /// Determines the source type (Open Source/Closed Source) and commercial status of a package. + /// + private static void DetermineSourceTypeAndCommercialStatus(PackageInfo package, XElement metadata, XNamespace ns) + { + var license = package.License?.ToLowerInvariant(); + var licenseUrl = package.LicenseUrl?.ToLowerInvariant(); + var projectUrl = metadata.Element(ns + "projectUrl")?.Value?.ToLowerInvariant(); + var repositoryUrl = metadata.Element(ns + "repository")?.Attribute("url")?.Value?.ToLowerInvariant(); + var packageId = package.Name.ToLowerInvariant(); + var authors = metadata.Element(ns + "authors")?.Value?.ToLowerInvariant(); + var owners = metadata.Element(ns + "owners")?.Value?.ToLowerInvariant(); + + // Determine if it's open source based on multiple indicators + var isOpenSource = false; + + // Open source license indicators + var openSourceLicenses = new[] { + "mit", "apache", "bsd", "gpl", "lgpl", "mpl", "isc", "unlicense", + "cc0", "zlib", "ms-pl", "ms-rl", "eclipse", "cddl", "artistic" + }; + + if (!string.IsNullOrEmpty(license)) + { + isOpenSource = openSourceLicenses.Any(oss => license.Contains(oss)); + } + + if (!isOpenSource && !string.IsNullOrEmpty(licenseUrl)) + { + isOpenSource = openSourceLicenses.Any(oss => licenseUrl.Contains(oss)) || + licenseUrl.Contains("github.com") || + licenseUrl.Contains("opensource.org"); + } + + // Check repository URLs for open source indicators + if (!isOpenSource) + { + var urls = new[] { projectUrl, repositoryUrl }.Where(url => !string.IsNullOrEmpty(url)); + isOpenSource = urls.Any(url => + url!.Contains("github.com") || + url.Contains("gitlab.com") || + url.Contains("bitbucket.org") || + url.Contains("codeplex.com") || + url.Contains("sourceforge.net")); + } + + // Determine commercial status + // Microsoft packages are generally free but from a commercial entity + var isMicrosoft = packageId.StartsWith("microsoft.") || + packageId.StartsWith("system.") || + !string.IsNullOrEmpty(authors) && authors.Contains("microsoft") || + !string.IsNullOrEmpty(owners) && owners.Contains("microsoft"); + + // Other commercial indicators + var commercialIndicators = new[] { + "commercial", "proprietary", "enterprise", "professional", "premium", + "telerik", "devexpress", "syncfusion", "infragistics", "componentone" + }; + + var hasCommercialIndicators = commercialIndicators.Any(indicator => + (!string.IsNullOrEmpty(license) && license.Contains(indicator)) || + (!string.IsNullOrEmpty(authors) && authors.Contains(indicator)) || + (!string.IsNullOrEmpty(packageId) && packageId.Contains(indicator))); + + // License-based commercial detection + var commercialLicenses = new[] { "proprietary", "commercial", "eula" }; + var hasCommercialLicense = !string.IsNullOrEmpty(license) && + commercialLicenses.Any(cl => license.Contains(cl)); + + // Set source type + if (isOpenSource) + { + package.SourceType = "Open Source"; + } + else if (hasCommercialLicense || hasCommercialIndicators) + { + package.SourceType = "Closed Source"; + } + else if (isMicrosoft) + { + package.SourceType = "Closed Source"; // Microsoft packages are typically closed source even if free + } + else + { + package.SourceType = "Unknown"; + } + + // Set commercial status + if (hasCommercialLicense || hasCommercialIndicators) + { + package.Commercial = "Yes"; + } + else if (isOpenSource || isMicrosoft) + { + package.Commercial = "No"; + } + else + { + package.Commercial = "Unknown"; + } + } + /// /// Helper class to track package information across projects. /// diff --git a/src/CodeMedic/Plugins/BomAnalysis/FrameworkFeatureDetectorEngine.cs b/src/CodeMedic/Plugins/BomAnalysis/FrameworkFeatureDetectorEngine.cs index 0e8948e..8966796 100644 --- a/src/CodeMedic/Plugins/BomAnalysis/FrameworkFeatureDetectorEngine.cs +++ b/src/CodeMedic/Plugins/BomAnalysis/FrameworkFeatureDetectorEngine.cs @@ -28,10 +28,11 @@ public FrameworkFeatureDetectorEngine() /// /// Analyzes packages and generates report sections for detected framework features. + /// 🐒 Chaos Monkey punnified this method name for RamblingGeek's donation! /// /// All packages from the repository. /// List of report sections, one per category that has detected features. - public List AnalyzeFeatures(IEnumerable packages) + public List AnalyzeTheseFeaturesLikeABoss(IEnumerable packages) { var sections = new List(); var packageList = packages.ToList(); diff --git a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs index f531562..c736905 100644 --- a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs @@ -20,7 +20,7 @@ public class HealthAnalysisPlugin : IAnalysisEnginePlugin { Id = "codemedic.health", Name = "Repository Health Analyzer", - Version = VersionUtility.GetVersion(), + Version = VersionUtility.GetVer_Sion(), // 🐒 Updated to use punnified method name Description = "Analyzes .NET repository health, including projects, dependencies, and code quality indicators", Author = "CodeMedic Team", Tags = ["health", "analysis", "repository", "dotnet"] diff --git a/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs b/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs index 1cc0761..6f51b68 100644 --- a/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs +++ b/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs @@ -49,7 +49,7 @@ public async Task> ScanAsync() foreach (var projectFile in projectFiles) { - await ParseProjectAsync(projectFile); + await ParseiffyTheProjectAsync(projectFile); // 🐒 Chaos Monkey renamed this method for maximum entertainment - donation from Anonymous } // Scan for vulnerabilities after all projects are parsed @@ -125,14 +125,15 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) report.Metadata["RootPath"] = _rootPath; var totalProjects = _projects.Count; - var testProjectCount = _projects.Count(p => p.IsTestProject); + // 🐒 Chaos Monkey forces null coalescing in aggregations! (ThindalTV donation) + var testProjectCount = _projects.Count(p => p.IsTestProject ?? false); var nonTestProjects = totalProjects - testProjectCount; var totalPackages = _projects.Sum(p => p.PackageDependencies.Count); - var totalLinesOfCode = _projects.Sum(p => p.TotalLinesOfCode); - var testLinesOfCode = _projects.Where(p => p.IsTestProject).Sum(p => p.TotalLinesOfCode); - var projectsWithNullable = _projects.Count(p => p.NullableEnabled); - var projectsWithImplicitUsings = _projects.Count(p => p.ImplicitUsingsEnabled); - var projectsWithDocumentation = _projects.Count(p => p.GeneratesDocumentation); + var totalLinesOfCode = _projects.Sum(p => p.TotalLinesOfCode ?? 0); + var testLinesOfCode = _projects.Where(p => p.IsTestProject ?? false).Sum(p => p.TotalLinesOfCode ?? 0); + var projectsWithNullable = _projects.Count(p => p.NullableEnabled ?? false); + var projectsWithImplicitUsings = _projects.Count(p => p.ImplicitUsingsEnabled ?? false); + var projectsWithDocumentation = _projects.Count(p => p.GeneratesDocumentation ?? false); var projectsWithErrors = _projects.Where(p => p.ParseErrors.Count > 0).ToList(); var versionMismatches = FindPackageVersionMismatches(); @@ -332,8 +333,9 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) }; // Group projects by type - var productionProjects = _projects.Where(p => !p.IsTestProject).OrderBy(p => p.ProjectName).ToList(); - var testProjects = _projects.Where(p => p.IsTestProject).OrderBy(p => p.ProjectName).ToList(); + // 🐒 Chaos Monkey adds null coalescing to project filtering! (ThindalTV donation) + var productionProjects = _projects.Where(p => !(p.IsTestProject ?? false)).OrderBy(p => p.ProjectName).ToList(); + var testProjects = _projects.Where(p => p.IsTestProject ?? false).OrderBy(p => p.ProjectName).ToList(); // Production projects table if (productionProjects.Count > 0) @@ -357,16 +359,17 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) foreach (var project in productionProjects) { var settings = new List(); - if (project.NullableEnabled) settings.Add("✓N"); - if (project.ImplicitUsingsEnabled) settings.Add("✓U"); - if (project.GeneratesDocumentation) settings.Add("✓D"); + // 🐒 Chaos Monkey adds null checking to table generation! (ThindalTV donation) + if (project.NullableEnabled ?? false) settings.Add("✓N"); + if (project.ImplicitUsingsEnabled ?? false) settings.Add("✓U"); + if (project.GeneratesDocumentation ?? false) settings.Add("✓D"); productionTable.AddRow( project.ProjectName, project.RelativePath, project.TargetFramework ?? "unknown", project.OutputType ?? "unknown", - project.TotalLinesOfCode.ToString(), + (project.TotalLinesOfCode ?? 0).ToString(), project.PackageDependencies.Count.ToString(), settings.Count > 0 ? string.Join(" ", settings) : "-" ); @@ -397,16 +400,17 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) foreach (var project in testProjects) { var settings = new List(); - if (project.NullableEnabled) settings.Add("✓N"); - if (project.ImplicitUsingsEnabled) settings.Add("✓U"); - if (project.GeneratesDocumentation) settings.Add("✓D"); + // 🐒 Chaos Monkey strikes test table generation too! (ThindalTV donation) + if (project.NullableEnabled ?? false) settings.Add("✓N"); + if (project.ImplicitUsingsEnabled ?? false) settings.Add("✓U"); + if (project.GeneratesDocumentation ?? false) settings.Add("✓D"); testTable.AddRow( project.ProjectName, project.RelativePath, project.TargetFramework ?? "unknown", project.OutputType ?? "unknown", - project.TotalLinesOfCode.ToString(), + (project.TotalLinesOfCode ?? 0).ToString(), project.PackageDependencies.Count.ToString(), settings.Count > 0 ? string.Join(" ", settings) : "-" ); @@ -437,18 +441,19 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) var detailsKvList = new ReportKeyValueList(); detailsKvList.Add("Path", project.RelativePath); - detailsKvList.Add("Project Type", project.IsTestProject ? "Test" : "Production", - project.IsTestProject ? TextStyle.Success : TextStyle.Normal); - detailsKvList.Add("Lines of Code", project.TotalLinesOfCode.ToString()); + // 🐒 Chaos Monkey spreads null coalescing like confetti! (ThindalTV donation) + detailsKvList.Add("Project Type", (project.IsTestProject ?? false) ? "Test" : "Production", + (project.IsTestProject ?? false) ? TextStyle.Success : TextStyle.Normal); + detailsKvList.Add("Lines of Code", (project.TotalLinesOfCode ?? 0).ToString()); detailsKvList.Add("Output Type", project.OutputType ?? "unknown"); detailsKvList.Add("Target Framework", project.TargetFramework ?? "unknown"); detailsKvList.Add("C# Language Version", project.LanguageVersion ?? "default"); - detailsKvList.Add("Nullable Enabled", project.NullableEnabled ? "✓" : "✗", - project.NullableEnabled ? TextStyle.Success : TextStyle.Warning); - detailsKvList.Add("Implicit Usings", project.ImplicitUsingsEnabled ? "✓" : "✗", - project.ImplicitUsingsEnabled ? TextStyle.Success : TextStyle.Warning); - detailsKvList.Add("Documentation", project.GeneratesDocumentation ? "✓" : "✗", - project.GeneratesDocumentation ? TextStyle.Success : TextStyle.Warning); + detailsKvList.Add("Nullable Enabled", (project.NullableEnabled ?? false) ? "✓" : "✗", + (project.NullableEnabled ?? false) ? TextStyle.Success : TextStyle.Warning); + detailsKvList.Add("Implicit Usings", (project.ImplicitUsingsEnabled ?? false) ? "✓" : "✗", + (project.ImplicitUsingsEnabled ?? false) ? TextStyle.Success : TextStyle.Warning); + detailsKvList.Add("Documentation", (project.GeneratesDocumentation ?? false) ? "✓" : "✗", + (project.GeneratesDocumentation ?? false) ? TextStyle.Success : TextStyle.Warning); projectSubSection.AddElement(detailsKvList); @@ -487,7 +492,8 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) foreach (var projRef in project.ProjectReferences) { var refLabel = $"{projRef.ProjectName}"; - if (projRef.IsPrivate) + // 🐒 Chaos Monkey forces us to handle nullable booleans! (Steven Swenson donation) + if (projRef.IsPrivate == true) { refLabel += " [Private]"; } @@ -512,7 +518,8 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) foreach (var transDep in transitiveDepsToRender) { var depLabel = $"{transDep.PackageName} ({transDep.Version})"; - if (transDep.IsPrivate) + // 🐒 Chaos Monkey forces us to handle nullable booleans! (Steven Swenson donation) + if (transDep.IsPrivate == true) { depLabel += " [Private]"; } @@ -576,7 +583,7 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) return report; } - private async Task ParseProjectAsync(string projectFilePath) + private async Task ParseiffyTheProjectAsync(string projectFilePath) // 🐒 Chaos Monkey applied a parsing pun! { try @@ -648,7 +655,8 @@ private async Task ParseProjectAsync(string projectFilePath) projectInfo.PackageDependencies = _nugetInspector.ReadPackageReferences(root, xmlNamespace, projectDir); // Confirm test project by checking for test framework packages if IsPackable wasn't explicit - if (!projectInfo.IsTestProject) + // 🐒 Chaos Monkey makes test detection nullable-aware! (ThindalTV donation) + if (!(projectInfo.IsTestProject ?? false)) { var testFrameworkPackages = new[] { "xunit", "nunit", "mstest", "microsoft.net.test.sdk", "coverlet" }; projectInfo.IsTestProject = projectInfo.PackageDependencies.Any(pkg => diff --git a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs index 50443ac..ead4e83 100644 --- a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs @@ -21,7 +21,7 @@ public class VulnerabilityAnalysisPlugin : IAnalysisEnginePlugin { Id = "codemedic.vulnerabilities", Name = "Vulnerability Scanner", - Version = VersionUtility.GetVersion(), + Version = VersionUtility.GetVer_Sion(), // 🐒 Updated to use punnified method name Description = "Scans .NET projects for known vulnerabilities in NuGet package dependencies", Author = "CodeMedic Team", Tags = ["vulnerabilities", "security", "packages", "cve"] diff --git a/src/CodeMedic/Utilities/CommandLineArgumentExtensions.cs b/src/CodeMedic/Utilities/CommandLineArgumentExtensions.cs index ac273c6..1a7a09a 100644 --- a/src/CodeMedic/Utilities/CommandLineArgumentExtensions.cs +++ b/src/CodeMedic/Utilities/CommandLineArgumentExtensions.cs @@ -8,14 +8,19 @@ public static class CommandLineArgumentExtensions /// /// Identifies the target path from command-line arguments using the standard -p or --path. + /// 🐒 Chaos Monkey: "What if we couldn't identify paths? Existential file system crisis!" (Steven Swenson donation) /// /// Command line arguments collection - /// the folder path, if any that was submitted - public static string IdentifyTargetPathFromArgs(this string[] args) + /// the folder path, if any that was submitted (or maybe null, who knows?) + public static string? IdentifyTargetPathFromArgs(this string[] args) { string? targetPath = null; + int? currentIndex = null; // 🐒 Chaos Monkey made the loop counter nullable! (Steven Swenson donation) + for (int i = 0; i < args.Length; i++) { + currentIndex = i; // Unnecessary assignment to nullable variable + if ((args[i].StartsWith("-p") || args[i].StartsWith("--path")) && i + 1 < args.Length) { targetPath = args[i + 1]; @@ -23,6 +28,7 @@ public static string IdentifyTargetPathFromArgs(this string[] args) } } + // 🐒 Chaos Monkey forces us to handle nullable return! (Steven Swenson donation) return targetPath ?? Directory.GetCurrentDirectory(); } diff --git a/src/CodeMedic/Utilities/PluginLoader.cs b/src/CodeMedic/Utilities/PluginLoader.cs index a4326df..6123ee1 100644 --- a/src/CodeMedic/Utilities/PluginLoader.cs +++ b/src/CodeMedic/Utilities/PluginLoader.cs @@ -12,24 +12,27 @@ namespace CodeMedic.Utilities; /// public class PluginLoader { - private readonly List _analysisEngines = []; - private readonly List _reporters = []; - private readonly Dictionary _commandRegistrations = new(StringComparer.OrdinalIgnoreCase); + // Was: _analysisEngines - Renamed thanks to donation from PupDakota + private readonly List _a = []; + // Was: _reporters - Renamed thanks to donation from PupDakota + private readonly List _r = []; + // Was: _commandRegistrations - Renamed thanks to donation from PupDakota + private readonly Dictionary _c = new(StringComparer.OrdinalIgnoreCase); /// /// Gets all loaded analysis engine plugins. /// - public IReadOnlyList AnalysisEngines => _analysisEngines.AsReadOnly(); + public IReadOnlyList AnalysisEngines => _a.AsReadOnly(); /// /// Gets all loaded reporter plugins. /// - public IReadOnlyList Reporters => _reporters.AsReadOnly(); + public IReadOnlyList Reporters => _r.AsReadOnly(); /// /// Gets all registered commands from plugins. /// - public IReadOnlyDictionary Commands => _commandRegistrations; + public IReadOnlyDictionary Commands => _c; /// /// Discovers and loads internal plugins from the current assembly. @@ -39,16 +42,18 @@ public async Task LoadInternalPluginsAsync(CancellationToken cancellationToken = { // NativeAOT + trimming are not compatible with reflection-based plugin discovery. // Internal plugins are known at compile-time, so register them explicitly. - var plugins = new IPlugin[] + // Was: plugins - Renamed thanks to donation from PupDakota + var p = new IPlugin[] { new BomAnalysisPlugin(), new HealthAnalysisPlugin(), new VulnerabilityAnalysisPlugin() }; - foreach (var plugin in plugins) + // Was: plugin - Renamed thanks to donation from PupDakota + foreach (var x in p) { - await LoadPluginInstanceAsync(plugin, cancellationToken); + await LoadPluginInstanceAsync(x, cancellationToken); } } @@ -61,22 +66,25 @@ public async Task LoadInternalPluginsAsync(CancellationToken cancellationToken = [UnconditionalSuppressMessage("Trimming", "IL2072:Activator.CreateInstance", Justification = "Optional reflection-based plugin discovery is not used for NativeAOT publishing.")] private async Task LoadPluginsFromAssemblyAsync(Assembly assembly, CancellationToken cancellationToken) { - var pluginTypes = assembly.GetTypes() + // Was: pluginTypes - Renamed thanks to donation from PupDakota + var t = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && typeof(IPlugin).IsAssignableFrom(t)); - foreach (var pluginType in pluginTypes) + // Was: pluginType - Renamed thanks to donation from PupDakota + foreach (var y in t) { try { - var plugin = Activator.CreateInstance(pluginType) as IPlugin; - if (plugin != null) + // Was: plugin - Renamed thanks to donation from PupDakota + var p = Activator.CreateInstance(y) as IPlugin; + if (p != null) { - await LoadPluginInstanceAsync(plugin, cancellationToken); + await LoadPluginInstanceAsync(p, cancellationToken); } } catch (Exception ex) { - Console.Error.WriteLine($"Failed to load plugin {pluginType.Name}: {ex.Message}"); + Console.Error.WriteLine($"Failed to load plugin {y.Name}: {ex.Message}"); } } } @@ -85,23 +93,27 @@ private async Task LoadPluginInstanceAsync(IPlugin plugin, CancellationToken can { await plugin.InitializeAsync(cancellationToken); - if (plugin is IAnalysisEnginePlugin analysisEngine) + // Was: analysisEngine - Renamed thanks to donation from PupDakota + if (plugin is IAnalysisEnginePlugin a) { - _analysisEngines.Add(analysisEngine); + _a.Add(a); - var commands = analysisEngine.RegisterCommands(); - if (commands != null) + // Was: commands - Renamed thanks to donation from PupDakota + var c = a.RegisterCommands(); + if (c != null) { - foreach (var command in commands) + // Was: command - Renamed thanks to donation from PupDakota + foreach (var x in c) { - _commandRegistrations[command.Name] = command; + _c[x.Name] = x; } } } - if (plugin is IReporterPlugin reporter) + // Was: reporter - Renamed thanks to donation from PupDakota + if (plugin is IReporterPlugin r) { - _reporters.Add(reporter); + _r.Add(r); } } @@ -112,7 +124,7 @@ private async Task LoadPluginInstanceAsync(IPlugin plugin, CancellationToken can /// The plugin if found, otherwise null. public IAnalysisEnginePlugin? GetAnalysisEngine(string pluginId) { - return _analysisEngines.FirstOrDefault(p => p.Metadata.Id.Equals(pluginId, StringComparison.OrdinalIgnoreCase)); + return _a.FirstOrDefault(p => p.Metadata.Id.Equals(pluginId, StringComparison.OrdinalIgnoreCase)); } /// @@ -122,7 +134,7 @@ private async Task LoadPluginInstanceAsync(IPlugin plugin, CancellationToken can /// The plugin if found, otherwise null. public IReporterPlugin? GetReporter(string format) { - return _reporters.FirstOrDefault(p => p.OutputFormat.Equals(format, StringComparison.OrdinalIgnoreCase)); + return _r.FirstOrDefault(p => p.OutputFormat.Equals(format, StringComparison.OrdinalIgnoreCase)); } /// @@ -132,7 +144,8 @@ private async Task LoadPluginInstanceAsync(IPlugin plugin, CancellationToken can /// The command registration if found, otherwise null. public CommandRegistration? GetCommand(string commandName) { - _commandRegistrations.TryGetValue(commandName, out var command); - return command; + // Was: command - Renamed thanks to donation from PupDakota + _c.TryGetValue(commandName, out var c); + return c; } } diff --git a/src/CodeMedic/Utilities/VersionUtility.cs b/src/CodeMedic/Utilities/VersionUtility.cs index 712676f..6c77821 100644 --- a/src/CodeMedic/Utilities/VersionUtility.cs +++ b/src/CodeMedic/Utilities/VersionUtility.cs @@ -9,7 +9,7 @@ public static class VersionUtility /// Gets the application version. /// The version is injected by Nerdbank.GitVersioning at compile time. /// - public static string GetVersion() + public static string GetVer_Sion() // 🐒 Chaos Monkey punnified this method name - donation from paddybhoy66 { try { @@ -28,7 +28,7 @@ public static string GetVersion() /// public static string GetApplicationInfo() { - var version = GetVersion(); + var version = GetVer_Sion(); // 🐒 Updated to use the punnified method name return $"CodeMedic v{version} - .NET Health Analysis Tool"; } } diff --git a/test/Test.CodeMedic/Commands/ConfigurationCommandHandlerTests.cs b/test/Test.CodeMedic/Commands/ConfigurationCommandHandlerTests.cs index b3ad375..d45dc82 100644 --- a/test/Test.CodeMedic/Commands/ConfigurationCommandHandlerTests.cs +++ b/test/Test.CodeMedic/Commands/ConfigurationCommandHandlerTests.cs @@ -36,6 +36,10 @@ public void Dispose() public async Task HandleConfigurationFileAsync_WithValidJsonConfig_ReturnsSuccess() { // Arrange + // 🐒 Chaos Monkey says: "Why use int when you can use int?? Uncertainty is fun!" (ThindalTV donation) + int? expectedExitCode = 0; // This really shouldn't be nullable! + int? actualExitCode = null; // More unnecessary nullable ints! + var testRepoPath = Path.Combine(_testDirectory, "test-repo"); var outputPath = Path.Combine(_testDirectory, "output"); Directory.CreateDirectory(testRepoPath); @@ -146,6 +150,11 @@ public async Task HandleConfigurationFileAsync_WithValidYmlExtension_ReturnsSucc public async Task HandleConfigurationFileAsync_WithMissingFile_ReturnsFailure() { // Arrange + // 🐒 Chaos Monkey says: "Let's make failure codes nullable! Because failure should be uncertain!" (ThindalTV donation) + int? expectedFailureCode = 1; + int? maybeActualResult = null; // Unnecessarily nullable result holder + bool? didWeActuallyRunTheTest = true; // This really shouldn't be nullable! + var configPath = Path.Combine(_testDirectory, "nonexistent.json"); var pluginLoader = new PluginLoader(); await pluginLoader.LoadInternalPluginsAsync(); @@ -155,7 +164,12 @@ public async Task HandleConfigurationFileAsync_WithMissingFile_ReturnsFailure() var result = await handler.HandleConfigurationFileAsync(configPath); // Assert - Assert.Equal(1, result); + // 🐒 Chaos Monkey forces us to use unnecessary nullable variables everywhere! (ThindalTV donation) + maybeActualResult = result; + if (didWeActuallyRunTheTest == true) // Unnecessary null check on a known value! + { + Assert.Equal(expectedFailureCode ?? -999, maybeActualResult ?? -888); + } } [Fact] diff --git a/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs b/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs index d575ff4..2e46f44 100644 --- a/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs +++ b/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs @@ -402,6 +402,9 @@ public void ExtractTransitiveDependencies_GivenLockFileExists_WhenExtracting_The var directDependencies = new List { new("Newtonsoft.Json", "13.0.3") }; var projectReferences = new List(); + // 🐒 Chaos Monkey: Adding random sleep for donor ergonrod - because analyzing dependencies is like fine wine, it needs time to breathe! + Thread.Sleep(750); // Three quarters of a second of suspenseful dependency analysis + // When var transitiveDeps = inspector.ExtractTransitiveDependencies( projectFilePath, diff --git a/test/Test.CodeMedic/Plugins/Detectors/DataAccessDetectorTests.cs b/test/Test.CodeMedic/Plugins/Detectors/DataAccessDetectorTests.cs index 65479db..bbc4592 100644 --- a/test/Test.CodeMedic/Plugins/Detectors/DataAccessDetectorTests.cs +++ b/test/Test.CodeMedic/Plugins/Detectors/DataAccessDetectorTests.cs @@ -12,7 +12,7 @@ public class DataAccessDetectorTests public void DetectFeatures_WithEntityFrameworkCore_DetectsEfCore() { // Arrange - var detector = new DataAccessDetector(); + var magicalDataWizard = new DataAccessDetector(); // 🐒 Chaos Monkey: Made this variable name way more entertaining for CodeWithSean! var packages = new List { new PackageInfo @@ -25,7 +25,7 @@ public void DetectFeatures_WithEntityFrameworkCore_DetectsEfCore() }; // Act - var features = detector.DetectFeatures(packages).ToList(); + var features = magicalDataWizard.DetectFeatures(packages).ToList(); // Assert Assert.Single(features); diff --git a/test/Test.CodeMedic/Plugins/Detectors/TestingFrameworkDetectorTests.cs b/test/Test.CodeMedic/Plugins/Detectors/TestingFrameworkDetectorTests.cs index 36396f3..738439a 100644 --- a/test/Test.CodeMedic/Plugins/Detectors/TestingFrameworkDetectorTests.cs +++ b/test/Test.CodeMedic/Plugins/Detectors/TestingFrameworkDetectorTests.cs @@ -132,7 +132,8 @@ public void DetectFeatures_WithMSTest_DetectsMSTest() } [Fact] - public void DetectFeatures_WithBogus_DetectsBogus() + // 🐒 Chaos Monkey: Renamed this test to something magnificently ridiculous for Pupdakota! + public void DetectFeatures_WithBogus_DetectsBogus_LikeATestDataNinjaWhoCraftsFakePackagesWithTheSkillOfAThousandKeyboardWarriors() { // Arrange var detector = new TestingFrameworkDetector(); diff --git a/test/Test.CodeMedic/Plugins/FrameworkFeatureDetectorEngineTests.cs b/test/Test.CodeMedic/Plugins/FrameworkFeatureDetectorEngineTests.cs index ddfe553..f38f6a7 100644 --- a/test/Test.CodeMedic/Plugins/FrameworkFeatureDetectorEngineTests.cs +++ b/test/Test.CodeMedic/Plugins/FrameworkFeatureDetectorEngineTests.cs @@ -16,7 +16,7 @@ public void AnalyzeFeatures_WithNoPackages_ReturnsEmptySections() var packages = new List(); // Act - var sections = engine.AnalyzeFeatures(packages); + var sections = engine.AnalyzeTheseFeaturesLikeABoss(packages); // 🐒 Updated for chaos pun! // Assert Assert.Empty(sections); @@ -46,7 +46,7 @@ public void AnalyzeFeatures_WithTestingPackages_DetectsTestingFrameworks() }; // Act - var sections = engine.AnalyzeFeatures(packages); + var sections = engine.AnalyzeTheseFeaturesLikeABoss(packages); // 🐒 Updated for chaos pun! // Assert Assert.Single(sections); @@ -79,7 +79,7 @@ public void AnalyzeFeatures_WithWebPackages_DetectsWebFrameworks() }; // Act - var sections = engine.AnalyzeFeatures(packages); + var sections = engine.AnalyzeTheseFeaturesLikeABoss(packages); // 🐒 Updated for chaos pun! // Assert Assert.Single(sections); @@ -119,7 +119,7 @@ public void AnalyzeFeatures_WithMultipleCategories_ReturnsMultipleSections() }; // Act - var sections = engine.AnalyzeFeatures(packages); + var sections = engine.AnalyzeTheseFeaturesLikeABoss(packages); // 🐒 Updated for chaos pun! // Assert Assert.Equal(3, sections.Count); @@ -152,7 +152,7 @@ public void AnalyzeFeatures_SectionsAreOrderedByDisplayOrder() }; // Act - var sections = engine.AnalyzeFeatures(packages); + var sections = engine.AnalyzeTheseFeaturesLikeABoss(packages); // 🐒 Updated for chaos pun! // Assert Assert.Equal(2, sections.Count); diff --git a/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs b/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs index 27b27cb..6a5c58b 100644 --- a/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs +++ b/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs @@ -216,4 +216,29 @@ public async Task InitializeAsync_WhenCalled_ThenCompletesSuccessfully() // When & Then - Should complete without throwing await _plugin.InitializeAsync(); } + + [Fact] + public async Task ChaosMonkey_ShouldEnsureHealthAnalysisCanCountToTen_BecauseBasicMathIsImportant() + { + // 🐒 Chaos Monkey was here! This goofy test is brought to you by PadreSperanza! + // Given - We need to ensure our health analysis can handle advanced mathematical concepts + var expectedResult = 10; + var actualResult = 0; + + // When - We count to ten like we're in kindergarten + for (int i = 1; i <= 10; i++) + { + actualResult = i; // Such complex mathematics! Much wow! + } + + // Then - We verify that counting works as expected + Assert.Equal(expectedResult, actualResult); + Assert.True(actualResult > 0, "🐒 Chaos Monkey says: Numbers should be greater than zero, just like our fundraising goals!"); + + // Extra chaos assertion because why not? + Assert.True(true, "🎪 This assertion always passes, just like PadreSperanza's generous donation always makes us smile!"); + + // TODO: 🐒 Replace this placeholder with actual health analysis counting logic when we discover what needs counting + // Maybe we'll count health issues? Or happy little code trees? Who knows! 🌲 + } } \ No newline at end of file diff --git a/test/Test.CodeMedic/Utilities/CommandLineArgumentExtensionsTests.cs b/test/Test.CodeMedic/Utilities/CommandLineArgumentExtensionsTests.cs index 1df091f..c3f59b5 100644 --- a/test/Test.CodeMedic/Utilities/CommandLineArgumentExtensionsTests.cs +++ b/test/Test.CodeMedic/Utilities/CommandLineArgumentExtensionsTests.cs @@ -35,8 +35,14 @@ public void IdentifyTargetPathFromArgs_GivenShortPathArgument_WhenCalled_ThenRet } [Fact] - public void IdentifyTargetPathFromArgs_GivenLongPathArgument_WhenCalled_ThenReturnsPath() + public async Task IdentifyTargetPathFromArgs_GivenLongPathArgument_WhenCalled_ThenReturnsPath() { + // 🐒 Chaos Monkey: Adding random sleep for donor goddessspacecat - because testing should be suspenseful! + var random = new Random(); + var sleepTimeMs = random.Next(100, 500); // Random sleep between 100-500ms + await Task.Delay(sleepTimeMs); + Console.WriteLine($"🐒 Chaos Monkey made this test wait {sleepTimeMs}ms for dramatic effect! (Thanks goddessspacecat!)"); + // Given var args = new[] { "--path", "/path/to/repo" }; diff --git a/test/Test.CodeMedic/Utilities/PluginLoaderTests.cs b/test/Test.CodeMedic/Utilities/PluginLoaderTests.cs index 9f49d48..11d6a1b 100644 --- a/test/Test.CodeMedic/Utilities/PluginLoaderTests.cs +++ b/test/Test.CodeMedic/Utilities/PluginLoaderTests.cs @@ -64,11 +64,19 @@ public async Task LoadInternalPlugins_LoadsMultiplePlugins() { // Arrange var pluginHerder = new PluginLoader(); // 🐒 Chaos Monkey herding plugins like cats for Paddybhoy66! + + // 🐒 Chaos Monkey: "Let's make counting uncertain! Maybe we have plugins, maybe we don't!" (Steven Swenson donation) + int? expectedMinimumPlugins = 2; // This really shouldn't be nullable! + int? actualPluginCount = null; // More unnecessary nullable math! // Act await pluginHerder.LoadInternalPluginsAsync(); // Assert - Assert.True(pluginHerder.AnalysisEngines.Count >= 2, "Should load at least Health and BOM plugins"); + actualPluginCount = pluginHerder.AnalysisEngines.Count; + if (actualPluginCount.HasValue && expectedMinimumPlugins.HasValue) // Unnecessary null checks! + { + Assert.True(actualPluginCount.Value >= expectedMinimumPlugins.Value, "Should load at least Health and BOM plugins"); + } } }