Files
git.stella-ops.org/src/AdvisoryAI/StellaOps.AdvisoryAI/Remediation/RemediationDeltaService.cs

325 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Service for computing and signing SBOM deltas during remediation.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-15, REMEDY-16, REMEDY-17
/// </summary>
public interface IRemediationDeltaService
{
/// <summary>
/// Compute SBOM delta between before and after remediation.
/// </summary>
Task<RemediationDelta> ComputeDeltaAsync(
RemediationPlan plan,
string beforeSbomPath,
string afterSbomPath,
CancellationToken cancellationToken = default);
/// <summary>
/// Sign the delta verdict with attestation.
/// </summary>
Task<SignedDeltaVerdict> SignDeltaAsync(
RemediationDelta delta,
IRemediationDeltaSigner signer,
CancellationToken cancellationToken = default);
/// <summary>
/// Generate PR description with delta verdict.
/// </summary>
Task<string> GeneratePrDescriptionAsync(
RemediationPlan plan,
SignedDeltaVerdict signedDelta,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Signer interface for delta verdicts.
/// </summary>
public interface IRemediationDeltaSigner
{
string KeyId { get; }
string Algorithm { get; }
Task<byte[]> SignAsync(byte[] data, CancellationToken cancellationToken = default);
}
/// <summary>
/// Delta result from remediation.
/// </summary>
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<ComponentChange> ComponentChanges { get; init; }
public required IReadOnlyList<VulnerabilityChange> VulnerabilityChanges { get; init; }
public required DeltaSummary Summary { get; init; }
public required string ComputedAt { get; init; }
}
/// <summary>
/// A component change in the delta.
/// </summary>
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<string> AffectedVulnerabilities { get; init; }
}
/// <summary>
/// A vulnerability change in the delta.
/// </summary>
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>
/// Summary of the delta.
/// </summary>
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
}
/// <summary>
/// Signed delta verdict.
/// </summary>
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; }
}
/// <summary>
/// Default implementation of remediation delta service.
/// </summary>
public sealed class RemediationDeltaService : IRemediationDeltaService
{
public async Task<RemediationDelta> 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<ComponentChange>();
var vulnChanges = new List<VulnerabilityChange>();
// 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<string>()
});
}
foreach (var purl in plan.ExpectedDelta.Removed)
{
componentChanges.Add(new ComponentChange
{
ChangeType = "removed",
Purl = purl,
AffectedVulnerabilities = Array.Empty<string>()
});
}
// 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<SignedDeltaVerdict> 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<string> 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<string> 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);
}
}