Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
CREATE SCHEMA IF NOT EXISTS remediation;
|
||||
|
||||
CREATE TABLE remediation.fix_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cve_id TEXT NOT NULL,
|
||||
purl TEXT NOT NULL,
|
||||
version_range TEXT NOT NULL,
|
||||
patch_content TEXT NOT NULL,
|
||||
description TEXT,
|
||||
contributor_id UUID,
|
||||
source_id UUID,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
trust_score DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
dsse_digest TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
verified_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE remediation.pr_submissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
fix_template_id UUID REFERENCES remediation.fix_templates(id),
|
||||
pr_url TEXT NOT NULL,
|
||||
repository_url TEXT NOT NULL,
|
||||
source_branch TEXT NOT NULL,
|
||||
target_branch TEXT NOT NULL,
|
||||
cve_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'opened',
|
||||
pre_scan_digest TEXT,
|
||||
post_scan_digest TEXT,
|
||||
reachability_delta_digest TEXT,
|
||||
fix_chain_dsse_digest TEXT,
|
||||
verdict TEXT,
|
||||
contributor_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
merged_at TIMESTAMPTZ,
|
||||
verified_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE remediation.contributors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT,
|
||||
verified_fixes INT NOT NULL DEFAULT 0,
|
||||
total_submissions INT NOT NULL DEFAULT 0,
|
||||
rejected_submissions INT NOT NULL DEFAULT 0,
|
||||
trust_score DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_active_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE remediation.marketplace_sources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT,
|
||||
source_type TEXT NOT NULL DEFAULT 'community',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
trust_score DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_sync_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_fix_templates_cve ON remediation.fix_templates(cve_id);
|
||||
CREATE INDEX idx_fix_templates_purl ON remediation.fix_templates(purl);
|
||||
CREATE INDEX idx_pr_submissions_cve ON remediation.pr_submissions(cve_id);
|
||||
CREATE INDEX idx_pr_submissions_status ON remediation.pr_submissions(status);
|
||||
@@ -0,0 +1,11 @@
|
||||
using StellaOps.Remediation.Core.Models;
|
||||
|
||||
namespace StellaOps.Remediation.Persistence.Repositories;
|
||||
|
||||
public interface IFixTemplateRepository
|
||||
{
|
||||
Task<IReadOnlyList<FixTemplate>> ListAsync(string? cveId = null, string? purl = null, int limit = 50, int offset = 0, CancellationToken ct = default);
|
||||
Task<FixTemplate?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<FixTemplate> InsertAsync(FixTemplate template, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<FixTemplate>> FindMatchesAsync(string cveId, string? purl = null, string? version = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using StellaOps.Remediation.Core.Models;
|
||||
|
||||
namespace StellaOps.Remediation.Persistence.Repositories;
|
||||
|
||||
public interface IPrSubmissionRepository
|
||||
{
|
||||
Task<IReadOnlyList<PrSubmission>> ListAsync(string? cveId = null, string? status = null, int limit = 50, int offset = 0, CancellationToken ct = default);
|
||||
Task<PrSubmission?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<PrSubmission> InsertAsync(PrSubmission submission, CancellationToken ct = default);
|
||||
Task UpdateStatusAsync(Guid id, string status, string? verdict = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using StellaOps.Remediation.Core.Models;
|
||||
|
||||
namespace StellaOps.Remediation.Persistence.Repositories;
|
||||
|
||||
public sealed class PostgresFixTemplateRepository : IFixTemplateRepository
|
||||
{
|
||||
// Stub: real implementation uses Npgsql/Dapper against remediation.fix_templates
|
||||
private readonly List<FixTemplate> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<FixTemplate>> ListAsync(string? cveId = null, string? purl = null, int limit = 50, int offset = 0, CancellationToken ct = default)
|
||||
{
|
||||
var query = _store.AsEnumerable();
|
||||
if (!string.IsNullOrEmpty(cveId))
|
||||
query = query.Where(t => t.CveId.Equals(cveId, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrEmpty(purl))
|
||||
query = query.Where(t => t.Purl.Equals(purl, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
IReadOnlyList<FixTemplate> result = query.Skip(offset).Take(limit).ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<FixTemplate?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var template = _store.FirstOrDefault(t => t.Id == id);
|
||||
return Task.FromResult(template);
|
||||
}
|
||||
|
||||
public Task<FixTemplate> InsertAsync(FixTemplate template, CancellationToken ct = default)
|
||||
{
|
||||
var created = template with { Id = template.Id == Guid.Empty ? Guid.NewGuid() : template.Id, CreatedAt = DateTimeOffset.UtcNow };
|
||||
_store.Add(created);
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<FixTemplate>> FindMatchesAsync(string cveId, string? purl = null, string? version = null, CancellationToken ct = default)
|
||||
{
|
||||
var query = _store.Where(t => t.CveId.Equals(cveId, StringComparison.OrdinalIgnoreCase) && t.Status == "verified");
|
||||
if (!string.IsNullOrEmpty(purl))
|
||||
query = query.Where(t => t.Purl.Equals(purl, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
query = query.Where(t => VersionRangeMatches(t.VersionRange, version));
|
||||
|
||||
IReadOnlyList<FixTemplate> result = query
|
||||
.OrderByDescending(t => t.TrustScore)
|
||||
.ThenByDescending(t => t.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static bool VersionRangeMatches(string? versionRange, string targetVersion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(targetVersion))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(versionRange))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var normalizedRange = versionRange.Trim();
|
||||
var normalizedTarget = targetVersion.Trim();
|
||||
|
||||
// Simple wildcard: 1.2.* matches 1.2.7
|
||||
if (normalizedRange.EndsWith('*'))
|
||||
{
|
||||
var prefix = normalizedRange[..^1];
|
||||
return normalizedTarget.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Exact token match across common delimiters.
|
||||
var tokens = normalizedRange
|
||||
.Split([',', ';', '|', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
if (tokens.Length > 1)
|
||||
{
|
||||
return tokens.Any(token => string.Equals(token, normalizedTarget, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Fallback: substring match supports lightweight expressions like ">=1.2.0 <2.0.0".
|
||||
return normalizedRange.Contains(normalizedTarget, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using StellaOps.Remediation.Core.Models;
|
||||
|
||||
namespace StellaOps.Remediation.Persistence.Repositories;
|
||||
|
||||
public sealed class PostgresPrSubmissionRepository : IPrSubmissionRepository
|
||||
{
|
||||
// Stub: real implementation uses Npgsql/Dapper against remediation.pr_submissions
|
||||
private readonly List<PrSubmission> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<PrSubmission>> ListAsync(string? cveId = null, string? status = null, int limit = 50, int offset = 0, CancellationToken ct = default)
|
||||
{
|
||||
var query = _store.AsEnumerable();
|
||||
if (!string.IsNullOrEmpty(cveId))
|
||||
query = query.Where(s => s.CveId.Equals(cveId, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
query = query.Where(s => s.Status.Equals(status, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
IReadOnlyList<PrSubmission> result = query.Skip(offset).Take(limit).ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<PrSubmission?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var submission = _store.FirstOrDefault(s => s.Id == id);
|
||||
return Task.FromResult(submission);
|
||||
}
|
||||
|
||||
public Task<PrSubmission> InsertAsync(PrSubmission submission, CancellationToken ct = default)
|
||||
{
|
||||
var created = submission with { Id = submission.Id == Guid.Empty ? Guid.NewGuid() : submission.Id, CreatedAt = DateTimeOffset.UtcNow };
|
||||
_store.Add(created);
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
public Task UpdateStatusAsync(Guid id, string status, string? verdict = null, CancellationToken ct = default)
|
||||
{
|
||||
var index = _store.FindIndex(s => s.Id == id);
|
||||
if (index >= 0)
|
||||
{
|
||||
_store[index] = _store[index] with { Status = status, Verdict = verdict };
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Remediation.Core\StellaOps.Remediation.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user