save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -233,12 +233,17 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
c.CreditType,
c.Contact is not null ? new[] { c.Contact } : Array.Empty<string>(),
AdvisoryProvenance.Empty)).ToArray();
var referenceModels = references.Select(r => new AdvisoryReference(
r.Url,
r.RefType,
null,
null,
AdvisoryProvenance.Empty)).ToArray();
var referenceDetails = TryReadReferenceDetails(entity.RawPayload);
var referenceModels = references.Select(r =>
{
referenceDetails.TryGetValue(r.Url, out var detail);
return new AdvisoryReference(
r.Url,
r.RefType,
detail.SourceTag,
detail.Summary,
AdvisoryProvenance.Empty);
}).ToArray();
var cvssModels = cvss.Select(c => new CvssMetric(
c.CvssVersion,
c.VectorString,
@@ -269,7 +274,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
}
}
var (platform, normalizedVersions) = ReadDatabaseSpecific(a.DatabaseSpecific);
var (platform, normalizedVersions, statuses) = ReadDatabaseSpecific(a.DatabaseSpecific);
var effectivePlatform = platform ?? ResolvePlatformFromRanges(versionRanges);
var resolvedNormalizedVersions = normalizedVersions ?? BuildNormalizedVersions(versionRanges);
@@ -278,7 +283,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
a.PackageName,
effectivePlatform,
versionRanges,
Array.Empty<AffectedPackageStatus>(),
statuses ?? Array.Empty<AffectedPackageStatus>(),
Array.Empty<AdvisoryProvenance>(),
resolvedNormalizedVersions);
}).ToArray();
@@ -377,6 +382,63 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
}
}
private static IReadOnlyDictionary<string, (string? SourceTag, string? Summary)> TryReadReferenceDetails(string? rawPayload)
{
if (string.IsNullOrWhiteSpace(rawPayload))
{
return new Dictionary<string, (string?, string?)>(StringComparer.OrdinalIgnoreCase);
}
try
{
using var document = JsonDocument.Parse(rawPayload, new JsonDocumentOptions
{
AllowTrailingCommas = true
});
if (!document.RootElement.TryGetProperty("references", out var referencesElement)
|| referencesElement.ValueKind != JsonValueKind.Array)
{
return new Dictionary<string, (string?, string?)>(StringComparer.OrdinalIgnoreCase);
}
var lookup = new Dictionary<string, (string?, string?)>(StringComparer.OrdinalIgnoreCase);
foreach (var element in referencesElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!element.TryGetProperty("url", out var urlElement) || urlElement.ValueKind != JsonValueKind.String)
{
continue;
}
var url = urlElement.GetString();
if (string.IsNullOrWhiteSpace(url))
{
continue;
}
var sourceTag = element.TryGetProperty("sourceTag", out var sourceTagElement) && sourceTagElement.ValueKind == JsonValueKind.String
? sourceTagElement.GetString()
: null;
var summary = element.TryGetProperty("summary", out var summaryElement) && summaryElement.ValueKind == JsonValueKind.String
? summaryElement.GetString()
: null;
lookup[url] = (sourceTag, summary);
}
return lookup;
}
catch (JsonException)
{
return new Dictionary<string, (string?, string?)>(StringComparer.OrdinalIgnoreCase);
}
}
private static string MapEcosystemToType(string ecosystem)
{
return ecosystem.ToLowerInvariant() switch
@@ -402,11 +464,14 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
};
}
private static (string? Platform, IReadOnlyList<NormalizedVersionRule>? NormalizedVersions) ReadDatabaseSpecific(string? databaseSpecific)
private static (
string? Platform,
IReadOnlyList<NormalizedVersionRule>? NormalizedVersions,
IReadOnlyList<AffectedPackageStatus>? Statuses) ReadDatabaseSpecific(string? databaseSpecific)
{
if (string.IsNullOrWhiteSpace(databaseSpecific) || databaseSpecific == "{}")
{
return (null, null);
return (null, null, null);
}
try
@@ -426,11 +491,24 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
normalizedVersions = JsonSerializer.Deserialize<NormalizedVersionRule[]>(normalizedValue.GetRawText(), JsonOptions);
}
return (platform, normalizedVersions);
IReadOnlyList<AffectedPackageStatus>? statuses = null;
if (root.TryGetProperty("statuses", out var statusValue) && statusValue.ValueKind == JsonValueKind.Array)
{
var statusStrings = JsonSerializer.Deserialize<string[]>(statusValue.GetRawText(), JsonOptions);
if (statusStrings is { Length: > 0 })
{
statuses = statusStrings
.Where(static status => !string.IsNullOrWhiteSpace(status))
.Select(static status => new AffectedPackageStatus(status.Trim(), AdvisoryProvenance.Empty))
.ToArray();
}
}
return (platform, normalizedVersions, statuses);
}
catch (JsonException)
{
return (null, null);
return (null, null, null);
}
}

