Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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>