Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism

- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency.
- Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling.
- Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies.
- Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification.
- Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -0,0 +1,360 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// AI-powered remediation planner implementation.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-03
/// </summary>
public sealed class AiRemediationPlanner : IRemediationPlanner
{
private readonly IPackageVersionResolver _versionResolver;
private readonly IRemediationPromptService _promptService;
private readonly IRemediationInferenceClient _inferenceClient;
private readonly IRemediationPlanStore _planStore;
public AiRemediationPlanner(
IPackageVersionResolver versionResolver,
IRemediationPromptService promptService,
IRemediationInferenceClient inferenceClient,
IRemediationPlanStore planStore)
{
_versionResolver = versionResolver;
_promptService = promptService;
_inferenceClient = inferenceClient;
_planStore = planStore;
}
public async Task<RemediationPlan> GeneratePlanAsync(
RemediationPlanRequest request,
CancellationToken cancellationToken = default)
{
// 1. Resolve package upgrade path
var versionResult = await _versionResolver.ResolveUpgradePathAsync(
request.ComponentPurl,
request.VulnerabilityId,
cancellationToken);
// 2. Determine remediation type if auto
var remediationType = request.RemediationType == RemediationType.Auto
? DetermineRemediationType(versionResult)
: request.RemediationType;
// 3. Build prompt with context
var prompt = await _promptService.BuildPromptAsync(
request,
versionResult,
remediationType,
cancellationToken);
// 4. Generate plan via LLM
var inferenceResult = await _inferenceClient.GeneratePlanAsync(prompt, cancellationToken);
// 5. Parse and validate steps
var steps = ParseSteps(inferenceResult.Content);
var riskAssessment = AssessRisk(steps, versionResult);
// 6. Determine authority and PR-readiness
var authority = DetermineAuthority(riskAssessment, versionResult);
var (prReady, notReadyReason) = DeterminePrReadiness(authority, steps, versionResult);
// 7. Build expected delta
var expectedDelta = BuildExpectedDelta(request, versionResult);
// 8. Build test requirements
var testRequirements = BuildTestRequirements(riskAssessment);
// 9. Compute input hashes
var inputHashes = ComputeInputHashes(request, versionResult, prompt);
// 10. Create plan
var planId = GeneratePlanId(inputHashes, inferenceResult.Content);
var plan = new RemediationPlan
{
PlanId = planId,
Request = request,
Steps = steps,
ExpectedDelta = expectedDelta,
RiskAssessment = riskAssessment,
TestRequirements = testRequirements,
Authority = authority,
PrReady = prReady,
NotReadyReason = notReadyReason,
ConfidenceScore = inferenceResult.Confidence,
ModelId = inferenceResult.ModelId,
GeneratedAt = DateTime.UtcNow.ToString("O"),
InputHashes = inputHashes,
EvidenceRefs = new List<string> { versionResult.CurrentVersion, versionResult.RecommendedVersion }
};
// 11. Store plan
await _planStore.StoreAsync(plan, cancellationToken);
return plan;
}
public async Task<bool> ValidatePlanAsync(string planId, CancellationToken cancellationToken = default)
{
var plan = await _planStore.GetAsync(planId, cancellationToken);
if (plan is null)
{
return false;
}
// Validate that upgrade path is still valid
var currentResult = await _versionResolver.ResolveUpgradePathAsync(
plan.Request.ComponentPurl,
plan.Request.VulnerabilityId,
cancellationToken);
return currentResult.RecommendedVersion == plan.EvidenceRefs[1];
}
public async Task<RemediationPlan?> GetPlanAsync(string planId, CancellationToken cancellationToken = default)
{
return await _planStore.GetAsync(planId, cancellationToken);
}
private static RemediationType DetermineRemediationType(VersionResolutionResult versionResult)
{
return versionResult.UpgradeType switch
{
"patch" => RemediationType.Bump,
"minor" => RemediationType.Bump,
"major" => RemediationType.Upgrade,
_ => RemediationType.Bump
};
}
private static IReadOnlyList<RemediationStep> ParseSteps(string content)
{
var steps = new List<RemediationStep>();
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var order = 1;
foreach (var line in lines)
{
if (line.TrimStart().StartsWith("- ") || line.TrimStart().StartsWith("* "))
{
var step = new RemediationStep
{
Order = order++,
ActionType = "update_package",
FilePath = "package.json", // Default, would be parsed from content
Description = line.TrimStart()[2..].Trim(),
Risk = RemediationRisk.Low
};
steps.Add(step);
}
}
if (steps.Count == 0)
{
// Fallback: create a single step from content
steps.Add(new RemediationStep
{
Order = 1,
ActionType = "update_package",
FilePath = "dependency_file",
Description = content.Length > 200 ? content[..200] : content,
Risk = RemediationRisk.Medium
});
}
return steps;
}
private static RemediationRisk AssessRisk(
IReadOnlyList<RemediationStep> steps,
VersionResolutionResult versionResult)
{
if (versionResult.BreakingChanges.Count > 0)
{
return RemediationRisk.High;
}
if (versionResult.UpgradeType == "major")
{
return RemediationRisk.High;
}
if (versionResult.UpgradeType == "minor")
{
return RemediationRisk.Medium;
}
return steps.Any(s => s.Risk == RemediationRisk.High)
? RemediationRisk.High
: steps.Any(s => s.Risk == RemediationRisk.Medium)
? RemediationRisk.Medium
: RemediationRisk.Low;
}
private static RemediationAuthority DetermineAuthority(
RemediationRisk risk,
VersionResolutionResult versionResult)
{
if (!versionResult.IsSafe)
{
return RemediationAuthority.Suggestion;
}
return risk switch
{
RemediationRisk.Low => RemediationAuthority.Draft,
RemediationRisk.Medium => RemediationAuthority.Draft,
RemediationRisk.High => RemediationAuthority.Suggestion,
_ => RemediationAuthority.Suggestion
};
}
private static (bool prReady, string? reason) DeterminePrReadiness(
RemediationAuthority authority,
IReadOnlyList<RemediationStep> steps,
VersionResolutionResult versionResult)
{
if (authority == RemediationAuthority.Suggestion)
{
return (false, "Remediation requires human review due to potential breaking changes");
}
if (!versionResult.IsSafe)
{
return (false, $"Upgrade path may introduce issues: {string.Join(", ", versionResult.BreakingChanges)}");
}
if (versionResult.NewVulnerabilities.Count > 0)
{
return (false, $"Upgrade introduces new vulnerabilities: {string.Join(", ", versionResult.NewVulnerabilities)}");
}
if (steps.Count == 0)
{
return (false, "No remediation steps could be determined");
}
return (true, null);
}
private static ExpectedSbomDelta BuildExpectedDelta(
RemediationPlanRequest request,
VersionResolutionResult versionResult)
{
return new ExpectedSbomDelta
{
Added = Array.Empty<string>(),
Removed = new List<string> { request.ComponentPurl },
Upgraded = new Dictionary<string, string>
{
{ request.ComponentPurl, $"{request.ComponentPurl.Split('@')[0]}@{versionResult.RecommendedVersion}" }
},
NetVulnerabilityChange = -versionResult.VulnerabilitiesFixed.Count + versionResult.NewVulnerabilities.Count
};
}
private static RemediationTestRequirements BuildTestRequirements(RemediationRisk risk)
{
return risk switch
{
RemediationRisk.Low => new RemediationTestRequirements
{
TestSuites = new List<string> { "unit" },
MinCoverage = 0,
RequireAllPass = true,
Timeout = TimeSpan.FromMinutes(10)
},
RemediationRisk.Medium => new RemediationTestRequirements
{
TestSuites = new List<string> { "unit", "integration" },
MinCoverage = 0.5,
RequireAllPass = true,
Timeout = TimeSpan.FromMinutes(30)
},
_ => new RemediationTestRequirements
{
TestSuites = new List<string> { "unit", "integration", "e2e" },
MinCoverage = 0.8,
RequireAllPass = true,
Timeout = TimeSpan.FromMinutes(60)
}
};
}
private static IReadOnlyList<string> ComputeInputHashes(
RemediationPlanRequest request,
VersionResolutionResult versionResult,
RemediationPrompt prompt)
{
return new List<string>
{
ComputeHash(JsonSerializer.Serialize(request)),
ComputeHash(JsonSerializer.Serialize(versionResult)),
ComputeHash(prompt.Content)
};
}
private static string GeneratePlanId(IReadOnlyList<string> inputHashes, string output)
{
var combined = string.Join("|", inputHashes) + "|" + output;
return $"plan:{ComputeHash(combined)[..16]}";
}
private static string ComputeHash(string content)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return Convert.ToHexStringLower(bytes);
}
}
/// <summary>
/// Prompt for remediation planning.
/// </summary>
public sealed record RemediationPrompt
{
public required string Content { get; init; }
public required string TemplateVersion { get; init; }
}
/// <summary>
/// Inference result from LLM for remediation.
/// </summary>
public sealed record RemediationInferenceResult
{
public required string Content { get; init; }
public required double Confidence { get; init; }
public required string ModelId { get; init; }
}
/// <summary>
/// Service for building remediation prompts.
/// </summary>
public interface IRemediationPromptService
{
Task<RemediationPrompt> BuildPromptAsync(
RemediationPlanRequest request,
VersionResolutionResult versionResult,
RemediationType type,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Client for LLM inference for remediation.
/// </summary>
public interface IRemediationInferenceClient
{
Task<RemediationInferenceResult> GeneratePlanAsync(
RemediationPrompt prompt,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store for remediation plans.
/// </summary>
public interface IRemediationPlanStore
{
Task StoreAsync(RemediationPlan plan, CancellationToken cancellationToken = default);
Task<RemediationPlan?> GetAsync(string planId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,126 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Azure DevOps implementation of pull request generator.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-11
/// </summary>
public sealed class AzureDevOpsPullRequestGenerator : IPullRequestGenerator
{
public string ScmType => "azure-devops";
public Task<PullRequestResult> CreatePullRequestAsync(
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
if (!plan.PrReady)
{
return Task.FromResult(new PullRequestResult
{
PrId = $"ado-pr-{Guid.NewGuid():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not PR-ready",
CreatedAt = DateTime.UtcNow.ToString("O"),
UpdatedAt = DateTime.UtcNow.ToString("O")
});
}
var branchName = GenerateBranchName(plan);
var prId = $"ado-pr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
// In a real implementation, this would use Azure DevOps REST API
return Task.FromResult(new PullRequestResult
{
PrId = prId,
PrNumber = new Random().Next(1000, 9999),
Url = $"https://dev.azure.com/{ExtractOrgProject(plan.Request.RepositoryUrl)}/_git/{ExtractRepoName(plan.Request.RepositoryUrl)}/pullrequest/{prId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Pull request is being created",
CreatedAt = now,
UpdatedAt = now
});
}
public Task<PullRequestResult> GetStatusAsync(
string prId,
CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow.ToString("O");
return Task.FromResult(new PullRequestResult
{
PrId = prId,
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Open,
StatusMessage = "Waiting for build",
CreatedAt = now,
UpdatedAt = now
});
}
public Task UpdateWithDeltaVerdictAsync(
string prId,
DeltaVerdictResult deltaVerdict,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task ClosePullRequestAsync(
string prId,
string reason,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
return $"stellaops/fix-{vulnId}-{timestamp}";
}
private static string ExtractOrgProject(string? repositoryUrl)
{
if (string.IsNullOrEmpty(repositoryUrl))
{
return "org/project";
}
// Azure DevOps URL format: https://dev.azure.com/{org}/{project}/_git/{repo}
var uri = new Uri(repositoryUrl);
var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length >= 2)
{
return $"{segments[0]}/{segments[1]}";
}
return "org/project";
}
private static string ExtractRepoName(string? repositoryUrl)
{
if (string.IsNullOrEmpty(repositoryUrl))
{
return "repo";
}
var uri = new Uri(repositoryUrl);
var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
// Find _git segment and return the next one
for (int i = 0; i < segments.Length - 1; i++)
{
if (segments[i] == "_git")
{
return segments[i + 1];
}
}
return segments[^1];
}
}

View File

@@ -0,0 +1,125 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// GitHub implementation of pull request generator.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-09
/// </summary>
public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
{
private readonly IRemediationPlanStore _planStore;
public GitHubPullRequestGenerator(IRemediationPlanStore planStore)
{
_planStore = planStore;
}
public string ScmType => "github";
public async Task<PullRequestResult> CreatePullRequestAsync(
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
// Validate plan is PR-ready
if (!plan.PrReady)
{
return new PullRequestResult
{
PrId = $"pr-{Guid.NewGuid():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not PR-ready",
CreatedAt = DateTime.UtcNow.ToString("O"),
UpdatedAt = DateTime.UtcNow.ToString("O")
};
}
// Generate branch name
var branchName = GenerateBranchName(plan);
// In a real implementation, this would:
// 1. Create a new branch
// 2. Apply remediation steps (update files)
// 3. Commit changes
// 4. Create PR via GitHub API
var prId = $"gh-pr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
return new PullRequestResult
{
PrId = prId,
PrNumber = new Random().Next(1000, 9999), // Placeholder
Url = $"https://github.com/{ExtractOwnerRepo(plan.Request.RepositoryUrl)}/pull/{prId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Pull request is being created",
CreatedAt = now,
UpdatedAt = now
};
}
public Task<PullRequestResult> GetStatusAsync(
string prId,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would query GitHub API
var now = DateTime.UtcNow.ToString("O");
return Task.FromResult(new PullRequestResult
{
PrId = prId,
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Open,
StatusMessage = "Waiting for CI",
CreatedAt = now,
UpdatedAt = now
});
}
public Task UpdateWithDeltaVerdictAsync(
string prId,
DeltaVerdictResult deltaVerdict,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would update PR description via GitHub API
return Task.CompletedTask;
}
public Task ClosePullRequestAsync(
string prId,
string reason,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would close PR via GitHub API
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
return $"stellaops/fix-{vulnId}-{timestamp}";
}
private static string ExtractOwnerRepo(string? repositoryUrl)
{
if (string.IsNullOrEmpty(repositoryUrl))
{
return "owner/repo";
}
// Extract owner/repo from GitHub URL
var uri = new Uri(repositoryUrl);
var path = uri.AbsolutePath.Trim('/');
if (path.EndsWith(".git"))
{
path = path[..^4];
}
return path;
}
}

View File

@@ -0,0 +1,105 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// GitLab implementation of pull request generator.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-10
/// </summary>
public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
{
public string ScmType => "gitlab";
public Task<PullRequestResult> CreatePullRequestAsync(
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
if (!plan.PrReady)
{
return Task.FromResult(new PullRequestResult
{
PrId = $"mr-{Guid.NewGuid():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not MR-ready",
CreatedAt = DateTime.UtcNow.ToString("O"),
UpdatedAt = DateTime.UtcNow.ToString("O")
});
}
var branchName = GenerateBranchName(plan);
var mrId = $"gl-mr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
// In a real implementation, this would use GitLab API
return Task.FromResult(new PullRequestResult
{
PrId = mrId,
PrNumber = new Random().Next(1000, 9999),
Url = $"https://gitlab.com/{ExtractProjectPath(plan.Request.RepositoryUrl)}/-/merge_requests/{mrId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Merge request is being created",
CreatedAt = now,
UpdatedAt = now
});
}
public Task<PullRequestResult> GetStatusAsync(
string prId,
CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow.ToString("O");
return Task.FromResult(new PullRequestResult
{
PrId = prId,
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Open,
StatusMessage = "Waiting for pipeline",
CreatedAt = now,
UpdatedAt = now
});
}
public Task UpdateWithDeltaVerdictAsync(
string prId,
DeltaVerdictResult deltaVerdict,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task ClosePullRequestAsync(
string prId,
string reason,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
return $"stellaops/fix-{vulnId}-{timestamp}";
}
private static string ExtractProjectPath(string? repositoryUrl)
{
if (string.IsNullOrEmpty(repositoryUrl))
{
return "group/project";
}
var uri = new Uri(repositoryUrl);
var path = uri.AbsolutePath.Trim('/');
if (path.EndsWith(".git"))
{
path = path[..^4];
}
return path;
}
}

View File

@@ -0,0 +1,88 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Version resolution result.
/// </summary>
public sealed record VersionResolutionResult
{
/// <summary>
/// Current version.
/// </summary>
public required string CurrentVersion { get; init; }
/// <summary>
/// Recommended upgrade version.
/// </summary>
public required string RecommendedVersion { get; init; }
/// <summary>
/// Latest available version.
/// </summary>
public required string LatestVersion { get; init; }
/// <summary>
/// Whether upgrade path is safe.
/// </summary>
public required bool IsSafe { get; init; }
/// <summary>
/// Breaking changes detected.
/// </summary>
public required IReadOnlyList<string> BreakingChanges { get; init; }
/// <summary>
/// Vulnerabilities fixed by upgrade.
/// </summary>
public required IReadOnlyList<string> VulnerabilitiesFixed { get; init; }
/// <summary>
/// New vulnerabilities introduced (rare but possible).
/// </summary>
public required IReadOnlyList<string> NewVulnerabilities { get; init; }
/// <summary>
/// Upgrade type (patch, minor, major).
/// </summary>
public required string UpgradeType { get; init; }
/// <summary>
/// Confidence in the resolution (0.0-1.0).
/// </summary>
public required double Confidence { get; init; }
}
/// <summary>
/// Service for resolving package versions and validating upgrade paths.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-04
/// </summary>
public interface IPackageVersionResolver
{
/// <summary>
/// Resolve upgrade path for a package.
/// </summary>
/// <param name="purl">Package URL.</param>
/// <param name="targetVulnerability">Vulnerability to fix.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Version resolution result.</returns>
Task<VersionResolutionResult> ResolveUpgradePathAsync(
string purl,
string targetVulnerability,
CancellationToken cancellationToken = default);
/// <summary>
/// Check if a specific version is available.
/// </summary>
/// <param name="purl">Package URL with version.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if version exists.</returns>
Task<bool> IsVersionAvailableAsync(string purl, CancellationToken cancellationToken = default);
/// <summary>
/// Get all available versions for a package.
/// </summary>
/// <param name="purl">Package URL (without version).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of available versions.</returns>
Task<IReadOnlyList<string>> GetAvailableVersionsAsync(string purl, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,218 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Status of a pull request.
/// </summary>
public enum PullRequestStatus
{
/// <summary>
/// PR is being created.
/// </summary>
Creating,
/// <summary>
/// PR is open and waiting for review.
/// </summary>
Open,
/// <summary>
/// PR build is in progress.
/// </summary>
Building,
/// <summary>
/// PR build passed.
/// </summary>
BuildPassed,
/// <summary>
/// PR build failed.
/// </summary>
BuildFailed,
/// <summary>
/// PR tests are running.
/// </summary>
Testing,
/// <summary>
/// PR tests passed.
/// </summary>
TestsPassed,
/// <summary>
/// PR tests failed.
/// </summary>
TestsFailed,
/// <summary>
/// PR is merged.
/// </summary>
Merged,
/// <summary>
/// PR is closed without merge.
/// </summary>
Closed,
/// <summary>
/// PR creation failed.
/// </summary>
Failed
}
/// <summary>
/// Result of creating a pull request.
/// </summary>
public sealed record PullRequestResult
{
/// <summary>
/// Unique PR identifier.
/// </summary>
public required string PrId { get; init; }
/// <summary>
/// PR number in the SCM.
/// </summary>
public required int PrNumber { get; init; }
/// <summary>
/// URL to view the PR.
/// </summary>
public required string Url { get; init; }
/// <summary>
/// Branch name for the PR.
/// </summary>
public required string BranchName { get; init; }
/// <summary>
/// Current status.
/// </summary>
public required PullRequestStatus Status { get; init; }
/// <summary>
/// Status message.
/// </summary>
public string? StatusMessage { get; init; }
/// <summary>
/// Build result if available.
/// </summary>
public BuildResult? BuildResult { get; init; }
/// <summary>
/// Test result if available.
/// </summary>
public TestResult? TestResult { get; init; }
/// <summary>
/// Delta verdict if available.
/// </summary>
public DeltaVerdictResult? DeltaVerdict { get; init; }
/// <summary>
/// Created timestamp.
/// </summary>
public required string CreatedAt { get; init; }
/// <summary>
/// Last updated timestamp.
/// </summary>
public required string UpdatedAt { get; init; }
}
/// <summary>
/// Build result from CI pipeline.
/// </summary>
public sealed record BuildResult
{
public required bool Success { get; init; }
public required string BuildId { get; init; }
public string? BuildUrl { get; init; }
public string? ErrorMessage { get; init; }
public required string CompletedAt { get; init; }
}
/// <summary>
/// Test result from test suite.
/// </summary>
public sealed record TestResult
{
public required bool AllPassed { get; init; }
public required int TotalTests { get; init; }
public required int PassedTests { get; init; }
public required int FailedTests { get; init; }
public required int SkippedTests { get; init; }
public double Coverage { get; init; }
public IReadOnlyList<string> FailedTestNames { get; init; } = Array.Empty<string>();
public required string CompletedAt { get; init; }
}
/// <summary>
/// Delta verdict result.
/// </summary>
public sealed record DeltaVerdictResult
{
public required bool Improved { get; init; }
public required int VulnerabilitiesFixed { get; init; }
public required int VulnerabilitiesIntroduced { get; init; }
public required string VerdictId { get; init; }
public string? SignatureId { get; init; }
public required string ComputedAt { get; init; }
}
/// <summary>
/// Service for generating pull requests from remediation plans.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-08
/// </summary>
public interface IPullRequestGenerator
{
/// <summary>
/// SCM type supported by this generator.
/// </summary>
string ScmType { get; }
/// <summary>
/// Create a pull request for a remediation plan.
/// </summary>
/// <param name="plan">Remediation plan to apply.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Pull request result.</returns>
Task<PullRequestResult> CreatePullRequestAsync(
RemediationPlan plan,
CancellationToken cancellationToken = default);
/// <summary>
/// Get the status of a pull request.
/// </summary>
/// <param name="prId">PR identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Current PR status.</returns>
Task<PullRequestResult> GetStatusAsync(
string prId,
CancellationToken cancellationToken = default);
/// <summary>
/// Update PR description with delta verdict.
/// </summary>
/// <param name="prId">PR identifier.</param>
/// <param name="deltaVerdict">Delta verdict to include.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateWithDeltaVerdictAsync(
string prId,
DeltaVerdictResult deltaVerdict,
CancellationToken cancellationToken = default);
/// <summary>
/// Close a pull request.
/// </summary>
/// <param name="prId">PR identifier.</param>
/// <param name="reason">Reason for closing.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task ClosePullRequestAsync(
string prId,
string reason,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,33 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Service for generating AI-powered remediation plans.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-02
/// </summary>
public interface IRemediationPlanner
{
/// <summary>
/// Generate a remediation plan for a finding.
/// </summary>
/// <param name="request">Remediation request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Remediation plan with steps and risk assessment.</returns>
Task<RemediationPlan> GeneratePlanAsync(RemediationPlanRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Validate a remediation plan against current state.
/// </summary>
/// <param name="planId">Plan ID to validate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if plan is still valid.</returns>
Task<bool> ValidatePlanAsync(string planId, CancellationToken cancellationToken = default);
/// <summary>
/// Get a stored remediation plan.
/// </summary>
/// <param name="planId">Plan ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The plan, or null if not found.</returns>
Task<RemediationPlan?> GetPlanAsync(string planId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,224 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Authority level of the remediation plan.
/// </summary>
public enum RemediationAuthority
{
/// <summary>
/// Verified: build passed, tests passed, delta verified.
/// </summary>
Verified,
/// <summary>
/// Suggestion: requires human review (build/tests failed or not run).
/// </summary>
Suggestion,
/// <summary>
/// Draft: initial plan not yet verified.
/// </summary>
Draft
}
/// <summary>
/// Risk level of the remediation.
/// </summary>
public enum RemediationRisk
{
/// <summary>
/// Low risk: patch version bump.
/// </summary>
Low,
/// <summary>
/// Medium risk: minor version bump.
/// </summary>
Medium,
/// <summary>
/// High risk: major version bump or breaking changes.
/// </summary>
High,
/// <summary>
/// Unknown risk: unable to determine.
/// </summary>
Unknown
}
/// <summary>
/// A single step in a remediation plan.
/// </summary>
public sealed record RemediationStep
{
/// <summary>
/// Step number (1-based).
/// </summary>
public required int Order { get; init; }
/// <summary>
/// Type of action (update_package, update_lockfile, update_config, run_command, etc.).
/// </summary>
public required string ActionType { get; init; }
/// <summary>
/// File path affected.
/// </summary>
public required string FilePath { get; init; }
/// <summary>
/// Description of the change.
/// </summary>
public required string Description { get; init; }
/// <summary>
/// Previous value (for diff).
/// </summary>
public string? PreviousValue { get; init; }
/// <summary>
/// New value (for diff).
/// </summary>
public string? NewValue { get; init; }
/// <summary>
/// Whether this step is optional.
/// </summary>
public bool Optional { get; init; }
/// <summary>
/// Risk assessment for this step.
/// </summary>
public RemediationRisk Risk { get; init; } = RemediationRisk.Low;
}
/// <summary>
/// Expected SBOM delta after remediation.
/// </summary>
public sealed record ExpectedSbomDelta
{
/// <summary>
/// Components to be added.
/// </summary>
public required IReadOnlyList<string> Added { get; init; }
/// <summary>
/// Components to be removed.
/// </summary>
public required IReadOnlyList<string> Removed { get; init; }
/// <summary>
/// Components to be upgraded (old_purl → new_purl).
/// </summary>
public required IReadOnlyDictionary<string, string> Upgraded { get; init; }
/// <summary>
/// Net vulnerability change (negative = improvement).
/// </summary>
public required int NetVulnerabilityChange { get; init; }
}
/// <summary>
/// Test requirements for verifying remediation.
/// </summary>
public sealed record RemediationTestRequirements
{
/// <summary>
/// Required test suites to run.
/// </summary>
public required IReadOnlyList<string> TestSuites { get; init; }
/// <summary>
/// Minimum coverage required.
/// </summary>
public double MinCoverage { get; init; }
/// <summary>
/// Whether all tests must pass.
/// </summary>
public bool RequireAllPass { get; init; } = true;
/// <summary>
/// Timeout for test execution.
/// </summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(30);
}
/// <summary>
/// A complete remediation plan.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-05
/// </summary>
public sealed record RemediationPlan
{
/// <summary>
/// Unique plan ID.
/// </summary>
public required string PlanId { get; init; }
/// <summary>
/// Original request.
/// </summary>
public required RemediationPlanRequest Request { get; init; }
/// <summary>
/// Remediation steps to apply.
/// </summary>
public required IReadOnlyList<RemediationStep> Steps { get; init; }
/// <summary>
/// Expected SBOM delta.
/// </summary>
public required ExpectedSbomDelta ExpectedDelta { get; init; }
/// <summary>
/// Overall risk assessment.
/// </summary>
public required RemediationRisk RiskAssessment { get; init; }
/// <summary>
/// Test requirements.
/// </summary>
public required RemediationTestRequirements TestRequirements { get; init; }
/// <summary>
/// Authority classification.
/// </summary>
public required RemediationAuthority Authority { get; init; }
/// <summary>
/// PR-ready flag (true if plan can be applied automatically).
/// </summary>
public required bool PrReady { get; init; }
/// <summary>
/// Reason if not PR-ready.
/// </summary>
public string? NotReadyReason { get; init; }
/// <summary>
/// Confidence score (0.0-1.0).
/// </summary>
public required double ConfidenceScore { get; init; }
/// <summary>
/// Model ID used for generation.
/// </summary>
public required string ModelId { get; init; }
/// <summary>
/// Generated timestamp (UTC ISO-8601).
/// </summary>
public required string GeneratedAt { get; init; }
/// <summary>
/// Input hashes for replay.
/// </summary>
public required IReadOnlyList<string> InputHashes { get; init; }
/// <summary>
/// Evidence refs used in planning.
/// </summary>
public required IReadOnlyList<string> EvidenceRefs { get; init; }
}

View File

@@ -0,0 +1,85 @@
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// Type of remediation to apply.
/// </summary>
public enum RemediationType
{
/// <summary>
/// Bump dependency to patched version.
/// </summary>
Bump,
/// <summary>
/// Upgrade base image to newer version.
/// </summary>
Upgrade,
/// <summary>
/// Apply configuration change to mitigate.
/// </summary>
Config,
/// <summary>
/// Apply backport patch.
/// </summary>
Backport,
/// <summary>
/// Auto-detect best remediation type.
/// </summary>
Auto
}
/// <summary>
/// Request for generating a remediation plan.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-01
/// </summary>
public sealed record RemediationPlanRequest
{
/// <summary>
/// Finding ID to remediate.
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// Artifact digest for context.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// Vulnerability ID (CVE, GHSA, etc.).
/// </summary>
public required string VulnerabilityId { get; init; }
/// <summary>
/// Affected component PURL.
/// </summary>
public required string ComponentPurl { get; init; }
/// <summary>
/// Type of remediation to apply.
/// </summary>
public RemediationType RemediationType { get; init; } = RemediationType.Auto;
/// <summary>
/// Repository URL for PR generation.
/// </summary>
public string? RepositoryUrl { get; init; }
/// <summary>
/// Target branch for PR (default: main).
/// </summary>
public string TargetBranch { get; init; } = "main";
/// <summary>
/// Whether to generate PR immediately.
/// </summary>
public bool AutoCreatePr { get; init; }
/// <summary>
/// Correlation ID for tracing.
/// </summary>
public string? CorrelationId { get; init; }
}