namespace StellaOps.Concelier.SourceIntel; using System.Text.RegularExpressions; /// /// Parses patch file headers for CVE references (Tier 3). /// Supports DEP-3 format (Debian) and standard patch headers. /// public static partial class PatchHeaderParser { /// /// Parse patch file for CVE references. /// public static PatchHeaderParseResult ParsePatchFile(string patchContent, string patchFilePath) { var lines = patchContent.Split('\n').Take(50).ToArray(); // Only check first 50 lines (header) var cveIds = new HashSet(); var description = ""; var bugReferences = new List(); var origin = ""; foreach (var line in lines) { // Stop at actual diff content if (line.StartsWith("---") || line.StartsWith("+++") || line.StartsWith("@@")) { break; } // DEP-3 Description field if (line.StartsWith("Description:")) { description = line["Description:".Length..].Trim(); } // DEP-3 Bug references if (line.StartsWith("Bug:") || line.StartsWith("Bug-Debian:") || line.StartsWith("Bug-Ubuntu:")) { var bugRef = line.Split(':')[1].Trim(); bugReferences.Add(bugRef); } // DEP-3 Origin if (line.StartsWith("Origin:")) { origin = line["Origin:".Length..].Trim(); } // Look for CVE mentions in any line var cveMatches = CvePatternRegex().Matches(line); foreach (Match match in cveMatches) { cveIds.Add(match.Groups[0].Value); } } // Also check filename for CVE pattern var filenameCves = CvePatternRegex().Matches(patchFilePath); foreach (Match match in filenameCves) { cveIds.Add(match.Groups[0].Value); } var confidence = CalculateConfidence(cveIds.Count, description, origin); return new PatchHeaderParseResult { PatchFilePath = patchFilePath, CveIds = cveIds.ToList(), Description = description, BugReferences = bugReferences, Origin = origin, Confidence = confidence, ParsedAt = DateTimeOffset.UtcNow }; } /// /// Batch parse multiple patches from debian/patches directory. /// public static IReadOnlyList ParsePatchDirectory( string basePath, IEnumerable patchFiles) { var results = new List(); foreach (var patchFile in patchFiles) { try { var fullPath = Path.Combine(basePath, patchFile); if (File.Exists(fullPath)) { var content = File.ReadAllText(fullPath); var result = ParsePatchFile(content, patchFile); if (result.CveIds.Count > 0) { results.Add(result); } } } catch { // Skip files that can't be read continue; } } return results; } private static double CalculateConfidence(int cveCount, string description, string origin) { if (cveCount == 0) { return 0.0; } // Base confidence for patch header CVE mention var confidence = 0.80; // Bonus for multiple CVEs (more explicit) if (cveCount > 1) { confidence += 0.05; } // Bonus for detailed description if (description.Length > 50) { confidence += 0.03; } // Bonus for upstream origin if (origin.Contains("upstream", StringComparison.OrdinalIgnoreCase)) { confidence += 0.02; } return Math.Min(confidence, 0.95); } [GeneratedRegex(@"CVE-\d{4}-[0-9A-Za-z]{4,}")] private static partial Regex CvePatternRegex(); } public sealed record PatchHeaderParseResult { public required string PatchFilePath { get; init; } public required IReadOnlyList CveIds { get; init; } public required string Description { get; init; } public required IReadOnlyList BugReferences { get; init; } public required string Origin { get; init; } public required double Confidence { get; init; } public required DateTimeOffset ParsedAt { get; init; } }