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
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-01 21:16:22 +02:00
parent c11d87d252
commit 909d9b6220
208 changed files with 860954 additions and 832 deletions

View File

@@ -16,6 +16,7 @@ public sealed record VexLinkset
string tenant,
string vulnerabilityId,
string productKey,
VexProductScope scope,
IEnumerable<VexLinksetObservationRefModel> observations,
IEnumerable<VexObservationDisagreement>? disagreements = null,
DateTimeOffset? createdAt = null,
@@ -25,6 +26,7 @@ public sealed record VexLinkset
Tenant = VexObservation.EnsureNotNullOrWhiteSpace(tenant, nameof(tenant)).ToLowerInvariant();
VulnerabilityId = VexObservation.EnsureNotNullOrWhiteSpace(vulnerabilityId, nameof(vulnerabilityId));
ProductKey = VexObservation.EnsureNotNullOrWhiteSpace(productKey, nameof(productKey));
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Observations = NormalizeObservations(observations);
Disagreements = NormalizeDisagreements(disagreements);
CreatedAt = (createdAt ?? DateTimeOffset.UtcNow).ToUniversalTime();
@@ -52,6 +54,11 @@ public sealed record VexLinkset
/// </summary>
public string ProductKey { get; }
/// <summary>
/// Canonical scope metadata for the product key.
/// </summary>
public VexProductScope Scope { get; }
/// <summary>
/// References to observations that contribute to this linkset.
/// </summary>
@@ -154,6 +161,7 @@ public sealed record VexLinkset
Tenant,
VulnerabilityId,
ProductKey,
Scope,
observations,
disagreements,
CreatedAt,

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Excititor.Core.Canonicalization;
namespace StellaOps.Excititor.Core.Observations;
@@ -49,6 +50,7 @@ public sealed class VexLinksetExtractionService
foreach (var group in groups)
{
var linksetId = BuildLinksetId(group.Key.VulnerabilityId, group.Key.ProductKey);
var scope = BuildScope(group.Key.ProductKey);
var obsForGroup = group.Select(x => x.obs);
var evt = VexLinksetUpdatedEventFactory.Create(
@@ -56,6 +58,7 @@ public sealed class VexLinksetExtractionService
linksetId,
group.Key.VulnerabilityId,
group.Key.ProductKey,
scope,
obsForGroup,
disagreements ?? Enumerable.Empty<VexObservationDisagreement>(),
now);
@@ -69,5 +72,46 @@ public sealed class VexLinksetExtractionService
private static string BuildLinksetId(string vulnerabilityId, string productKey)
=> $"vex:{vulnerabilityId}:{productKey}".ToLowerInvariant();
private static VexProductScope BuildScope(string productKey)
{
var canonicalizer = new VexProductKeyCanonicalizer();
try
{
var canonical = canonicalizer.Canonicalize(productKey);
var identifiers = canonical.Links
.Where(link => link is not null && !string.IsNullOrWhiteSpace(link.Identifier))
.Select(link => link.Identifier.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var purl = canonical.Links.FirstOrDefault(link => string.Equals(link.Type, "purl", StringComparison.OrdinalIgnoreCase))?.Identifier;
var cpe = canonical.Links.FirstOrDefault(link => string.Equals(link.Type, "cpe", StringComparison.OrdinalIgnoreCase))?.Identifier;
var version = ExtractVersion(purl ?? canonical.ProductKey);
return new VexProductScope(
ProductKey: canonical.ProductKey,
Type: canonical.Scope.ToString().ToLowerInvariant(),
Version: version,
Purl: purl,
Cpe: cpe,
Identifiers: identifiers);
}
catch
{
return VexProductScope.Unknown(productKey);
}
}
private static string? ExtractVersion(string? key)
{
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
var at = key.LastIndexOf('@');
return at >= 0 && at < key.Length - 1 ? key[(at + 1)..] : null;
}
private static string Normalize(string value) => VexObservation.EnsureNotNullOrWhiteSpace(value, nameof(value));
}

View File

@@ -21,6 +21,7 @@ public static class VexLinksetUpdatedEventFactory
string linksetId,
string vulnerabilityId,
string productKey,
VexProductScope scope,
IEnumerable<VexObservation> observations,
IEnumerable<VexObservationDisagreement> disagreements,
DateTimeOffset createdAtUtc)
@@ -62,6 +63,7 @@ public static class VexLinksetUpdatedEventFactory
normalizedLinksetId,
normalizedVulnerabilityId,
normalizedProductKey,
scope,
observationRefs,
disagreementList,
createdAtUtc);
@@ -151,6 +153,7 @@ public sealed record VexLinksetUpdatedEvent(
string LinksetId,
string VulnerabilityId,
string ProductKey,
VexProductScope Scope,
ImmutableArray<VexLinksetObservationRefCore> Observations,
ImmutableArray<VexObservationDisagreement> Disagreements,
DateTimeOffset CreatedAtUtc);

