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:
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user