wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -11,22 +11,22 @@ public interface IRiskStateRepository
/// <summary>
/// Store a risk state snapshot.
/// </summary>
Task StoreSnapshotAsync(RiskStateSnapshot snapshot, CancellationToken ct = default);
Task StoreSnapshotAsync(RiskStateSnapshot snapshot, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Store multiple risk state snapshots.
/// </summary>
Task StoreSnapshotsAsync(IReadOnlyList<RiskStateSnapshot> snapshots, CancellationToken ct = default);
Task StoreSnapshotsAsync(IReadOnlyList<RiskStateSnapshot> snapshots, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get the latest snapshot for a finding.
/// </summary>
Task<RiskStateSnapshot?> GetLatestSnapshotAsync(FindingKey findingKey, CancellationToken ct = default);
Task<RiskStateSnapshot?> GetLatestSnapshotAsync(FindingKey findingKey, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get snapshots for a scan.
/// </summary>
Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsForScanAsync(string scanId, CancellationToken ct = default);
Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsForScanAsync(string scanId, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get snapshot history for a finding.
@@ -34,12 +34,13 @@ public interface IRiskStateRepository
Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotHistoryAsync(
FindingKey findingKey,
int limit = 10,
CancellationToken ct = default);
CancellationToken ct = default,
string? tenantId = null);
/// <summary>
/// Get snapshots by state hash.
/// </summary>
Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsByHashAsync(string stateHash, CancellationToken ct = default);
Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsByHashAsync(string stateHash, CancellationToken ct = default, string? tenantId = null);
}
/// <summary>
@@ -50,17 +51,17 @@ public interface IMaterialRiskChangeRepository
/// <summary>
/// Store a material risk change result.
/// </summary>
Task StoreChangeAsync(MaterialRiskChangeResult change, string scanId, CancellationToken ct = default);
Task StoreChangeAsync(MaterialRiskChangeResult change, string scanId, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Store multiple material risk change results.
/// </summary>
Task StoreChangesAsync(IReadOnlyList<MaterialRiskChangeResult> changes, string scanId, CancellationToken ct = default);
Task StoreChangesAsync(IReadOnlyList<MaterialRiskChangeResult> changes, string scanId, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get material changes for a scan.
/// </summary>
Task<IReadOnlyList<MaterialRiskChangeResult>> GetChangesForScanAsync(string scanId, CancellationToken ct = default);
Task<IReadOnlyList<MaterialRiskChangeResult>> GetChangesForScanAsync(string scanId, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get material changes for a finding.
@@ -68,14 +69,16 @@ public interface IMaterialRiskChangeRepository
Task<IReadOnlyList<MaterialRiskChangeResult>> GetChangesForFindingAsync(
FindingKey findingKey,
int limit = 10,
CancellationToken ct = default);
CancellationToken ct = default,
string? tenantId = null);
/// <summary>
/// Query material changes with filters.
/// </summary>
Task<MaterialRiskChangeQueryResult> QueryChangesAsync(
MaterialRiskChangeQuery query,
CancellationToken ct = default);
CancellationToken ct = default,
string? tenantId = null);
}
/// <summary>
@@ -105,32 +108,40 @@ public sealed record MaterialRiskChangeQueryResult(
/// </summary>
public sealed class InMemoryRiskStateRepository : IRiskStateRepository
{
private readonly List<RiskStateSnapshot> _snapshots = [];
private readonly List<(string TenantId, RiskStateSnapshot Snapshot)> _snapshots = [];
private readonly object _lock = new();
public Task StoreSnapshotAsync(RiskStateSnapshot snapshot, CancellationToken ct = default)
public Task StoreSnapshotAsync(RiskStateSnapshot snapshot, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
_snapshots.Add(snapshot);
_snapshots.Add((normalizedTenant, snapshot));
}
return Task.CompletedTask;
}
public Task StoreSnapshotsAsync(IReadOnlyList<RiskStateSnapshot> snapshots, CancellationToken ct = default)
public Task StoreSnapshotsAsync(IReadOnlyList<RiskStateSnapshot> snapshots, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
_snapshots.AddRange(snapshots);
foreach (var snapshot in snapshots)
{
_snapshots.Add((normalizedTenant, snapshot));
}
}
return Task.CompletedTask;
}
public Task<RiskStateSnapshot?> GetLatestSnapshotAsync(FindingKey findingKey, CancellationToken ct = default)
public Task<RiskStateSnapshot?> GetLatestSnapshotAsync(FindingKey findingKey, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
var snapshot = _snapshots
.Where(entry => string.Equals(entry.TenantId, normalizedTenant, StringComparison.Ordinal))
.Select(entry => entry.Snapshot)
.Where(s => s.FindingKey == findingKey)
.OrderByDescending(s => s.CapturedAt)
.FirstOrDefault();
@@ -138,11 +149,14 @@ public sealed class InMemoryRiskStateRepository : IRiskStateRepository
}
}
public Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsForScanAsync(string scanId, CancellationToken ct = default)
public Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsForScanAsync(string scanId, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
var snapshots = _snapshots
.Where(entry => string.Equals(entry.TenantId, normalizedTenant, StringComparison.Ordinal))
.Select(entry => entry.Snapshot)
.Where(s => s.ScanId == scanId)
.ToList();
return Task.FromResult<IReadOnlyList<RiskStateSnapshot>>(snapshots);
@@ -152,11 +166,15 @@ public sealed class InMemoryRiskStateRepository : IRiskStateRepository
public Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotHistoryAsync(
FindingKey findingKey,
int limit = 10,
CancellationToken ct = default)
CancellationToken ct = default,
string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
var snapshots = _snapshots
.Where(entry => string.Equals(entry.TenantId, normalizedTenant, StringComparison.Ordinal))
.Select(entry => entry.Snapshot)
.Where(s => s.FindingKey == findingKey)
.OrderByDescending(s => s.CapturedAt)
.Take(limit)
@@ -165,16 +183,24 @@ public sealed class InMemoryRiskStateRepository : IRiskStateRepository
}
}
public Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsByHashAsync(string stateHash, CancellationToken ct = default)
public Task<IReadOnlyList<RiskStateSnapshot>> GetSnapshotsByHashAsync(string stateHash, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
var snapshots = _snapshots
.Where(entry => string.Equals(entry.TenantId, normalizedTenant, StringComparison.Ordinal))
.Select(entry => entry.Snapshot)
.Where(s => s.ComputeStateHash() == stateHash)
.ToList();
return Task.FromResult<IReadOnlyList<RiskStateSnapshot>>(snapshots);
}
}
private static string NormalizeTenant(string? tenantId)
=> string.IsNullOrWhiteSpace(tenantId)
? "default"
: tenantId.Trim().ToLowerInvariant();
}
/// <summary>
@@ -186,54 +212,70 @@ public sealed class InMemoryVexCandidateStore : IVexCandidateStore
private readonly Dictionary<string, VexCandidateReview> _reviews = [];
private readonly object _lock = new();
public Task StoreCandidatesAsync(IReadOnlyList<VexCandidate> candidates, CancellationToken ct = default)
public Task StoreCandidatesAsync(IReadOnlyList<VexCandidate> candidates, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
foreach (var candidate in candidates)
{
_candidates[candidate.CandidateId] = candidate;
_candidates[BuildCandidateKey(normalizedTenant, candidate.CandidateId)] = candidate;
}
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<VexCandidate>> GetCandidatesAsync(string imageDigest, CancellationToken ct = default)
public Task<IReadOnlyList<VexCandidate>> GetCandidatesAsync(string imageDigest, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
var tenantPrefix = $"{normalizedTenant}:";
lock (_lock)
{
var candidates = _candidates.Values
.Where(c => c.ImageDigest == imageDigest)
var candidates = _candidates
.Where(entry => entry.Key.StartsWith(tenantPrefix, StringComparison.Ordinal))
.Select(entry => entry.Value)
.Where(candidate => candidate.ImageDigest == imageDigest)
.ToList();
return Task.FromResult<IReadOnlyList<VexCandidate>>(candidates);
}
}
public Task<VexCandidate?> GetCandidateAsync(string candidateId, CancellationToken ct = default)
public Task<VexCandidate?> GetCandidateAsync(string candidateId, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
lock (_lock)
{
_candidates.TryGetValue(candidateId, out var candidate);
_candidates.TryGetValue(BuildCandidateKey(normalizedTenant, candidateId), out var candidate);
return Task.FromResult(candidate);
}
}
public Task<bool> ReviewCandidateAsync(string candidateId, VexCandidateReview review, CancellationToken ct = default)
public Task<bool> ReviewCandidateAsync(string candidateId, VexCandidateReview review, CancellationToken ct = default, string? tenantId = null)
{
var normalizedTenant = NormalizeTenant(tenantId);
var candidateKey = BuildCandidateKey(normalizedTenant, candidateId);
lock (_lock)
{
if (!_candidates.ContainsKey(candidateId))
if (!_candidates.ContainsKey(candidateKey))
return Task.FromResult(false);
_reviews[candidateId] = review;
_reviews[candidateKey] = review;
// Update candidate to mark as reviewed
if (_candidates.TryGetValue(candidateId, out var candidate))
if (_candidates.TryGetValue(candidateKey, out var candidate))
{
_candidates[candidateId] = candidate with { RequiresReview = false };
_candidates[candidateKey] = candidate with { RequiresReview = false };
}
return Task.FromResult(true);
}
}
private static string BuildCandidateKey(string tenantId, string candidateId)
=> $"{tenantId}:{candidateId}";
private static string NormalizeTenant(string? tenantId)
=> string.IsNullOrWhiteSpace(tenantId)
? "default"
: tenantId.Trim().ToLowerInvariant();
}

View File

@@ -136,22 +136,22 @@ public interface IVexCandidateStore
/// <summary>
/// Store candidates.
/// </summary>
Task StoreCandidatesAsync(IReadOnlyList<VexCandidate> candidates, CancellationToken ct = default);
Task StoreCandidatesAsync(IReadOnlyList<VexCandidate> candidates, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get candidates for an image.
/// </summary>
Task<IReadOnlyList<VexCandidate>> GetCandidatesAsync(string imageDigest, CancellationToken ct = default);
Task<IReadOnlyList<VexCandidate>> GetCandidatesAsync(string imageDigest, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Get a specific candidate by ID.
/// </summary>
Task<VexCandidate?> GetCandidateAsync(string candidateId, CancellationToken ct = default);
Task<VexCandidate?> GetCandidateAsync(string candidateId, CancellationToken ct = default, string? tenantId = null);
/// <summary>
/// Mark a candidate as reviewed.
/// </summary>
Task<bool> ReviewCandidateAsync(string candidateId, VexCandidateReview review, CancellationToken ct = default);
Task<bool> ReviewCandidateAsync(string candidateId, VexCandidateReview review, CancellationToken ct = default, string? tenantId = null);
}
/// <summary>