new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -285,6 +285,9 @@ public static class EvidencePackEndpoints
"html" => EvidencePackExportFormat.Html,
"pdf" => EvidencePackExportFormat.Pdf,
"signedjson" => EvidencePackExportFormat.SignedJson,
// Sprint: SPRINT_20260112_005_BE_evidence_card_api (EVPCARD-BE-001)
"evidencecard" or "evidence-card" or "card" => EvidencePackExportFormat.EvidenceCard,
"evidencecardcompact" or "card-compact" => EvidencePackExportFormat.EvidenceCardCompact,
_ => EvidencePackExportFormat.Json
};

View File

@@ -0,0 +1,325 @@
// <copyright file="PrTemplateBuilder.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_007_BE_remediation_pr_generator (REMEDY-BE-001)
// </copyright>
using System.Globalization;
using System.Text;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Builds deterministic PR.md templates for remediation pull requests.
/// </summary>
public sealed class PrTemplateBuilder
{
/// <summary>
/// Builds a PR description from a remediation plan.
/// </summary>
public string BuildPrBody(RemediationPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var sb = new StringBuilder();
// Header section
sb.AppendLine("## Security Remediation");
sb.AppendLine();
sb.AppendLine($"**Plan ID:** `{plan.PlanId}`");
sb.AppendLine($"**Authority:** {plan.Authority}");
sb.AppendLine($"**Risk Level:** {plan.RiskAssessment}");
sb.AppendLine($"**Confidence:** {plan.ConfidenceScore:P0}");
sb.AppendLine($"**Generated:** {plan.GeneratedAt}");
sb.AppendLine();
// Summary section
AppendSummary(sb, plan);
// Steps section
AppendSteps(sb, plan);
// Expected changes section
AppendExpectedChanges(sb, plan);
// Test requirements section
AppendTestRequirements(sb, plan);
// Rollback section
AppendRollbackSteps(sb, plan);
// VEX claim section
AppendVexClaim(sb, plan);
// Evidence section
AppendEvidence(sb, plan);
// Footer
sb.AppendLine("---");
sb.AppendLine($"*Generated by StellaOps AdvisoryAI ({plan.ModelId})*");
return sb.ToString();
}
/// <summary>
/// Builds a PR title from a remediation plan.
/// </summary>
public string BuildPrTitle(RemediationPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var riskEmoji = plan.RiskAssessment switch
{
RemediationRisk.Low => "[LOW]",
RemediationRisk.Medium => "[MEDIUM]",
RemediationRisk.High => "[HIGH]",
_ => "[UNKNOWN]"
};
return $"{riskEmoji} Security fix: {plan.Request.VulnerabilityId}";
}
/// <summary>
/// Builds a branch name from a remediation plan.
/// </summary>
public string BuildBranchName(RemediationPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var sanitizedPlanId = plan.PlanId
.ToLowerInvariant()
.Replace(" ", "-")
.Replace("_", "-");
return $"stellaops/security-fix/{sanitizedPlanId}";
}
private static void AppendSummary(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### Summary");
sb.AppendLine();
sb.AppendLine($"This PR remediates vulnerability **{plan.Request.VulnerabilityId}** in component **{plan.Request.ComponentPurl}**.");
sb.AppendLine();
sb.AppendLine("**Vulnerability addressed:**");
sb.AppendLine($"- `{plan.Request.VulnerabilityId}`");
sb.AppendLine();
}
private static void AppendSteps(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### Remediation Steps");
sb.AppendLine();
foreach (var step in plan.Steps.OrderBy(s => s.Order))
{
var optionalTag = step.Optional ? " *(optional)*" : "";
var riskTag = step.Risk != RemediationRisk.Low ? $" [{step.Risk}]" : "";
sb.AppendLine($"{step.Order}. **{step.ActionType}**{riskTag}{optionalTag}");
sb.AppendLine($" - File: `{step.FilePath}`");
sb.AppendLine($" - {step.Description}");
if (!string.IsNullOrEmpty(step.PreviousValue) && !string.IsNullOrEmpty(step.NewValue))
{
sb.AppendLine($" - Change: `{step.PreviousValue}` -> `{step.NewValue}`");
}
sb.AppendLine();
}
}
private static void AppendExpectedChanges(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### Expected SBOM Changes");
sb.AppendLine();
var delta = plan.ExpectedDelta;
if (delta.Upgraded.Count > 0)
{
sb.AppendLine("**Upgrades:**");
foreach (var (oldPurl, newPurl) in delta.Upgraded.OrderBy(kvp => kvp.Key, StringComparer.Ordinal))
{
sb.AppendLine($"- `{oldPurl}` -> `{newPurl}`");
}
sb.AppendLine();
}
if (delta.Added.Count > 0)
{
sb.AppendLine("**Added:**");
foreach (var purl in delta.Added.OrderBy(p => p, StringComparer.Ordinal))
{
sb.AppendLine($"- `{purl}`");
}
sb.AppendLine();
}
if (delta.Removed.Count > 0)
{
sb.AppendLine("**Removed:**");
foreach (var purl in delta.Removed.OrderBy(p => p, StringComparer.Ordinal))
{
sb.AppendLine($"- `{purl}`");
}
sb.AppendLine();
}
var changeSign = delta.NetVulnerabilityChange <= 0 ? "" : "+";
sb.AppendLine($"**Net vulnerability change:** {changeSign}{delta.NetVulnerabilityChange}");
sb.AppendLine();
}
private static void AppendTestRequirements(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### Test Requirements");
sb.AppendLine();
var tests = plan.TestRequirements;
if (tests.TestSuites.Count > 0)
{
sb.AppendLine("**Required test suites:**");
foreach (var suite in tests.TestSuites.OrderBy(s => s, StringComparer.Ordinal))
{
sb.AppendLine($"- `{suite}`");
}
sb.AppendLine();
}
sb.AppendLine($"- Minimum coverage: {tests.MinCoverage:P0}");
sb.AppendLine($"- Require all pass: {(tests.RequireAllPass ? "Yes" : "No")}");
sb.AppendLine($"- Timeout: {tests.Timeout.TotalMinutes:F0} minutes");
sb.AppendLine();
}
private static void AppendRollbackSteps(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### Rollback Steps");
sb.AppendLine();
sb.AppendLine("If this remediation causes issues, rollback using:");
sb.AppendLine();
sb.AppendLine("```bash");
sb.AppendLine("# Revert this PR");
sb.AppendLine($"git revert <commit-sha>");
sb.AppendLine();
sb.AppendLine("# Or restore previous versions:");
foreach (var step in plan.Steps.Where(s => !string.IsNullOrEmpty(s.PreviousValue)).OrderBy(s => s.Order))
{
sb.AppendLine($"# {step.FilePath}: restore '{step.PreviousValue}'");
}
sb.AppendLine("```");
sb.AppendLine();
}
private static void AppendVexClaim(StringBuilder sb, RemediationPlan plan)
{
sb.AppendLine("### VEX Claim");
sb.AppendLine();
sb.AppendLine("Upon merge, the following VEX statements will be generated:");
sb.AppendLine();
sb.AppendLine($"- `{plan.Request.VulnerabilityId}`: status=`fixed`, justification=`vulnerable_code_not_present`");
sb.AppendLine();
sb.AppendLine("These VEX statements will be signed and attached to the evidence pack.");
sb.AppendLine();
}
private static void AppendEvidence(StringBuilder sb, RemediationPlan plan)
{
if (plan.EvidenceRefs.Count == 0)
{
return;
}
sb.AppendLine("### Evidence");
sb.AppendLine();
sb.AppendLine("**Evidence references:**");
foreach (var evidenceRef in plan.EvidenceRefs.OrderBy(e => e, StringComparer.Ordinal))
{
sb.AppendLine($"- `{evidenceRef}`");
}
sb.AppendLine();
if (plan.InputHashes.Count > 0)
{
sb.AppendLine("**Input hashes (for replay):**");
sb.AppendLine("```");
foreach (var hash in plan.InputHashes.OrderBy(h => h, StringComparer.Ordinal))
{
sb.AppendLine(hash);
}
sb.AppendLine("```");
sb.AppendLine();
}
}
}
/// <summary>
/// Rollback step for a remediation.
/// </summary>
public sealed record RollbackStep
{
/// <summary>
/// Step order.
/// </summary>
public required int Order { get; init; }
/// <summary>
/// File to restore.
/// </summary>
public required string FilePath { get; init; }
/// <summary>
/// Command or action to execute.
/// </summary>
public required string Command { get; init; }
/// <summary>
/// Description.
/// </summary>
public required string Description { get; init; }
}
/// <summary>
/// Generated PR metadata.
/// </summary>
public sealed record PrMetadata
{
/// <summary>
/// PR title.
/// </summary>
public required string Title { get; init; }
/// <summary>
/// Branch name.
/// </summary>
public required string BranchName { get; init; }
/// <summary>
/// PR body (Markdown).
/// </summary>
public required string Body { get; init; }
/// <summary>
/// Labels to apply.
/// </summary>
public required IReadOnlyList<string> Labels { get; init; }
/// <summary>
/// Reviewers to request.
/// </summary>
public required IReadOnlyList<string> Reviewers { get; init; }
/// <summary>
/// Whether auto-merge should be enabled.
/// </summary>
public bool EnableAutoMerge { get; init; }
/// <summary>
/// Draft status.
/// </summary>
public bool IsDraft { get; init; }
}

View File

@@ -7,6 +7,10 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Bench.AdvisoryAI" />
<InternalsVisibleTo Include="StellaOps.AdvisoryAI.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />