Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user