Implement ledger metrics for observability and add tests for Ruby packages endpoints
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added `LedgerMetrics` class to record write latency and total events for ledger operations.
- Created comprehensive tests for Ruby packages endpoints, covering scenarios for missing inventory, successful retrieval, and identifier handling.
- Introduced `TestSurfaceSecretsScope` for managing environment variables during tests.
- Developed `ProvenanceMongoExtensions` for attaching DSSE provenance and trust information to event documents.
- Implemented `EventProvenanceWriter` and `EventWriter` classes for managing event provenance in MongoDB.
- Established MongoDB indexes for efficient querying of events based on provenance and trust.
- Added models and JSON parsing logic for DSSE provenance and trust information.
This commit is contained in:
master
2025-11-13 09:29:09 +02:00
parent 151f6b35cc
commit 61f963fd52
101 changed files with 5881 additions and 1776 deletions

View File

@@ -27,31 +27,43 @@ public sealed class AdvisoryConflictDocument
[BsonElement("statementIds")]
public List<string> StatementIds { get; set; } = new();
[BsonElement("details")]
public BsonDocument Details { get; set; } = new();
[BsonElement("details")]
public BsonDocument Details { get; set; } = new();
[BsonElement("provenance")]
[BsonIgnoreIfNull]
public BsonDocument? Provenance { get; set; }
[BsonElement("trust")]
[BsonIgnoreIfNull]
public BsonDocument? Trust { get; set; }
}
internal static class AdvisoryConflictDocumentExtensions
{
public static AdvisoryConflictDocument FromRecord(AdvisoryConflictRecord record)
=> new()
{
Id = record.Id.ToString(),
VulnerabilityKey = record.VulnerabilityKey,
ConflictHash = record.ConflictHash,
AsOf = record.AsOf.UtcDateTime,
RecordedAt = record.RecordedAt.UtcDateTime,
StatementIds = record.StatementIds.Select(static id => id.ToString()).ToList(),
Details = (BsonDocument)record.Details.DeepClone(),
};
public static AdvisoryConflictRecord ToRecord(this AdvisoryConflictDocument document)
=> new(
Guid.Parse(document.Id),
document.VulnerabilityKey,
document.ConflictHash,
DateTime.SpecifyKind(document.AsOf, DateTimeKind.Utc),
DateTime.SpecifyKind(document.RecordedAt, DateTimeKind.Utc),
document.StatementIds.Select(static value => Guid.Parse(value)).ToList(),
(BsonDocument)document.Details.DeepClone());
=> new()
{
Id = record.Id.ToString(),
VulnerabilityKey = record.VulnerabilityKey,
ConflictHash = record.ConflictHash,
AsOf = record.AsOf.UtcDateTime,
RecordedAt = record.RecordedAt.UtcDateTime,
StatementIds = record.StatementIds.Select(static id => id.ToString()).ToList(),
Details = (BsonDocument)record.Details.DeepClone(),
Provenance = record.Provenance is null ? null : (BsonDocument)record.Provenance.DeepClone(),
Trust = record.Trust is null ? null : (BsonDocument)record.Trust.DeepClone(),
};
public static AdvisoryConflictRecord ToRecord(this AdvisoryConflictDocument document)
=> new(
Guid.Parse(document.Id),
document.VulnerabilityKey,
document.ConflictHash,
DateTime.SpecifyKind(document.AsOf, DateTimeKind.Utc),
DateTime.SpecifyKind(document.RecordedAt, DateTimeKind.Utc),
document.StatementIds.Select(static value => Guid.Parse(value)).ToList(),
(BsonDocument)document.Details.DeepClone(),
document.Provenance is null ? null : (BsonDocument)document.Provenance.DeepClone(),
document.Trust is null ? null : (BsonDocument)document.Trust.DeepClone());
}

View File

