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