up
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user