View File

@@ -88,7 +88,7 @@ public sealed class AdvisoryConverter
ImpactScore = null,
Source = metric.Provenance.Source,
IsPrimary = isPrimaryCvss,
CreatedAt = now
CreatedAt = metric.Provenance.RecordedAt
});
isPrimaryCvss = false;
}
@@ -264,6 +264,11 @@ public sealed class AdvisoryConverter
payload["normalizedVersions"] = package.NormalizedVersions;
}
if (!package.Statuses.IsEmpty)
{
payload["statuses"] = package.Statuses.Select(static status => status.Status).ToArray();
}
return payload.Count == 0
? null
: JsonSerializer.Serialize(payload, JsonOptions);

View File

@@ -130,6 +130,7 @@ public sealed class AdvisoryCanonicalRepository : RepositoryBase<ConcelierDataSo
public async Task<Guid> UpsertAsync(AdvisoryCanonicalEntity entity, CancellationToken ct = default)
{
var normalizedSeverity = NormalizeSeverity(entity.Severity);
const string sql = """
INSERT INTO vuln.advisory_canonical
(id, cve, affects_key, version_range, weakness, merge_hash,
@@ -161,7 +162,7 @@ public sealed class AdvisoryCanonicalRepository : RepositoryBase<ConcelierDataSo
AddTextArrayParameter(cmd, "weakness", entity.Weakness);
AddParameter(cmd, "merge_hash", entity.MergeHash);
AddParameter(cmd, "status", entity.Status);
AddParameter(cmd, "severity", entity.Severity);
AddParameter(cmd, "severity", normalizedSeverity);
AddParameter(cmd, "epss_score", entity.EpssScore);
AddParameter(cmd, "exploit_known", entity.ExploitKnown);
AddParameter(cmd, "title", entity.Title);
@@ -235,6 +236,16 @@ public sealed class AdvisoryCanonicalRepository : RepositoryBase<ConcelierDataSo
#endregion
private static string? NormalizeSeverity(string? severity)
{
if (string.IsNullOrWhiteSpace(severity))
{
return null;
}
return severity.Trim().ToLowerInvariant();
}
#region Source Edge Operations
public Task<IReadOnlyList<AdvisorySourceEdgeEntity>> GetSourceEdgesAsync(Guid canonicalId, CancellationToken ct = default)

View File

@@ -33,10 +33,10 @@ public sealed class AdvisoryCvssRepository : RepositoryBase<ConcelierDataSource>
const string insertSql = """
INSERT INTO vuln.advisory_cvss
(id, advisory_id, cvss_version, vector_string, base_score, base_severity,
exploitability_score, impact_score, source, is_primary)
exploitability_score, impact_score, source, is_primary, created_at)
VALUES
(@id, @advisory_id, @cvss_version, @vector_string, @base_score, @base_severity,
@exploitability_score, @impact_score, @source, @is_primary)
@exploitability_score, @impact_score, @source, @is_primary, @created_at)
""";
foreach (var score in scores)
@@ -53,6 +53,7 @@ public sealed class AdvisoryCvssRepository : RepositoryBase<ConcelierDataSource>
AddParameter(insertCmd, "impact_score", score.ImpactScore);
AddParameter(insertCmd, "source", score.Source);
AddParameter(insertCmd, "is_primary", score.IsPrimary);
AddParameter(insertCmd, "created_at", score.CreatedAt);
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}

View File

@@ -194,6 +194,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
int offset = 0,
CancellationToken cancellationToken = default)
{
var normalizedSeverity = NormalizeSeverity(severity);
var sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
@@ -203,7 +204,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
WHERE search_vector @@ websearch_to_tsquery('english', @query)
""";
if (!string.IsNullOrEmpty(severity))
if (!string.IsNullOrEmpty(normalizedSeverity))
{
sql += " AND severity = @severity";
}
@@ -216,9 +217,9 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
cmd =>
{
AddParameter(cmd, "query", query);
if (!string.IsNullOrEmpty(severity))
if (!string.IsNullOrEmpty(normalizedSeverity))
{
AddParameter(cmd, "severity", severity);
AddParameter(cmd, "severity", normalizedSeverity);
}
AddParameter(cmd, "limit", limit);
AddParameter(cmd, "offset", offset);
@@ -234,6 +235,8 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
int offset = 0,
CancellationToken cancellationToken = default)
{
var normalizedSeverity = NormalizeSeverity(severity)
?? throw new ArgumentException("Severity must be provided.", nameof(severity));
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
@@ -249,7 +252,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
sql,
cmd =>
{
AddParameter(cmd, "severity", severity);
AddParameter(cmd, "severity", normalizedSeverity);
AddParameter(cmd, "limit", limit);
AddParameter(cmd, "offset", offset);
},
@@ -363,6 +366,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
IEnumerable<KevFlagEntity>? kevFlags,
CancellationToken cancellationToken)
{
var normalizedSeverity = NormalizeSeverity(advisory.Severity);
const string sql = """
INSERT INTO vuln.advisories (
id, advisory_key, primary_vuln_id, source_id, title, summary, description,
@@ -404,7 +408,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
AddParameter(command, "title", advisory.Title);
AddParameter(command, "summary", advisory.Summary);
AddParameter(command, "description", advisory.Description);
AddParameter(command, "severity", advisory.Severity);
AddParameter(command, "severity", normalizedSeverity);
AddParameter(command, "published_at", advisory.PublishedAt);
AddParameter(command, "modified_at", advisory.ModifiedAt);
AddParameter(command, "withdrawn_at", advisory.WithdrawnAt);
@@ -450,6 +454,16 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
return result;
}
private static string? NormalizeSeverity(string? severity)
{
if (string.IsNullOrWhiteSpace(severity))
{
return null;
}
return severity.Trim().ToLowerInvariant();
}
private static async Task ReplaceAliasesAsync(
Guid advisoryId,
IEnumerable<AdvisoryAliasEntity> aliases,
@@ -498,10 +512,10 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
const string insertSql = """
INSERT INTO vuln.advisory_cvss
(id, advisory_id, cvss_version, vector_string, base_score, base_severity,
exploitability_score, impact_score, source, is_primary)
exploitability_score, impact_score, source, is_primary, created_at)
VALUES
(@id, @advisory_id, @cvss_version, @vector_string, @base_score, @base_severity,
@exploitability_score, @impact_score, @source, @is_primary)
@exploitability_score, @impact_score, @source, @is_primary, @created_at)
""";
foreach (var score in scores)
@@ -517,6 +531,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
insertCmd.Parameters.AddWithValue("impact_score", (object?)score.ImpactScore ?? DBNull.Value);
insertCmd.Parameters.AddWithValue("source", (object?)score.Source ?? DBNull.Value);
insertCmd.Parameters.AddWithValue("is_primary", score.IsPrimary);
insertCmd.Parameters.AddWithValue("created_at", score.CreatedAt);
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0230-M | DONE | Maintainability audit for StellaOps.Concelier.Persistence. |
| AUDIT-0230-T | DONE | Test coverage audit for StellaOps.Concelier.Persistence. |
| AUDIT-0230-A | TODO | Pending approval for changes. |
| CICD-VAL-SMOKE-001 | DONE | Smoke validation: restore reference summaries from raw payload. |