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