View File

@@ -0,0 +1,18 @@
using System.Collections.Immutable;
namespace StellaOps.Excititor.Core.Observations;
/// <summary>
/// Canonical scope metadata derived from a product identifier.
/// </summary>
public sealed record VexProductScope(
string ProductKey,
string Type,
string? Version,
string? Purl,
string? Cpe,
ImmutableArray<string> Identifiers)
{
public static VexProductScope Unknown(string productKey) =>
new(productKey ?? string.Empty, "unknown", null, null, null, ImmutableArray<string>.Empty);
}

View File

@@ -103,13 +103,25 @@ public sealed record VexWorkerJobContext
string connectorId,
Guid runId,
string? checkpoint,
DateTimeOffset startedAt)
DateTimeOffset startedAt,
Guid? orchestratorJobId = null,
Guid? orchestratorLeaseId = null,
DateTimeOffset? leaseExpiresAt = null,
string? jobType = null,
string? correlationId = null,
Guid? orchestratorRunId = null)
{
Tenant = EnsureNotNullOrWhiteSpace(tenant, nameof(tenant));
ConnectorId = EnsureNotNullOrWhiteSpace(connectorId, nameof(connectorId));
RunId = runId;
Checkpoint = checkpoint?.Trim();
StartedAt = startedAt;
OrchestratorJobId = orchestratorJobId;
OrchestratorLeaseId = orchestratorLeaseId;
LeaseExpiresAt = leaseExpiresAt;
JobType = jobType;
CorrelationId = correlationId;
OrchestratorRunId = orchestratorRunId;
}
public string Tenant { get; }
@@ -117,6 +129,12 @@ public sealed record VexWorkerJobContext
public Guid RunId { get; }
public string? Checkpoint { get; }
public DateTimeOffset StartedAt { get; }
public Guid? OrchestratorJobId { get; }
public Guid? OrchestratorLeaseId { get; }
public DateTimeOffset? LeaseExpiresAt { get; private set; }
public string? JobType { get; }
public string? CorrelationId { get; }
public Guid? OrchestratorRunId { get; }
/// <summary>
/// Current sequence number for heartbeats.
@@ -128,6 +146,11 @@ public sealed record VexWorkerJobContext
/// </summary>
public long NextSequence() => ++Sequence;
/// <summary>
/// Updates the tracked lease expiration when the orchestrator extends it.
/// </summary>
public void UpdateLease(DateTimeOffset leaseUntil) => LeaseExpiresAt = leaseUntil;
private static string EnsureNotNullOrWhiteSpace(string value, string name)
=> string.IsNullOrWhiteSpace(value) ? throw new ArgumentException($"{name} must be provided.", name) : value.Trim();
}

View File

