using System.Text; namespace StellaOps.AdvisoryAI.Remediation; /// /// Service for computing and signing SBOM deltas during remediation. /// Sprint: SPRINT_20251226_016_AI_remedy_autopilot /// Task: REMEDY-15, REMEDY-16, REMEDY-17 /// public interface IRemediationDeltaService { /// /// Compute SBOM delta between before and after remediation. /// Task ComputeDeltaAsync( RemediationPlan plan, string beforeSbomPath, string afterSbomPath, CancellationToken cancellationToken = default); /// /// Sign the delta verdict with attestation. /// Task SignDeltaAsync( RemediationDelta delta, IRemediationDeltaSigner signer, CancellationToken cancellationToken = default); /// /// Generate PR description with delta verdict. /// Task GeneratePrDescriptionAsync( RemediationPlan plan, SignedDeltaVerdict signedDelta, CancellationToken cancellationToken = default); } /// /// Signer interface for delta verdicts. /// public interface IRemediationDeltaSigner { string KeyId { get; } string Algorithm { get; } Task SignAsync(byte[] data, CancellationToken cancellationToken = default); } /// /// Delta result from remediation. /// public sealed record RemediationDelta { public required string DeltaId { get; init; } public required string PlanId { get; init; } public required string BeforeSbomDigest { get; init; } public required string AfterSbomDigest { get; init; } public required IReadOnlyList ComponentChanges { get; init; } public required IReadOnlyList VulnerabilityChanges { get; init; } public required DeltaSummary Summary { get; init; } public required string ComputedAt { get; init; } } /// /// A component change in the delta. /// public sealed record ComponentChange { public required string ChangeType { get; init; } // added, removed, upgraded public required string Purl { get; init; } public string? OldVersion { get; init; } public string? NewVersion { get; init; } public required IReadOnlyList AffectedVulnerabilities { get; init; } } /// /// A vulnerability change in the delta. /// public sealed record VulnerabilityChange { public required string ChangeType { get; init; } // fixed, introduced, status_changed public required string VulnerabilityId { get; init; } public required string Severity { get; init; } public string? OldStatus { get; init; } public string? NewStatus { get; init; } public required string ComponentPurl { get; init; } } /// /// Summary of the delta. /// public sealed record DeltaSummary { public required int ComponentsAdded { get; init; } public required int ComponentsRemoved { get; init; } public required int ComponentsUpgraded { get; init; } public required int VulnerabilitiesFixed { get; init; } public required int VulnerabilitiesIntroduced { get; init; } public required int NetVulnerabilityChange { get; init; } public required bool IsImprovement { get; init; } public required string RiskTrend { get; init; } // improved, degraded, stable } /// /// Signed delta verdict. /// public sealed record SignedDeltaVerdict { public required RemediationDelta Delta { get; init; } public required string SignatureId { get; init; } public required string KeyId { get; init; } public required string Algorithm { get; init; } public required string Signature { get; init; } public required string SignedAt { get; init; } } /// /// Default implementation of remediation delta service. /// public sealed class RemediationDeltaService : IRemediationDeltaService { public async Task ComputeDeltaAsync( RemediationPlan plan, string beforeSbomPath, string afterSbomPath, CancellationToken cancellationToken = default) { // In production, this would use the DeltaComputationEngine // For now, create delta from the plan's expected delta var componentChanges = new List(); var vulnChanges = new List(); // Convert expected delta to component changes foreach (var (oldPurl, newPurl) in plan.ExpectedDelta.Upgraded) { componentChanges.Add(new ComponentChange { ChangeType = "upgraded", Purl = oldPurl, OldVersion = ExtractVersion(oldPurl), NewVersion = ExtractVersion(newPurl), AffectedVulnerabilities = new[] { plan.Request.VulnerabilityId } }); } foreach (var purl in plan.ExpectedDelta.Added) { componentChanges.Add(new ComponentChange { ChangeType = "added", Purl = purl, AffectedVulnerabilities = Array.Empty() }); } foreach (var purl in plan.ExpectedDelta.Removed) { componentChanges.Add(new ComponentChange { ChangeType = "removed", Purl = purl, AffectedVulnerabilities = Array.Empty() }); } // Add vulnerability fix vulnChanges.Add(new VulnerabilityChange { ChangeType = "fixed", VulnerabilityId = plan.Request.VulnerabilityId, Severity = "high", // Would come from advisory data OldStatus = "affected", NewStatus = "fixed", ComponentPurl = plan.Request.ComponentPurl }); var summary = new DeltaSummary { ComponentsAdded = plan.ExpectedDelta.Added.Count, ComponentsRemoved = plan.ExpectedDelta.Removed.Count, ComponentsUpgraded = plan.ExpectedDelta.Upgraded.Count, VulnerabilitiesFixed = Math.Abs(Math.Min(0, plan.ExpectedDelta.NetVulnerabilityChange)), VulnerabilitiesIntroduced = Math.Max(0, plan.ExpectedDelta.NetVulnerabilityChange), NetVulnerabilityChange = plan.ExpectedDelta.NetVulnerabilityChange, IsImprovement = plan.ExpectedDelta.NetVulnerabilityChange < 0, RiskTrend = plan.ExpectedDelta.NetVulnerabilityChange < 0 ? "improved" : plan.ExpectedDelta.NetVulnerabilityChange > 0 ? "degraded" : "stable" }; var deltaId = $"delta-{plan.PlanId}-{DateTime.UtcNow:yyyyMMddHHmmss}"; return new RemediationDelta { DeltaId = deltaId, PlanId = plan.PlanId, BeforeSbomDigest = await ComputeFileDigestAsync(beforeSbomPath, cancellationToken), AfterSbomDigest = await ComputeFileDigestAsync(afterSbomPath, cancellationToken), ComponentChanges = componentChanges, VulnerabilityChanges = vulnChanges, Summary = summary, ComputedAt = DateTime.UtcNow.ToString("o") }; } public async Task SignDeltaAsync( RemediationDelta delta, IRemediationDeltaSigner signer, CancellationToken cancellationToken = default) { // Serialize delta to canonical JSON for signing var deltaJson = System.Text.Json.JsonSerializer.Serialize(delta, new System.Text.Json.JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower }); var dataToSign = Encoding.UTF8.GetBytes(deltaJson); var signature = await signer.SignAsync(dataToSign, cancellationToken); var signatureBase64 = Convert.ToBase64String(signature); var signatureId = $"sig-{delta.DeltaId}-{signer.KeyId[..8]}"; return new SignedDeltaVerdict { Delta = delta, SignatureId = signatureId, KeyId = signer.KeyId, Algorithm = signer.Algorithm, Signature = signatureBase64, SignedAt = DateTime.UtcNow.ToString("o") }; } public Task GeneratePrDescriptionAsync( RemediationPlan plan, SignedDeltaVerdict signedDelta, CancellationToken cancellationToken = default) { var sb = new StringBuilder(); sb.AppendLine("## Security Remediation"); sb.AppendLine(); sb.AppendLine($"This PR remediates **{plan.Request.VulnerabilityId}** affecting `{plan.Request.ComponentPurl}`."); sb.AppendLine(); // Risk assessment sb.AppendLine("### Risk Assessment"); sb.AppendLine(); sb.AppendLine($"- **Risk Level**: {plan.RiskAssessment}"); sb.AppendLine($"- **Confidence**: {plan.ConfidenceScore:P0}"); sb.AppendLine($"- **Authority**: {plan.Authority}"); sb.AppendLine(); // Changes sb.AppendLine("### Changes"); sb.AppendLine(); foreach (var step in plan.Steps) { sb.AppendLine($"- {step.Description}"); if (!string.IsNullOrEmpty(step.PreviousValue) && !string.IsNullOrEmpty(step.NewValue)) { sb.AppendLine($" - `{step.PreviousValue}` → `{step.NewValue}`"); } } sb.AppendLine(); // Delta verdict sb.AppendLine("### Delta Verdict"); sb.AppendLine(); var summary = signedDelta.Delta.Summary; var trendEmoji = summary.RiskTrend switch { "improved" => "✅", "degraded" => "⚠️", _ => "➖" }; sb.AppendLine($"{trendEmoji} **{summary.RiskTrend.ToUpperInvariant()}**"); sb.AppendLine(); sb.AppendLine($"| Metric | Count |"); sb.AppendLine($"|--------|-------|"); sb.AppendLine($"| Vulnerabilities Fixed | {summary.VulnerabilitiesFixed} |"); sb.AppendLine($"| Vulnerabilities Introduced | {summary.VulnerabilitiesIntroduced} |"); sb.AppendLine($"| Net Change | {summary.NetVulnerabilityChange} |"); sb.AppendLine($"| Components Upgraded | {summary.ComponentsUpgraded} |"); sb.AppendLine(); // Signature verification sb.AppendLine("### Attestation"); sb.AppendLine(); sb.AppendLine("```"); sb.AppendLine($"Delta ID: {signedDelta.Delta.DeltaId}"); sb.AppendLine($"Signature ID: {signedDelta.SignatureId}"); sb.AppendLine($"Algorithm: {signedDelta.Algorithm}"); sb.AppendLine($"Signed At: {signedDelta.SignedAt}"); sb.AppendLine("```"); sb.AppendLine(); // Footer sb.AppendLine("---"); sb.AppendLine($"*Generated by StellaOps Remedy Autopilot using {plan.ModelId}*"); return Task.FromResult(sb.ToString()); } private static string ExtractVersion(string purl) { // Extract version from PURL like pkg:npm/lodash@4.17.21 var atIndex = purl.LastIndexOf('@'); return atIndex >= 0 ? purl[(atIndex + 1)..] : "unknown"; } private static async Task ComputeFileDigestAsync( string filePath, CancellationToken cancellationToken) { if (!File.Exists(filePath)) { return "file-not-found"; } await using var stream = File.OpenRead(filePath); var hash = await System.Security.Cryptography.SHA256.HashDataAsync(stream, cancellationToken); return Convert.ToHexStringLower(hash); } }