up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-24 07:52:25 +02:00
parent 5970f0d9bd
commit 150b3730ef
215 changed files with 8119 additions and 740 deletions

View File

@@ -32,6 +32,7 @@ public sealed record LnmLinksetPage(
public sealed record LnmLinksetNormalized(
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases,
[property: JsonPropertyName("purl")] IReadOnlyList<string>? Purl,
[property: JsonPropertyName("cpe")] IReadOnlyList<string>? Cpe,
[property: JsonPropertyName("versions")] IReadOnlyList<string>? Versions,
[property: JsonPropertyName("ranges")] IReadOnlyList<object>? Ranges,
[property: JsonPropertyName("severities")] IReadOnlyList<object>? Severities);

View File

@@ -1752,6 +1752,9 @@ LnmLinksetResponse ToLnmResponse(
bool includeObservations)
{
var normalized = linkset.Normalized;
var severity = normalized?.Severities?.FirstOrDefault() is { } severityDict
? ExtractSeverity(severityDict)
: null;
var conflicts = includeConflicts
? (linkset.Conflicts ?? Array.Empty<AdvisoryLinksetConflict>()).Select(c =>
new LnmLinksetConflict(
@@ -1764,7 +1767,13 @@ LnmLinksetResponse ToLnmResponse(
: Array.Empty<LnmLinksetConflict>();
var timeline = includeTimeline
? Array.Empty<LnmLinksetTimeline>() // timeline not yet captured in linkset store
? new[]
{
new LnmLinksetTimeline(
Event: "created",
At: linkset.CreatedAt,
EvidenceHash: linkset.Provenance?.ObservationHashes?.FirstOrDefault())
}
: Array.Empty<LnmLinksetTimeline>();
var provenance = linkset.Provenance is null
@@ -1780,6 +1789,7 @@ LnmLinksetResponse ToLnmResponse(
: new LnmLinksetNormalized(
Aliases: null,
Purl: normalized.Purls,
Cpe: normalized.Cpes,
Versions: normalized.Versions,
Ranges: normalized.Ranges?.Select(r => (object)r).ToArray(),
Severities: normalized.Severities?.Select(s => (object)s).ToArray());
@@ -1788,11 +1798,11 @@ LnmLinksetResponse ToLnmResponse(
linkset.AdvisoryId,
linkset.Source,
normalized?.Purls ?? Array.Empty<string>(),
Array.Empty<string>(),
normalized?.Cpes ?? Array.Empty<string>(),
Summary: null,
PublishedAt: linkset.CreatedAt,
ModifiedAt: linkset.CreatedAt,
Severity: null,
Severity: severity,
Status: "fact-only",
provenance,
conflicts,
@@ -1803,6 +1813,27 @@ LnmLinksetResponse ToLnmResponse(
Observations: includeObservations ? linkset.ObservationIds : Array.Empty<string>());
}
string? ExtractSeverity(IReadOnlyDictionary<string, object?> severityDict)
{
if (severityDict.TryGetValue("system", out var systemObj) && systemObj is string system && !string.IsNullOrWhiteSpace(system) &&
severityDict.TryGetValue("score", out var scoreObj))
{
return $"{system}:{scoreObj}";
}
if (severityDict.TryGetValue("score", out var scoreOnly) && scoreOnly is not null)
{
return scoreOnly.ToString();
}
if (severityDict.TryGetValue("value", out var value) && value is string valueString && !string.IsNullOrWhiteSpace(valueString))
{
return valueString;
}
return null;
}
IResult JsonResult<T>(T value, int? statusCode = null)
{
var payload = JsonSerializer.Serialize(value, Program.JsonOptions);

View File

@@ -241,6 +241,7 @@ components:
properties:
aliases: { type: array, items: { type: string } }
purl: { type: array, items: { type: string } }
cpe: { type: array, items: { type: string } }
versions: { type: array, items: { type: string } }
ranges: { type: array, items: { type: object } }
severities: { type: array, items: { type: object } }

View File

@@ -20,10 +20,14 @@ public sealed record AdvisoryLinkset(
public sealed record AdvisoryLinksetNormalized(
IReadOnlyList<string>? Purls,
IReadOnlyList<string>? Cpes,
IReadOnlyList<string>? Versions,
IReadOnlyList<Dictionary<string, object?>>? Ranges,
IReadOnlyList<Dictionary<string, object?>>? Severities)
{
public List<string>? CpesToList()
=> Cpes is null ? null : Cpes.ToList();
public List<BsonDocument>? RangesToBson()
=> Ranges is null ? null : Ranges.Select(BsonDocumentHelper.FromDictionary).ToList();

View File

@@ -12,7 +12,7 @@ internal static class AdvisoryLinksetNormalization
public static AdvisoryLinksetNormalized? FromRawLinkset(RawLinkset linkset)
{
ArgumentNullException.ThrowIfNull(linkset);
return Build(linkset.PackageUrls);
return Build(linkset.PackageUrls, linkset.Cpes);
}
public static AdvisoryLinksetNormalized? FromPurls(IEnumerable<string>? purls)
@@ -22,7 +22,7 @@ internal static class AdvisoryLinksetNormalization
return null;
}
return Build(purls);
return Build(purls, Enumerable.Empty<string>());
}
public static (AdvisoryLinksetNormalized? normalized, double? confidence, IReadOnlyList<AdvisoryLinksetConflict> conflicts) FromRawLinksetWithConfidence(
@@ -31,7 +31,7 @@ internal static class AdvisoryLinksetNormalization
{
ArgumentNullException.ThrowIfNull(linkset);
var normalized = Build(linkset.PackageUrls);
var normalized = Build(linkset.PackageUrls, linkset.Cpes);
var inputs = new[]
{
@@ -51,18 +51,19 @@ internal static class AdvisoryLinksetNormalization
return (normalized, coerced, conflicts);
}
private static AdvisoryLinksetNormalized? Build(IEnumerable<string> purlValues)
private static AdvisoryLinksetNormalized? Build(IEnumerable<string> purlValues, IEnumerable<string>? cpeValues)
{
var normalizedPurls = NormalizePurls(purlValues);
var normalizedCpes = NormalizeCpes(cpeValues);
var versions = ExtractVersions(normalizedPurls);
var ranges = BuildVersionRanges(normalizedPurls);
if (normalizedPurls.Count == 0 && versions.Count == 0 && ranges.Count == 0)
if (normalizedPurls.Count == 0 && normalizedCpes.Count == 0 && versions.Count == 0 && ranges.Count == 0)
{
return null;
}
return new AdvisoryLinksetNormalized(normalizedPurls, versions, ranges, null);
return new AdvisoryLinksetNormalized(normalizedPurls, normalizedCpes, versions, ranges, null);
}
private static List<string> NormalizePurls(IEnumerable<string> purls)
@@ -147,6 +148,31 @@ internal static class AdvisoryLinksetNormalization
return ranges;
}
private static List<string> NormalizeCpes(IEnumerable<string>? cpes)
{
if (cpes is null)
{
return new List<string>(capacity: 0);
}
var distinct = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var cpe in cpes)
{
var normalized = Validation.TrimToNull(cpe);
if (normalized is null)
{
continue;
}
if (LinksetNormalization.TryNormalizeCpe(normalized, out var canonical) && !string.IsNullOrEmpty(canonical))
{
distinct.Add(canonical);
}
}
return distinct.ToList();
}
private static bool LooksLikeRange(string value)
{
return value.IndexOfAny(new[] { '^', '~', '*', ' ', ',', '|', '>' , '<' }) >= 0 ||

View File

@@ -61,6 +61,11 @@ public sealed class AdvisoryLinksetNormalizedDocument
public List<string>? Purls { get; set; }
= new();
[BsonElement("cpes")]
[BsonIgnoreIfNull]
public List<string>? Cpes { get; set; }
= new();
[BsonElement("versions")]
[BsonIgnoreIfNull]
public List<string>? Versions { get; set; }

View File

@@ -125,6 +125,7 @@ internal sealed class ConcelierMongoLinksetStore : IMongoAdvisoryLinksetStore
Normalized = linkset.Normalized is null ? null : new AdvisoryLinksetNormalizedDocument
{
Purls = linkset.Normalized.Purls is null ? null : new List<string>(linkset.Normalized.Purls),
Cpes = linkset.Normalized.Cpes is null ? null : new List<string>(linkset.Normalized.Cpes),
Versions = linkset.Normalized.Versions is null ? null : new List<string>(linkset.Normalized.Versions),
Ranges = linkset.Normalized.RangesToBson(),
Severities = linkset.Normalized.SeveritiesToBson(),
@@ -141,6 +142,7 @@ internal sealed class ConcelierMongoLinksetStore : IMongoAdvisoryLinksetStore
doc.Observations.ToImmutableArray(),
doc.Normalized is null ? null : new CoreLinksets.AdvisoryLinksetNormalized(
doc.Normalized.Purls,
doc.Normalized.Cpes,
doc.Normalized.Versions,
doc.Normalized.Ranges?.Select(ToDictionary).ToList(),
doc.Normalized.Severities?.Select(ToDictionary).ToList()),

View File

@@ -214,6 +214,7 @@ internal sealed class EnsureLinkNotMergeCollectionsMigration : IMongoMigration
{ "properties", new BsonDocument
{
{ "purls", new BsonDocument { { "bsonType", new BsonArray { "array", "null" } }, { "items", new BsonDocument("bsonType", "string") } } },
{ "cpes", new BsonDocument { { "bsonType", new BsonArray { "array", "null" } }, { "items", new BsonDocument("bsonType", "string") } } },
{ "versions", new BsonDocument { { "bsonType", new BsonArray { "array", "null" } }, { "items", new BsonDocument("bsonType", "string") } } },
{ "ranges", new BsonDocument { { "bsonType", new BsonArray { "array", "null" } }, { "items", new BsonDocument("bsonType", "object") } } },
{ "severities", new BsonDocument { { "bsonType", new BsonArray { "array", "null" } }, { "items", new BsonDocument("bsonType", "object") } } }

View File

@@ -14,17 +14,17 @@ public sealed class AdvisoryLinksetQueryServiceTests
{
new("tenant", "ghsa", "adv-003",
ImmutableArray.Create("obs-003"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/a"}, new[]{"1.0.0"}, null, null),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/a"}, null, new[]{"1.0.0"}, null, null),
null, null, null,
DateTimeOffset.Parse("2025-11-10T12:00:00Z"), null),
new("tenant", "ghsa", "adv-002",
ImmutableArray.Create("obs-002"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/b"}, new[]{"2.0.0"}, null, null),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/b"}, null, new[]{"2.0.0"}, null, null),
null, null, null,
DateTimeOffset.Parse("2025-11-09T12:00:00Z"), null),
new("tenant", "ghsa", "adv-001",
ImmutableArray.Create("obs-001"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/c"}, new[]{"3.0.0"}, null, null),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/c"}, null, new[]{"3.0.0"}, null, null),
null, null, null,
DateTimeOffset.Parse("2025-11-08T12:00:00Z"), null),
};

View File

@@ -18,6 +18,7 @@ public class PolicyAuthSignalFactoryTests
ObservationIds: ImmutableArray.Create("obs-1"),
Normalized: new AdvisoryLinksetNormalized(
Purls: new[] { "purl:pkg:maven/org.example/app@1.2.3" },
Cpes: null,
Versions: Array.Empty<string>(),
Ranges: null,
Severities: null),

View File

@@ -18,6 +18,7 @@ public class AdvisorySummaryMapperTests
ObservationIds: ImmutableArray.Create("obs1", "obs2"),
Normalized: new AdvisoryLinksetNormalized(
Purls: new[] { "pkg:maven/log4j/log4j@2.17.1" },
Cpes: null,
Versions: null,
Ranges: null,
Severities: null),