@@ -4,11 +4,13 @@ using MongoDB.Bson;
namespace StellaOps.Concelier.Storage.Mongo.Conflicts;
public sealed record AdvisoryConflictRecord(
Guid Id,
string VulnerabilityKey,
byte[] ConflictHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
IReadOnlyList<Guid> StatementIds,
BsonDocument Details);
public sealed record AdvisoryConflictRecord(
Guid Id,
string VulnerabilityKey,
byte[] ConflictHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
IReadOnlyList<Guid> StatementIds,
BsonDocument Details,
BsonDocument? Provenance = null,
BsonDocument? Trust = null);

View File

@@ -1,224 +1,425 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Conflicts;
using StellaOps.Concelier.Storage.Mongo.Statements;
namespace StellaOps.Concelier.Storage.Mongo.Events;
public sealed class MongoAdvisoryEventRepository : IAdvisoryEventRepository
{
private readonly IAdvisoryStatementStore _statementStore;
private readonly IAdvisoryConflictStore _conflictStore;
public MongoAdvisoryEventRepository(
IAdvisoryStatementStore statementStore,
IAdvisoryConflictStore conflictStore)
{
_statementStore = statementStore ?? throw new ArgumentNullException(nameof(statementStore));
_conflictStore = conflictStore ?? throw new ArgumentNullException(nameof(conflictStore));
}
public async ValueTask InsertStatementsAsync(
IReadOnlyCollection<AdvisoryStatementEntry> statements,
CancellationToken cancellationToken)
{
if (statements is null)
{
throw new ArgumentNullException(nameof(statements));
}
if (statements.Count == 0)
{
return;
}
var records = statements
.Select(static entry =>
{
var payload = BsonDocument.Parse(entry.CanonicalJson);
return new AdvisoryStatementRecord(
entry.StatementId,
entry.VulnerabilityKey,
entry.AdvisoryKey,
entry.StatementHash.ToArray(),
entry.AsOf,
entry.RecordedAt,
payload,
entry.InputDocumentIds.ToArray());
})
.ToList();
await _statementStore.InsertAsync(records, cancellationToken).ConfigureAwait(false);
}
public async ValueTask InsertConflictsAsync(
IReadOnlyCollection<AdvisoryConflictEntry> conflicts,
CancellationToken cancellationToken)
{
if (conflicts is null)
{
throw new ArgumentNullException(nameof(conflicts));
}
if (conflicts.Count == 0)
{
return;
}
var records = conflicts
.Select(static entry =>
{
var payload = BsonDocument.Parse(entry.CanonicalJson);
return new AdvisoryConflictRecord(
entry.ConflictId,
entry.VulnerabilityKey,
entry.ConflictHash.ToArray(),
entry.AsOf,
entry.RecordedAt,
entry.StatementIds.ToArray(),
payload);
})
.ToList();
await _conflictStore.InsertAsync(records, cancellationToken).ConfigureAwait(false);
}
public async ValueTask<IReadOnlyList<AdvisoryStatementEntry>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken)
{
var records = await _statementStore
.GetStatementsAsync(vulnerabilityKey, asOf, cancellationToken)
.ConfigureAwait(false);
if (records.Count == 0)
{
return Array.Empty<AdvisoryStatementEntry>();
}
var entries = records
.Select(static record =>
{
var advisory = CanonicalJsonSerializer.Deserialize<Advisory>(record.Payload.ToJson());
var canonicalJson = CanonicalJsonSerializer.Serialize(advisory);
return new AdvisoryStatementEntry(
record.Id,
record.VulnerabilityKey,
record.AdvisoryKey,
canonicalJson,
record.StatementHash.ToImmutableArray(),
record.AsOf,
record.RecordedAt,
record.InputDocumentIds.ToImmutableArray());
})
.ToList();
return entries;
}
public async ValueTask<IReadOnlyList<AdvisoryConflictEntry>> GetConflictsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken)
{
var records = await _conflictStore
.GetConflictsAsync(vulnerabilityKey, asOf, cancellationToken)
.ConfigureAwait(false);
if (records.Count == 0)
{
return Array.Empty<AdvisoryConflictEntry>();
}
var entries = records
.Select(static record =>
{
var canonicalJson = Canonicalize(record.Details);
return new AdvisoryConflictEntry(
record.Id,
record.VulnerabilityKey,
canonicalJson,
record.ConflictHash.ToImmutableArray(),
record.AsOf,
record.RecordedAt,
record.StatementIds.ToImmutableArray());
})
.ToList();
return entries;
}
private static readonly JsonWriterOptions CanonicalWriterOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false,
SkipValidation = false,
};
private static string Canonicalize(BsonDocument document)
{
using var json = JsonDocument.Parse(document.ToJson());
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream, CanonicalWriterOptions))
{
WriteCanonical(json.RootElement, writer);
}
return Encoding.UTF8.GetString(stream.ToArray());
}
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (var property in element.EnumerateObject().OrderBy(static p => p.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(property.Name);
WriteCanonical(property.Value, writer);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in element.EnumerateArray())
{
WriteCanonical(item, writer);
}
writer.WriteEndArray();
break;
case JsonValueKind.String:
writer.WriteStringValue(element.GetString());
break;
case JsonValueKind.Number:
writer.WriteRawValue(element.GetRawText());
break;
case JsonValueKind.True:
writer.WriteBooleanValue(true);
break;
case JsonValueKind.False:
writer.WriteBooleanValue(false);
break;
case JsonValueKind.Null:
writer.WriteNullValue();
break;
default:
writer.WriteRawValue(element.GetRawText());
break;
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Conflicts;
using StellaOps.Concelier.Storage.Mongo.Statements;
using StellaOps.Provenance.Mongo;
namespace StellaOps.Concelier.Storage.Mongo.Events;
public sealed class MongoAdvisoryEventRepository : IAdvisoryEventRepository
{
private readonly IAdvisoryStatementStore _statementStore;
private readonly IAdvisoryConflictStore _conflictStore;
public MongoAdvisoryEventRepository(
IAdvisoryStatementStore statementStore,
IAdvisoryConflictStore conflictStore)
{
_statementStore = statementStore ?? throw new ArgumentNullException(nameof(statementStore));
_conflictStore = conflictStore ?? throw new ArgumentNullException(nameof(conflictStore));
}
public async ValueTask InsertStatementsAsync(
IReadOnlyCollection<AdvisoryStatementEntry> statements,
CancellationToken cancellationToken)
{
if (statements is null)
{
throw new ArgumentNullException(nameof(statements));
}
if (statements.Count == 0)
{
return;
}
var records = statements
.Select(static entry =>
{
var payload = BsonDocument.Parse(entry.CanonicalJson);
var (provenanceDoc, trustDoc) = BuildMetadata(entry.Provenance, entry.Trust);
return new AdvisoryStatementRecord(
entry.StatementId,
entry.VulnerabilityKey,
entry.AdvisoryKey,
entry.StatementHash.ToArray(),
entry.AsOf,
entry.RecordedAt,
payload,
entry.InputDocumentIds.ToArray(),
provenanceDoc,
trustDoc);
})
.ToList();
await _statementStore.InsertAsync(records, cancellationToken).ConfigureAwait(false);
}
public async ValueTask InsertConflictsAsync(
IReadOnlyCollection<AdvisoryConflictEntry> conflicts,
CancellationToken cancellationToken)
{
if (conflicts is null)
{
throw new ArgumentNullException(nameof(conflicts));
}
if (conflicts.Count == 0)
{
return;
}
var records = conflicts
.Select(static entry =>
{
var payload = BsonDocument.Parse(entry.CanonicalJson);
var (provenanceDoc, trustDoc) = BuildMetadata(entry.Provenance, entry.Trust);
return new AdvisoryConflictRecord(
entry.ConflictId,
entry.VulnerabilityKey,
entry.ConflictHash.ToArray(),
entry.AsOf,
entry.RecordedAt,
entry.StatementIds.ToArray(),
payload,
provenanceDoc,
trustDoc);
})
.ToList();
await _conflictStore.InsertAsync(records, cancellationToken).ConfigureAwait(false);
}
public async ValueTask<IReadOnlyList<AdvisoryStatementEntry>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken)
{
var records = await _statementStore
.GetStatementsAsync(vulnerabilityKey, asOf, cancellationToken)
.ConfigureAwait(false);
if (records.Count == 0)
{
return Array.Empty<AdvisoryStatementEntry>();
}
var entries = records
.Select(static record =>
{
var advisory = CanonicalJsonSerializer.Deserialize<Advisory>(record.Payload.ToJson());
var canonicalJson = CanonicalJsonSerializer.Serialize(advisory);
var (provenance, trust) = ParseMetadata(record.Provenance, record.Trust);
return new AdvisoryStatementEntry(
record.Id,
record.VulnerabilityKey,
record.AdvisoryKey,
canonicalJson,
record.StatementHash.ToImmutableArray(),
record.AsOf,
record.RecordedAt,
record.InputDocumentIds.ToImmutableArray(),
provenance,
trust);
})
.ToList();
return entries;
}
public async ValueTask<IReadOnlyList<AdvisoryConflictEntry>> GetConflictsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken)
{
var records = await _conflictStore
.GetConflictsAsync(vulnerabilityKey, asOf, cancellationToken)
.ConfigureAwait(false);
if (records.Count == 0)
{
return Array.Empty<AdvisoryConflictEntry>();
}
var entries = records
.Select(static record =>
{
var canonicalJson = Canonicalize(record.Details);
var (provenance, trust) = ParseMetadata(record.Provenance, record.Trust);
return new AdvisoryConflictEntry(
record.Id,
record.VulnerabilityKey,
canonicalJson,
record.ConflictHash.ToImmutableArray(),
record.AsOf,
record.RecordedAt,
record.StatementIds.ToImmutableArray(),
provenance,
trust);
})
.ToList();
return entries;
}
public async ValueTask AttachStatementProvenanceAsync(
Guid statementId,
DsseProvenance dsse,
TrustInfo trust,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(dsse);
ArgumentNullException.ThrowIfNull(trust);
var (provenanceDoc, trustDoc) = BuildMetadata(dsse, trust);
if (provenanceDoc is null || trustDoc is null)
{
throw new InvalidOperationException("Failed to build provenance documents.");
}
await _statementStore
.UpdateProvenanceAsync(statementId, provenanceDoc, trustDoc, cancellationToken)
.ConfigureAwait(false);
}
private static readonly JsonWriterOptions CanonicalWriterOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false,
SkipValidation = false,
};
private static string Canonicalize(BsonDocument document)
{
using var json = JsonDocument.Parse(document.ToJson());
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream, CanonicalWriterOptions))
{
WriteCanonical(json.RootElement, writer);
}
return Encoding.UTF8.GetString(stream.ToArray());
}
private static (BsonDocument? Provenance, BsonDocument? Trust) BuildMetadata(DsseProvenance? provenance, TrustInfo? trust)
{
if (provenance is null || trust is null)
{
return (null, null);
}
var metadata = new BsonDocument();
metadata.AttachDsseProvenance(provenance, trust);
var provenanceDoc = metadata.TryGetValue("provenance", out var provenanceValue)
? (BsonDocument)provenanceValue.DeepClone()
: null;
var trustDoc = metadata.TryGetValue("trust", out var trustValue)
? (BsonDocument)trustValue.DeepClone()
: null;
return (provenanceDoc, trustDoc);
}
private static (DsseProvenance?, TrustInfo?) ParseMetadata(BsonDocument? provenanceDoc, BsonDocument? trustDoc)
{
DsseProvenance? dsse = null;
if (provenanceDoc is not null &&
provenanceDoc.TryGetValue("dsse", out var dsseValue) &&
dsseValue is BsonDocument dsseBody)
{
if (TryGetString(dsseBody, "envelopeDigest", out var envelopeDigest) &&
TryGetString(dsseBody, "payloadType", out var payloadType) &&
dsseBody.TryGetValue("key", out var keyValue) &&
keyValue is BsonDocument keyDoc &&
TryGetString(keyDoc, "keyId", out var keyId))
{
var keyInfo = new DsseKeyInfo
{
KeyId = keyId,
Issuer = GetOptionalString(keyDoc, "issuer"),
Algo = GetOptionalString(keyDoc, "algo"),
};
dsse = new DsseProvenance
{
EnvelopeDigest = envelopeDigest,
PayloadType = payloadType,
Key = keyInfo,
Rekor = ParseRekor(dsseBody),
Chain = ParseChain(dsseBody)
};
}
}
TrustInfo? trust = null;
if (trustDoc is not null)
{
trust = new TrustInfo
{
Verified = trustDoc.TryGetValue("verified", out var verifiedValue) && verifiedValue.ToBoolean(),
Verifier = GetOptionalString(trustDoc, "verifier"),
Witnesses = trustDoc.TryGetValue("witnesses", out var witnessValue) && witnessValue.IsInt32 ? witnessValue.AsInt32 : (int?)null,
PolicyScore = trustDoc.TryGetValue("policyScore", out var scoreValue) && scoreValue.IsNumeric ? scoreValue.AsDouble : (double?)null
};
}
return (dsse, trust);
}
private static DsseRekorInfo? ParseRekor(BsonDocument dsseBody)
{
if (!dsseBody.TryGetValue("rekor", out var rekorValue) || !rekorValue.IsBsonDocument)
{
return null;
}
var rekorDoc = rekorValue.AsBsonDocument;
if (!TryGetInt64(rekorDoc, "logIndex", out var logIndex))
{
return null;
}
return new DsseRekorInfo
{
LogIndex = logIndex,
Uuid = GetOptionalString(rekorDoc, "uuid") ?? string.Empty,
IntegratedTime = TryGetInt64(rekorDoc, "integratedTime", out var integratedTime) ? integratedTime : null,
MirrorSeq = TryGetInt64(rekorDoc, "mirrorSeq", out var mirrorSeq) ? mirrorSeq : null
};
}
private static IReadOnlyCollection<DsseChainLink>? ParseChain(BsonDocument dsseBody)
{
if (!dsseBody.TryGetValue("chain", out var chainValue) || !chainValue.IsBsonArray)
{
return null;
}
var links = new List<DsseChainLink>();
foreach (var element in chainValue.AsBsonArray)
{
if (!element.IsBsonDocument)
{
continue;
}
var linkDoc = element.AsBsonDocument;
if (!TryGetString(linkDoc, "type", out var type) ||
!TryGetString(linkDoc, "id", out var id) ||
!TryGetString(linkDoc, "digest", out var digest))
{
continue;
}
links.Add(new DsseChainLink
{
Type = type,
Id = id,
Digest = digest
});
}
return links.Count == 0 ? null : links;
}
private static bool TryGetString(BsonDocument document, string name, out string value)
{
if (document.TryGetValue(name, out var bsonValue) && bsonValue.IsString)
{
value = bsonValue.AsString;
return true;
}
value = string.Empty;
return false;
}
private static string? GetOptionalString(BsonDocument document, string name)
=> document.TryGetValue(name, out var bsonValue) && bsonValue.IsString ? bsonValue.AsString : null;
private static bool TryGetInt64(BsonDocument document, string name, out long value)
{
if (document.TryGetValue(name, out var bsonValue))
{
if (bsonValue.IsInt64)
{
value = bsonValue.AsInt64;
return true;
}
if (bsonValue.IsInt32)
{
value = bsonValue.AsInt32;
return true;
}
if (bsonValue.IsString && long.TryParse(bsonValue.AsString, out var parsed))
{
value = parsed;
return true;
}
}
value = 0;
return false;
}
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (var property in element.EnumerateObject().OrderBy(static p => p.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(property.Name);
WriteCanonical(property.Value, writer);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in element.EnumerateArray())
{
WriteCanonical(item, writer);
}
writer.WriteEndArray();
break;
case JsonValueKind.String:
writer.WriteStringValue(element.GetString());
break;
case JsonValueKind.Number:
writer.WriteRawValue(element.GetRawText());
break;
case JsonValueKind.True:
writer.WriteBooleanValue(true);
break;
case JsonValueKind.False:
writer.WriteBooleanValue(false);
break;
case JsonValueKind.Null:
writer.WriteNullValue();
break;
default:
writer.WriteRawValue(element.GetRawText());
break;
}
}
}