@@ -57,6 +57,15 @@ internal sealed class MongoVexLinksetEventPublisher : IVexLinksetEventPublisher
LinksetId = @event.LinksetId,
VulnerabilityId = @event.VulnerabilityId,
ProductKey = @event.ProductKey,
Scope = new VexLinksetScopeRecord
{
ProductKey = @event.Scope.ProductKey,
Type = @event.Scope.Type,
Version = @event.Scope.Version,
Purl = @event.Scope.Purl,
Cpe = @event.Scope.Cpe,
Identifiers = @event.Scope.Identifiers.ToList(),
},
Observations = @event.Observations
.Select(o => new VexLinksetEventObservationRecord
{

View File

@@ -98,6 +98,7 @@ internal sealed class MongoVexLinksetStore : IVexLinksetStore
normalizedTenant,
normalizedVuln,
normalizedProduct,
scope: VexProductScope.Unknown(normalizedProduct),
observations: Array.Empty<VexLinksetObservationRefModel>(),
disagreements: null,
createdAt: DateTimeOffset.UtcNow,
@@ -275,6 +276,7 @@ internal sealed class MongoVexLinksetStore : IVexLinksetStore
LinksetId = linkset.LinksetId,
VulnerabilityId = linkset.VulnerabilityId.ToLowerInvariant(),
ProductKey = linkset.ProductKey.ToLowerInvariant(),
Scope = ToScopeRecord(linkset.Scope),
ProviderIds = linkset.ProviderIds.ToList(),
Statuses = linkset.Statuses.ToList(),
CreatedAt = linkset.CreatedAt.UtcDateTime,
@@ -326,14 +328,49 @@ internal sealed class MongoVexLinksetStore : IVexLinksetStore
d.Confidence))
.ToImmutableArray() ?? ImmutableArray<VexObservationDisagreement>.Empty;
var scope = record.Scope is not null
? ToScope(record.Scope)
: VexProductScope.Unknown(record.ProductKey);
return new VexLinkset(
linksetId: record.LinksetId,
tenant: record.Tenant,
vulnerabilityId: record.VulnerabilityId,
productKey: record.ProductKey,
scope: scope,
observations: observations,
disagreements: disagreements,
createdAt: new DateTimeOffset(record.CreatedAt, TimeSpan.Zero),
updatedAt: new DateTimeOffset(record.UpdatedAt, TimeSpan.Zero));
}
private static VexLinksetScopeRecord ToScopeRecord(VexProductScope scope)
{
return new VexLinksetScopeRecord
{
ProductKey = scope.ProductKey,
Type = scope.Type,
Version = scope.Version,
Purl = scope.Purl,
Cpe = scope.Cpe,
Identifiers = scope.Identifiers.ToList()
};
}
private static VexProductScope ToScope(VexLinksetScopeRecord record)
{
var identifiers = record.Identifiers?
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray() ?? ImmutableArray<string>.Empty;
return new VexProductScope(
ProductKey: record.ProductKey ?? string.Empty,
Type: record.Type ?? "unknown",
Version: record.Version,
Purl: record.Purl,
Cpe: record.Cpe,
Identifiers: identifiers);
}
}

View File

@@ -484,6 +484,8 @@ internal sealed class VexLinksetRecord
public string ProductKey { get; set; } = default!;
public VexLinksetScopeRecord? Scope { get; set; }
public List<string> ProviderIds { get; set; } = new();
public List<string> Statuses { get; set; } = new();
@@ -1397,6 +1399,8 @@ internal sealed class VexLinksetEventRecord
public string ProductKey { get; set; } = default!;
public VexLinksetScopeRecord? Scope { get; set; }
public List<VexLinksetEventObservationRecord> Observations { get; set; } = new();
public List<VexLinksetDisagreementRecord> Disagreements { get; set; } = new();
@@ -1410,6 +1414,24 @@ internal sealed class VexLinksetEventRecord
public int ObservationCount { get; set; } = 0;
}
internal sealed class VexLinksetScopeRecord
{
public string ProductKey { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string? Version { get; set; }
= null;
public string? Purl { get; set; }
= null;
public string? Cpe { get; set; }
= null;
public List<string> Identifiers { get; set; } = new();
}
[BsonIgnoreExtraElements]
internal sealed class VexLinksetEventObservationRecord
{