View File

@@ -28,7 +28,15 @@ public sealed class AdvisoryStatementDocument
public DateTime RecordedAt { get; set; }
[BsonElement("payload")]
public BsonDocument Payload { get; set; } = new();
public BsonDocument Payload { get; set; } = new();
[BsonElement("provenance")]
[BsonIgnoreIfNull]
public BsonDocument? Provenance { get; set; }
[BsonElement("trust")]
[BsonIgnoreIfNull]
public BsonDocument? Trust { get; set; }
[BsonElement("inputDocuments")]
public List<string> InputDocuments { get; set; } = new();
@@ -37,26 +45,30 @@ public sealed class AdvisoryStatementDocument
internal static class AdvisoryStatementDocumentExtensions
{
public static AdvisoryStatementDocument FromRecord(AdvisoryStatementRecord record)
=> new()
{
Id = record.Id.ToString(),
VulnerabilityKey = record.VulnerabilityKey,
AdvisoryKey = record.AdvisoryKey,
StatementHash = record.StatementHash,
AsOf = record.AsOf.UtcDateTime,
RecordedAt = record.RecordedAt.UtcDateTime,
Payload = (BsonDocument)record.Payload.DeepClone(),
InputDocuments = record.InputDocumentIds.Select(static id => id.ToString()).ToList(),
};
=> new()
{
Id = record.Id.ToString(),
VulnerabilityKey = record.VulnerabilityKey,
AdvisoryKey = record.AdvisoryKey,
StatementHash = record.StatementHash,
AsOf = record.AsOf.UtcDateTime,
RecordedAt = record.RecordedAt.UtcDateTime,
Payload = (BsonDocument)record.Payload.DeepClone(),
Provenance = record.Provenance is null ? null : (BsonDocument)record.Provenance.DeepClone(),
Trust = record.Trust is null ? null : (BsonDocument)record.Trust.DeepClone(),
InputDocuments = record.InputDocumentIds.Select(static id => id.ToString()).ToList(),
};
public static AdvisoryStatementRecord ToRecord(this AdvisoryStatementDocument document)
=> new(
Guid.Parse(document.Id),
document.VulnerabilityKey,
document.AdvisoryKey,
document.StatementHash,
DateTime.SpecifyKind(document.AsOf, DateTimeKind.Utc),
DateTime.SpecifyKind(document.RecordedAt, DateTimeKind.Utc),
(BsonDocument)document.Payload.DeepClone(),
document.InputDocuments.Select(static value => Guid.Parse(value)).ToList());
=> new(
Guid.Parse(document.Id),
document.VulnerabilityKey,
document.AdvisoryKey,
document.StatementHash,
DateTime.SpecifyKind(document.AsOf, DateTimeKind.Utc),
DateTime.SpecifyKind(document.RecordedAt, DateTimeKind.Utc),
(BsonDocument)document.Payload.DeepClone(),
document.InputDocuments.Select(static value => Guid.Parse(value)).ToList(),
document.Provenance is null ? null : (BsonDocument)document.Provenance.DeepClone(),
document.Trust is null ? null : (BsonDocument)document.Trust.DeepClone());
}

View File

@@ -4,12 +4,14 @@ using MongoDB.Bson;
namespace StellaOps.Concelier.Storage.Mongo.Statements;
public sealed record AdvisoryStatementRecord(
Guid Id,
string VulnerabilityKey,
string AdvisoryKey,
byte[] StatementHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
BsonDocument Payload,
IReadOnlyList<Guid> InputDocumentIds);
public sealed record AdvisoryStatementRecord(
Guid Id,
string VulnerabilityKey,
string AdvisoryKey,
byte[] StatementHash,
DateTimeOffset AsOf,
DateTimeOffset RecordedAt,
BsonDocument Payload,
IReadOnlyList<Guid> InputDocumentIds,
BsonDocument? Provenance = null,
BsonDocument? Trust = null);

View File

@@ -3,23 +3,31 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver;
namespace StellaOps.Concelier.Storage.Mongo.Statements;
public interface IAdvisoryStatementStore
{
ValueTask InsertAsync(
IReadOnlyCollection<AdvisoryStatementRecord> statements,
CancellationToken cancellationToken,
IClientSessionHandle? session = null);
ValueTask<IReadOnlyList<AdvisoryStatementRecord>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken,
IClientSessionHandle? session = null);
}
public interface IAdvisoryStatementStore
{
ValueTask InsertAsync(
IReadOnlyCollection<AdvisoryStatementRecord> statements,
CancellationToken cancellationToken,
IClientSessionHandle? session = null);
ValueTask<IReadOnlyList<AdvisoryStatementRecord>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken,
IClientSessionHandle? session = null);
ValueTask UpdateProvenanceAsync(
Guid statementId,
BsonDocument provenance,
BsonDocument trust,
CancellationToken cancellationToken,
IClientSessionHandle? session = null);
}
public sealed class AdvisoryStatementStore : IAdvisoryStatementStore
{
@@ -63,13 +71,13 @@ public sealed class AdvisoryStatementStore : IAdvisoryStatementStore
}
}
public async ValueTask<IReadOnlyList<AdvisoryStatementRecord>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken,
IClientSessionHandle? session = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityKey);
public async ValueTask<IReadOnlyList<AdvisoryStatementRecord>> GetStatementsAsync(
string vulnerabilityKey,
DateTimeOffset? asOf,
CancellationToken cancellationToken,
IClientSessionHandle? session = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityKey);
var filter = Builders<AdvisoryStatementDocument>.Filter.Eq(document => document.VulnerabilityKey, vulnerabilityKey);
@@ -88,6 +96,31 @@ public sealed class AdvisoryStatementStore : IAdvisoryStatementStore
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return documents.Select(static document => document.ToRecord()).ToList();
}
}
return documents.Select(static document => document.ToRecord()).ToList();
}
public async ValueTask UpdateProvenanceAsync(
Guid statementId,
BsonDocument provenance,
BsonDocument trust,
CancellationToken cancellationToken,
IClientSessionHandle? session = null)
{
ArgumentNullException.ThrowIfNull(provenance);
ArgumentNullException.ThrowIfNull(trust);
var filter = Builders<AdvisoryStatementDocument>.Filter.Eq(document => document.Id, statementId.ToString());
var update = Builders<AdvisoryStatementDocument>.Update
.Set(document => document.Provenance, provenance)
.Set(document => document.Trust, trust);
var result = session is null
? await _collection.UpdateOneAsync(filter, update, cancellationToken: cancellationToken).ConfigureAwait(false)
: await _collection.UpdateOneAsync(session, filter, update, cancellationToken: cancellationToken).ConfigureAwait(false);
if (result.MatchedCount == 0)
{
throw new InvalidOperationException($"Statement {statementId} not found.");
}
}
}

View File

@@ -15,5 +15,6 @@
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Provenance.Mongo\StellaOps.Provenance.Mongo.csproj" />
</ItemGroup>
</Project>