This commit is contained in:
StellaOps Bot
2025-12-07 22:49:53 +02:00
parent 11597679ed
commit 7c24ed96ee
204 changed files with 23313 additions and 1430 deletions

View File

@@ -0,0 +1,32 @@
<Project>
<PropertyGroup>
<!-- Keep Concelier test harness active while trimming Mongo dependencies. Allow opt-out per project. -->
<UseConcelierTestInfra Condition="'$(UseConcelierTestInfra)'==''">true</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<!-- Concelier is migrating off MongoDB; strip implicit Mongo2Go/Mongo driver packages inherited from the repo root. -->
<PackageReference Remove="Mongo2Go" />
<PackageReference Remove="MongoDB.Driver" />
</ItemGroup>
<ItemGroup Condition="$([System.String]::Copy('$(MSBuildProjectName)').EndsWith('.Tests')) and '$(UseConcelierTestInfra)'=='true'">
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.10.0" />
<ProjectReference Include="$(MSBuildThisFileDirectory)__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj"
Condition="Exists('$(MSBuildThisFileDirectory)__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj')" />
<Using Include="StellaOps.Concelier.Testing"
Condition="Exists('$(MSBuildThisFileDirectory)__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj')" />
<Using Include="Xunit" />
</ItemGroup>
<!-- Keep OpenSSL shim sources available to Mongo2Go-free test harnesses if needed. -->
<ItemGroup Condition="$([System.String]::Copy('$(MSBuildProjectName)').EndsWith('.Tests')) and '$(UseConcelierTestInfra)'=='true'">
<None Include="$(MSBuildThisFileDirectory)..\..\tests\native\openssl-1.1\linux-x64\*.so.1.1"
Link="native/linux-x64/%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(MSBuildThisFileDirectory)..\..\tests\shared\OpenSslLegacyShim.cs" Link="Shared/OpenSslLegacyShim.cs" />
<Compile Include="$(MSBuildThisFileDirectory)..\..\tests\shared\OpenSslAutoInit.cs" Link="Shared/OpenSslAutoInit.cs" />
</ItemGroup>
</Project>

View File

@@ -30,7 +30,7 @@ public sealed class RawDocumentStorage
string uri,
byte[] content,
string? contentType,
DateTimeOffset? expiresAt,
DateTimeOffset? ExpiresAt,
CancellationToken cancellationToken,
Guid? documentId = null)
{

View File

@@ -418,7 +418,7 @@ public sealed class UbuntuConnector : IFeedConnector
await _stateRepository.UpdateCursorAsync(SourceName, doc, _timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
}
private static string ComputeNoticeHash(BsonDocument document)
private string ComputeNoticeHash(BsonDocument document)
{
var bytes = document.ToBson();
var hash = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace StellaOps.Concelier.Core.Linksets
{
public static class PolicyAuthSignalFactory
{
public static PolicyAuthSignal ToPolicyAuthSignal(AdvisoryLinkset linkset)
{
if (linkset is null) throw new ArgumentNullException(nameof(linkset));
var subject = linkset.Normalized?.Purls?.FirstOrDefault() ?? linkset.AdvisoryId;
var evidenceUri = $"urn:linkset:{linkset.AdvisoryId}";
return new PolicyAuthSignal(
Id: linkset.AdvisoryId,
Tenant: linkset.TenantId,
Subject: subject ?? string.Empty,
Source: linkset.Source,
SignalType: "reachability",
Evidence: new[]
{
new PolicyAuthEvidence(evidenceUri)
});
}
}
public sealed record PolicyAuthSignal(
string Id,
string Tenant,
string Subject,
string Source,
string SignalType,
IReadOnlyList<PolicyAuthEvidence> Evidence);
public sealed record PolicyAuthEvidence(string Uri);
}

View File

@@ -1,248 +1,276 @@
using System;
using System.Collections;
using System.Text;
using System.Globalization;
using System.Text.Json;
namespace MongoDB.Bson
{
public readonly struct ObjectId : IEquatable<ObjectId>
public class BsonValue : IEquatable<BsonValue?>
{
public Guid Value { get; }
public ObjectId(Guid value) => Value = value;
public ObjectId(string value) => Value = Guid.TryParse(value, out var g) ? g : Guid.Empty;
public static ObjectId GenerateNewId() => new(Guid.NewGuid());
public static ObjectId Empty => new(Guid.Empty);
public bool Equals(ObjectId other) => Value.Equals(other.Value);
public override bool Equals(object? obj) => obj is ObjectId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString("N");
public static bool operator ==(ObjectId left, ObjectId right) => left.Equals(right);
public static bool operator !=(ObjectId left, ObjectId right) => !left.Equals(right);
}
protected object? RawValue;
public enum BsonType { Document, Array, String, Boolean, Int32, Int64, Double, DateTime, Guid, Null }
public class BsonValue
{
protected readonly object? _value;
public BsonValue(object? value) => _value = value;
internal object? RawValue => _value;
public static implicit operator BsonValue(string value) => new BsonString(value ?? string.Empty);
public static implicit operator BsonValue(bool value) => new BsonBoolean(value);
public static implicit operator BsonValue(int value) => new BsonInt32(value);
public static implicit operator BsonValue(long value) => new BsonInt64(value);
public static implicit operator BsonValue(double value) => new BsonDouble(value);
public static implicit operator BsonValue(DateTime value) => new BsonDateTime(DateTime.SpecifyKind(value, DateTimeKind.Utc));
public static implicit operator BsonValue(DateTimeOffset value) => new BsonDateTime(value.UtcDateTime);
public static implicit operator BsonValue(Guid value) => new BsonString(value.ToString("D"));
public static BsonValue Create(object? value) => BsonDocument.WrapExternal(value);
public virtual BsonType BsonType => _value switch
public BsonValue(object? value = null)
{
null => BsonType.Null,
BsonDocument => BsonType.Document,
BsonArray => BsonType.Array,
string => BsonType.String,
bool => BsonType.Boolean,
int => BsonType.Int32,
long => BsonType.Int64,
double => BsonType.Double,
DateTime => BsonType.DateTime,
DateTimeOffset => BsonType.DateTime,
Guid => BsonType.Guid,
_ => BsonType.Null
};
public bool IsString => _value is string;
public bool IsBsonDocument => _value is BsonDocument;
public bool IsBsonArray => _value is BsonArray;
public bool IsBsonNull => _value is null;
public string AsString => _value?.ToString() ?? string.Empty;
public BsonDocument AsBsonDocument => _value as BsonDocument ?? throw new InvalidCastException();
public BsonArray AsBsonArray => _value as BsonArray ?? throw new InvalidCastException();
public Guid AsGuid => _value is Guid g ? g : Guid.Empty;
public DateTime AsDateTime => _value switch
{
DateTimeOffset dto => dto.UtcDateTime,
DateTime dt => dt,
_ => DateTime.MinValue
};
public int AsInt32 => _value is int i ? i : 0;
public long AsInt64 => _value is long l ? l : 0;
public double AsDouble => _value is double d ? d : 0d;
public bool AsBoolean => _value is bool b && b;
public bool IsInt32 => _value is int;
public DateTime ToUniversalTime() => _value switch
{
DateTimeOffset dto => dto.UtcDateTime,
DateTime dt => dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime(),
string s when DateTimeOffset.TryParse(s, out var parsed) => parsed.UtcDateTime,
_ => DateTime.MinValue
};
public override string ToString() => _value?.ToString() ?? string.Empty;
}
public class BsonString : BsonValue { public BsonString(string value) : base(value) { } }
public class BsonBoolean : BsonValue { public BsonBoolean(bool value) : base(value) { } }
public class BsonInt32 : BsonValue { public BsonInt32(int value) : base(value) { } }
public class BsonInt64 : BsonValue { public BsonInt64(long value) : base(value) { } }
public class BsonDouble : BsonValue { public BsonDouble(double value) : base(value) { } }
public class BsonDateTime : BsonValue { public BsonDateTime(DateTime value) : base(value) { } }
public class BsonNull : BsonValue
{
private BsonNull() : base(null) { }
public static BsonNull Value { get; } = new();
}
public sealed class BsonElement
{
public BsonElement(string name, BsonValue value)
{
Name = name;
Value = value;
RawValue = value;
}
public string Name { get; }
public BsonValue Value { get; }
}
public bool IsString => RawValue is string;
public bool IsBoolean => RawValue is bool;
public bool IsBsonDocument => RawValue is BsonDocument;
public bool IsBsonArray => RawValue is BsonArray;
public class BsonBinaryData : BsonValue
{
private readonly byte[] _bytes;
public BsonBinaryData(byte[] bytes) : base(null) => _bytes = bytes ?? Array.Empty<byte>();
public BsonBinaryData(Guid guid) : this(guid.ToByteArray()) { }
public byte[] AsByteArray => _bytes;
public Guid ToGuid() => new(_bytes);
}
public class BsonArray : BsonValue, IEnumerable<BsonValue>
{
private readonly List<BsonValue> _items = new();
public BsonArray() : base(null) { }
public BsonArray(IEnumerable<BsonValue> values) : this() => _items.AddRange(values);
public BsonArray(IEnumerable<object?> values) : this()
public string AsString => RawValue switch
{
foreach (var value in values)
{
_items.Add(BsonDocument.WrapExternal(value));
}
}
public void Add(BsonValue value) => _items.Add(value);
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public BsonValue this[int index] { get => _items[index]; set => _items[index] = value; }
public int Count => _items.Count;
null => string.Empty,
string s => s,
Guid g => g.ToString(),
_ => Convert.ToString(RawValue, CultureInfo.InvariantCulture) ?? string.Empty
};
public bool AsBoolean => RawValue switch
{
bool b => b,
string s when bool.TryParse(s, out var b) => b,
int i => i != 0,
long l => l != 0,
_ => false
};
public int ToInt32() => RawValue switch
{
int i => i,
long l => (int)l,
double d => (int)d,
string s when int.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var i) => i,
_ => 0
};
public Guid AsGuid => RawValue switch
{
Guid g => g,
string s when Guid.TryParse(s, out var g) => g,
_ => Guid.Empty
};
public ObjectId AsObjectId => RawValue switch
{
ObjectId o => o,
string s => ObjectId.Parse(s),
_ => ObjectId.Empty
};
public BsonDocument AsBsonDocument => RawValue as BsonDocument ?? (this as BsonDocument ?? new BsonDocument());
public BsonArray AsBsonArray => RawValue as BsonArray ?? (this as BsonArray ?? new BsonArray());
public override string ToString() => AsString;
internal virtual BsonValue Clone() => new BsonValue(RawValue);
public bool Equals(BsonValue? other) => other is not null && Equals(RawValue, other.RawValue);
public override bool Equals(object? obj) => obj is BsonValue other && Equals(other);
public override int GetHashCode() => RawValue?.GetHashCode() ?? 0;
public static implicit operator BsonValue(string value) => new(value);
public static implicit operator BsonValue(Guid value) => new(value);
public static implicit operator BsonValue(int value) => new(value);
public static implicit operator BsonValue(long value) => new(value);
public static implicit operator BsonValue(bool value) => new(value);
public static implicit operator BsonValue(double value) => new(value);
public static implicit operator BsonValue(DateTimeOffset value) => new(value);
}
public class BsonDocument : BsonValue, IEnumerable<KeyValuePair<string, BsonValue>>
public sealed class BsonDocument : BsonValue, IDictionary<string, BsonValue>
{
private readonly Dictionary<string, BsonValue> _values = new(StringComparer.Ordinal);
public BsonDocument() : base(null) { }
public BsonDocument(string key, object? value) : this() => _values[key] = Wrap(value);
public BsonDocument(IEnumerable<KeyValuePair<string, object?>> pairs) : this()
public BsonDocument()
: base(null)
{
foreach (var kvp in pairs)
RawValue = this;
}
public BsonDocument(IDictionary<string, object?> values)
: this()
{
foreach (var kvp in values)
{
_values[kvp.Key] = Wrap(kvp.Value);
_values[kvp.Key] = ToBsonValue(kvp.Value);
}
}
private static BsonValue Wrap(object? value) => value switch
{
BsonValue v => v,
IEnumerable<BsonValue> enumerable => new BsonArray(enumerable),
IEnumerable<object?> objEnum => new BsonArray(objEnum.Select(Wrap)),
_ => new BsonValue(value)
};
internal static BsonValue WrapExternal(object? value) => Wrap(value);
public int ElementCount => _values.Count;
public BsonValue this[string key]
{
get => _values[key];
set => _values[key] = Wrap(value);
set => _values[key] = value ?? new BsonValue();
}
public int ElementCount => _values.Count;
public IEnumerable<BsonElement> Elements => _values.Select(kvp => new BsonElement(kvp.Key, kvp.Value));
public ICollection<string> Keys => _values.Keys;
public ICollection<BsonValue> Values => _values.Values;
public int Count => _values.Count;
public bool IsReadOnly => false;
public bool Contains(string key) => _values.ContainsKey(key);
public void Add(string key, BsonValue value) => _values[key] = value ?? new BsonValue();
public void Add(string key, object? value) => _values[key] = ToBsonValue(value);
public void Add(KeyValuePair<string, BsonValue> item) => Add(item.Key, item.Value);
public void Clear() => _values.Clear();
public bool Contains(KeyValuePair<string, BsonValue> item) => _values.Contains(item);
public bool ContainsKey(string key) => _values.ContainsKey(key);
public void CopyTo(KeyValuePair<string, BsonValue>[] array, int arrayIndex) => ((IDictionary<string, BsonValue>)_values).CopyTo(array, arrayIndex);
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator();
public bool Remove(string key) => _values.Remove(key);
public bool Remove(KeyValuePair<string, BsonValue> item) => _values.Remove(item.Key);
public bool TryGetValue(string key, out BsonValue value) => _values.TryGetValue(key, out value!);
public BsonValue GetValue(string key, BsonValue? defaultValue = null)
{
return _values.TryGetValue(key, out var value)
? value
: defaultValue ?? new BsonValue(null);
}
public bool Remove(string key) => _values.Remove(key);
public void Add(string key, BsonValue value) => _values[key] = value;
public void Add(string key, object? value) => _values[key] = Wrap(value);
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public BsonValue GetValue(string key) => _values[key];
public BsonDocument DeepClone()
{
var clone = new BsonDocument();
var copy = new BsonDocument();
foreach (var kvp in _values)
{
clone[kvp.Key] = kvp.Value;
copy._values[kvp.Key] = kvp.Value?.Clone() ?? new BsonValue();
}
return clone;
return copy;
}
public static BsonDocument Parse(string json)
{
using var doc = JsonDocument.Parse(json);
return FromElement(doc.RootElement);
return FromElement(doc.RootElement).AsBsonDocument;
}
private static BsonDocument FromElement(JsonElement element)
private static BsonValue FromElement(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.Object => FromObject(element),
JsonValueKind.Array => FromArray(element),
JsonValueKind.String => new BsonValue(element.GetString()),
JsonValueKind.Number => element.TryGetInt64(out var l) ? new BsonValue(l) : new BsonValue(element.GetDouble()),
JsonValueKind.True => new BsonValue(true),
JsonValueKind.False => new BsonValue(false),
JsonValueKind.Null or JsonValueKind.Undefined => new BsonValue(null),
_ => new BsonValue(element.ToString())
};
}
private static BsonDocument FromObject(JsonElement element)
{
var doc = new BsonDocument();
foreach (var prop in element.EnumerateObject())
foreach (var property in element.EnumerateObject())
{
doc[prop.Name] = FromJsonValue(prop.Value);
doc[property.Name] = FromElement(property.Value);
}
return doc;
}
private static BsonValue FromJsonValue(JsonElement element) => element.ValueKind switch
private static BsonArray FromArray(JsonElement element)
{
JsonValueKind.Object => FromElement(element),
JsonValueKind.Array => new BsonArray(element.EnumerateArray().Select(FromJsonValue)),
JsonValueKind.String => new BsonString(element.GetString() ?? string.Empty),
JsonValueKind.Number => element.TryGetInt64(out var l) ? new BsonInt64(l) : new BsonDouble(element.GetDouble()),
JsonValueKind.True => new BsonBoolean(true),
JsonValueKind.False => new BsonBoolean(false),
JsonValueKind.Null or JsonValueKind.Undefined => new BsonValue(null),
_ => new BsonValue(null)
};
public string ToJson(MongoDB.Bson.IO.JsonWriterSettings? settings = null)
{
var dict = _values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value));
return JsonSerializer.Serialize(dict, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var array = new BsonArray();
foreach (var item in element.EnumerateArray())
{
array.Add(FromElement(item));
}
return array;
}
public byte[] ToBson() => Encoding.UTF8.GetBytes(ToJson());
private static object? Unwrap(BsonValue value) => value switch
internal static BsonValue ToBsonValue(object? value)
{
BsonDocument doc => doc._values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value)),
BsonArray array => array.Select(Unwrap).ToArray(),
_ => value.RawValue
};
return value switch
{
null => new BsonValue(null),
BsonValue bson => bson,
string s => new BsonValue(s),
Guid g => new BsonValue(g),
int i => new BsonValue(i),
long l => new BsonValue(l),
bool b => new BsonValue(b),
double d => new BsonValue(d),
float f => new BsonValue(f),
DateTime dt => new BsonValue(dt),
DateTimeOffset dto => new BsonValue(dto),
IEnumerable<object?> enumerable => new BsonArray(enumerable.Select(ToBsonValue)),
_ => new BsonValue(value)
};
}
internal override BsonValue Clone() => DeepClone();
}
public sealed class BsonArray : BsonValue, IList<BsonValue>
{
private readonly List<BsonValue> _items = new();
public BsonArray()
: base(null)
{
RawValue = this;
}
public BsonArray(IEnumerable<BsonValue> items)
: this()
{
_items.AddRange(items);
}
public BsonValue this[int index]
{
get => _items[index];
set => _items[index] = value ?? new BsonValue();
}
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Add(BsonValue item) => _items.Add(item ?? new BsonValue());
public void Add(object? item) => _items.Add(BsonDocument.ToBsonValue(item));
public void Clear() => _items.Clear();
public bool Contains(BsonValue item) => _items.Contains(item);
public void CopyTo(BsonValue[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
public int IndexOf(BsonValue item) => _items.IndexOf(item);
public void Insert(int index, BsonValue item) => _items.Insert(index, item ?? new BsonValue());
public bool Remove(BsonValue item) => _items.Remove(item);
public void RemoveAt(int index) => _items.RemoveAt(index);
internal override BsonValue Clone() => new BsonArray(_items.Select(i => i.Clone()));
}
public readonly struct ObjectId : IEquatable<ObjectId>
{
private readonly string _value;
public ObjectId(string value)
{
_value = value;
}
public static ObjectId Empty { get; } = new(string.Empty);
public override string ToString() => _value;
public static ObjectId Parse(string value) => new(value ?? string.Empty);
public bool Equals(ObjectId other) => string.Equals(_value, other._value, StringComparison.Ordinal);
public override bool Equals(object? obj) => obj is ObjectId other && Equals(other);
public override int GetHashCode() => _value?.GetHashCode(StringComparison.Ordinal) ?? 0;
}
}
namespace MongoDB.Bson.IO
namespace MongoDB.Bson.Serialization.Attributes
{
public enum JsonOutputMode { Strict, RelaxedExtendedJson }
public class JsonWriterSettings
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class BsonElementAttribute : Attribute
{
public JsonOutputMode OutputMode { get; set; } = JsonOutputMode.Strict;
public BsonElementAttribute(string elementName)
{
ElementName = elementName;
}
public string ElementName { get; }
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
namespace MongoDB.Driver
{
@@ -31,6 +32,7 @@ namespace MongoDB.Driver
public interface IMongoClient
{
IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null);
Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default);
}
public class MongoClient : IMongoClient
@@ -38,20 +40,47 @@ namespace MongoDB.Driver
public MongoClient(string connectionString) { }
public MongoClient(MongoClientSettings settings) { }
public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null) => new MongoDatabase(name);
public Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) => Task.CompletedTask;
}
public class MongoDatabaseSettings { }
public sealed class DatabaseNamespace
{
public DatabaseNamespace(string databaseName) => DatabaseName = databaseName;
public string DatabaseName { get; }
}
public interface IMongoDatabase
{
IMongoCollection<TDocument> GetCollection<TDocument>(string name, MongoCollectionSettings? settings = null);
DatabaseNamespace DatabaseNamespace { get; }
Task DropCollectionAsync(string name, CancellationToken cancellationToken = default);
BsonDocument RunCommand(BsonDocument command, CancellationToken cancellationToken = default);
T RunCommand<T>(BsonDocument command, CancellationToken cancellationToken = default);
Task<T> RunCommandAsync<T>(BsonDocument command, CancellationToken cancellationToken = default);
BsonDocument RunCommand(string command, CancellationToken cancellationToken = default);
T RunCommand<T>(string command, CancellationToken cancellationToken = default);
Task<T> RunCommandAsync<T>(string command, CancellationToken cancellationToken = default);
}
public class MongoDatabase : IMongoDatabase
{
public MongoDatabase(string name) => Name = name;
public MongoDatabase(string name)
{
Name = name;
DatabaseNamespace = new DatabaseNamespace(name);
}
public string Name { get; }
public DatabaseNamespace DatabaseNamespace { get; }
public IMongoCollection<TDocument> GetCollection<TDocument>(string name, MongoCollectionSettings? settings = null) => new MongoCollection<TDocument>(name);
public Task DropCollectionAsync(string name, CancellationToken cancellationToken = default) => Task.CompletedTask;
public BsonDocument RunCommand(BsonDocument command, CancellationToken cancellationToken = default) => new();
public T RunCommand<T>(BsonDocument command, CancellationToken cancellationToken = default) => default!;
public Task<T> RunCommandAsync<T>(BsonDocument command, CancellationToken cancellationToken = default) => Task.FromResult(default(T)!);
public BsonDocument RunCommand(string command, CancellationToken cancellationToken = default) => new();
public T RunCommand<T>(string command, CancellationToken cancellationToken = default) => default!;
public Task<T> RunCommandAsync<T>(string command, CancellationToken cancellationToken = default) => Task.FromResult(default(T)!);
}
public class MongoCollectionSettings { }
@@ -59,8 +88,10 @@ namespace MongoDB.Driver
public interface IMongoCollection<TDocument>
{
Task InsertOneAsync(TDocument document, InsertOneOptions? options = null, CancellationToken cancellationToken = default);
Task InsertManyAsync(IEnumerable<TDocument> documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default);
Task<ReplaceOneResult> ReplaceOneAsync(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default);
Task<DeleteResult> DeleteOneAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default);
Task<DeleteResult> DeleteManyAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default);
Task<IAsyncCursor<TDocument>> FindAsync(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null, CancellationToken cancellationToken = default);
IFindFluent<TDocument, TDocument> Find(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null);
Task<long> CountDocumentsAsync(FilterDefinition<TDocument> filter, CountOptions? options = null, CancellationToken cancellationToken = default);
@@ -88,6 +119,12 @@ namespace MongoDB.Driver
return Task.CompletedTask;
}
public Task InsertManyAsync(IEnumerable<TDocument> documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default)
{
_docs.AddRange(documents);
return Task.CompletedTask;
}
public Task<ReplaceOneResult> ReplaceOneAsync(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default)
{
_docs.Clear();
@@ -102,6 +139,13 @@ namespace MongoDB.Driver
return Task.FromResult(new DeleteResult(removed ? 1 : 0));
}
public Task<DeleteResult> DeleteManyAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default)
{
var removed = _docs.Count;
_docs.Clear();
return Task.FromResult(new DeleteResult(removed));
}
public Task<IAsyncCursor<TDocument>> FindAsync(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null, CancellationToken cancellationToken = default)
=> Task.FromResult<IAsyncCursor<TDocument>>(new AsyncCursor<TDocument>(_docs));
@@ -212,7 +256,10 @@ namespace MongoDB.Driver
=> new FindFluentProjected<TDocument, TNewProjection>(Enumerable.Empty<TNewProjection>());
}
public class FilterDefinition<TDocument> { }
public class FilterDefinition<TDocument>
{
public static FilterDefinition<TDocument> Empty { get; } = new();
}
public class UpdateDefinition<TDocument> { }
public class ProjectionDefinition<TDocument, TProjection> { }
public class SortDefinition<TDocument> { }
@@ -222,6 +269,7 @@ namespace MongoDB.Driver
public class FindOneAndReplaceOptions<TDocument, TProjection> { public bool IsUpsert { get; set; } }
public class FindOneAndUpdateOptions<TDocument, TProjection> { public bool IsUpsert { get; set; } }
public class InsertOneOptions { }
public class InsertManyOptions { }
public class CreateIndexOptions { }
public class IndexKeysDefinition<TDocument> { }
@@ -284,7 +332,7 @@ namespace Mongo2Go
private MongoDbRunner(string connectionString) => ConnectionString = connectionString;
public static MongoDbRunner Start() => new("mongodb://localhost:27017/fake");
public static MongoDbRunner Start(bool singleNodeReplSet = false) => new("mongodb://localhost:27017/fake");
public void Dispose()
{

View File

@@ -1,19 +1,27 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Storage.Mongo
{
public static class MongoStorageDefaults
{
public const string DefaultDatabaseName = "concelier";
public static class Collections
{
public const string AdvisoryStatements = "advisory_statements";
public const string AdvisoryRaw = "advisory_raw";
public const string Advisory = "advisory";
public const string AdvisoryObservations = "advisory_observations";
public const string AdvisoryLinksets = "advisory_linksets";
public const string Alias = "aliases";
public const string Dto = "dto";
public const string MergeEvent = "merge_events";
public const string Document = "documents";
public const string PsirtFlags = "psirt_flags";
}
}
@@ -64,13 +72,32 @@ namespace StellaOps.Concelier.Storage.Mongo
this.FetchedAt = FetchedAt ?? CreatedAt;
}
public DocumentRecord(
Guid Id,
string SourceName,
string Uri,
string Sha256,
string Status = "pending_parse",
string? ContentType = null,
IReadOnlyDictionary<string, string>? Headers = null,
IReadOnlyDictionary<string, string>? Metadata = null,
string? Etag = null,
DateTimeOffset? LastModified = null,
Guid? PayloadId = null,
DateTimeOffset? ExpiresAt = null,
byte[]? Payload = null,
DateTimeOffset? FetchedAt = null)
: this(Id, SourceName, Uri, DateTimeOffset.UtcNow, Sha256, Status, ContentType, Headers, Metadata, Etag, LastModified, PayloadId, ExpiresAt, Payload, FetchedAt)
{
}
public Guid Id { get; init; }
public string SourceName { get; init; }
public string Uri { get; init; }
public string SourceName { get; init; } = string.Empty;
public string Uri { get; init; } = string.Empty;
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset FetchedAt { get; init; }
public string Sha256 { get; init; }
public string Status { get; init; }
public string Sha256 { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public string? ContentType { get; init; }
public IReadOnlyDictionary<string, string>? Headers { get; init; }
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
@@ -81,37 +108,37 @@ namespace StellaOps.Concelier.Storage.Mongo
public byte[]? Payload { get; init; }
}
public interface IDocumentStore
{
Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken);
Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken);
Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken);
Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken);
}
public interface IDocumentStore
{
Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken);
Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken);
Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken);
Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken);
}
public class InMemoryDocumentStore : IDocumentStore
{
private readonly ConcurrentDictionary<(string Source, string Uri), DocumentRecord> _records = new();
private readonly ConcurrentDictionary<Guid, DocumentRecord> _byId = new();
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
{
_records.TryGetValue((sourceName, uri), out var record);
return Task.FromResult<DocumentRecord?>(record);
}
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
{
_records.TryGetValue((sourceName, uri), out var record);
return Task.FromResult<DocumentRecord?>(record);
}
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
{
_byId.TryGetValue(id, out var record);
return Task.FromResult<DocumentRecord?>(record);
}
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
{
_byId.TryGetValue(id, out var record);
return Task.FromResult<DocumentRecord?>(record);
}
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
{
_records[(record.SourceName, record.Uri)] = record;
_byId[record.Id] = record;
return Task.FromResult(record);
}
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
{
_records[(record.SourceName, record.Uri)] = record;
_byId[record.Id] = record;
return Task.FromResult(record);
}
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
{
@@ -129,6 +156,22 @@ public interface IDocumentStore
{
private readonly InMemoryDocumentStore _inner = new();
public DocumentStore()
{
}
public DocumentStore(object? database, MongoStorageOptions? options)
{
}
public DocumentStore(object? database, object? logger)
{
}
public DocumentStore(object? database, MongoStorageOptions? options, object? logger)
{
}
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
=> _inner.FindBySourceAndUriAsync(sourceName, uri, cancellationToken);
@@ -142,47 +185,70 @@ public interface IDocumentStore
=> _inner.UpdateStatusAsync(id, status, cancellationToken);
}
public record DtoRecord(
Guid Id,
Guid DocumentId,
string SourceName,
string Format,
MongoDB.Bson.BsonDocument Payload,
DateTimeOffset CreatedAt)
public record DtoRecord
{
public DtoRecord(
Guid Id,
Guid DocumentId,
string SourceName,
string Format,
MongoDB.Bson.BsonDocument Payload,
DateTimeOffset CreatedAt,
string? SchemaVersion = null,
DateTimeOffset? ValidatedAt = null)
{
this.Id = Id;
this.DocumentId = DocumentId;
this.SourceName = SourceName;
this.Format = Format;
this.Payload = Payload;
this.CreatedAt = CreatedAt;
this.SchemaVersion = SchemaVersion ?? string.Empty;
this.ValidatedAt = ValidatedAt ?? CreatedAt;
}
public Guid Id { get; init; }
public Guid DocumentId { get; init; }
public string SourceName { get; init; } = string.Empty;
public string Format { get; init; } = string.Empty;
public MongoDB.Bson.BsonDocument Payload { get; init; } = new();
public DateTimeOffset CreatedAt { get; init; }
public string SchemaVersion { get; init; } = string.Empty;
public DateTimeOffset ValidatedAt { get; init; } = CreatedAt;
public DateTimeOffset ValidatedAt { get; init; }
}
public interface IDtoStore
{
Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken);
Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken);
Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, CancellationToken cancellationToken);
}
public class InMemoryDtoStore : IDtoStore
{
private readonly ConcurrentDictionary<Guid, DtoRecord> _records = new();
public Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken)
public interface IDtoStore
{
_records[record.DocumentId] = record;
return Task.FromResult(record);
Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken);
Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken);
Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, int limit, CancellationToken cancellationToken);
}
public Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken)
public class InMemoryDtoStore : IDtoStore
{
_records.TryGetValue(documentId, out var record);
return Task.FromResult<DtoRecord?>(record);
}
private readonly ConcurrentDictionary<Guid, DtoRecord> _records = new();
public Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, CancellationToken cancellationToken)
{
var matches = _records.Values.Where(r => string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase)).ToArray();
return Task.FromResult<IReadOnlyList<DtoRecord>>(matches);
public Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken)
{
_records[record.DocumentId] = record;
return Task.FromResult(record);
}
public Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken)
{
_records.TryGetValue(documentId, out var record);
return Task.FromResult<DtoRecord?>(record);
}
public Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, int limit, CancellationToken cancellationToken)
{
var matches = _records.Values
.Where(r => string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase))
.Take(limit)
.ToArray();
return Task.FromResult<IReadOnlyList<DtoRecord>>(matches);
}
}
}
internal sealed class RawDocumentStorage
{
@@ -251,7 +317,7 @@ public sealed record SourceStateRecord(
sourceName,
Enabled: current?.Enabled ?? true,
Paused: current?.Paused ?? false,
Cursor: cursor.DeepClone(),
Cursor: cursor.DeepClone().AsBsonDocument,
LastSuccess: completedAt,
LastFailure: current?.LastFailure,
FailCount: current?.FailCount ?? 0,
@@ -288,6 +354,18 @@ public sealed record SourceStateRecord(
{
private readonly InMemorySourceStateRepository _inner = new();
public MongoSourceStateRepository()
{
}
public MongoSourceStateRepository(object? database, MongoStorageOptions? options)
{
}
public MongoSourceStateRepository(object? database, object? logger)
{
}
public Task<SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken)
=> _inner.TryGetAsync(sourceName, cancellationToken);
@@ -304,6 +382,15 @@ public sealed record SourceStateRecord(
namespace StellaOps.Concelier.Storage.Mongo.Advisories
{
public sealed class AdvisoryDocument
{
public string AdvisoryKey { get; set; } = string.Empty;
public MongoDB.Bson.BsonDocument Payload { get; set; } = new();
public DateTime? Modified { get; set; }
public DateTime? Published { get; set; }
public DateTime? CreatedAt { get; set; }
}
public interface IAdvisoryStore
{
Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken);
@@ -360,18 +447,49 @@ namespace StellaOps.Concelier.Storage.Mongo.Aliases
public sealed record AliasEntry(string Scheme, string Value);
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value, DateTimeOffset? UpdatedAt = null);
public sealed record AliasCollision(string Scheme, string Value, IReadOnlyList<string> AdvisoryKeys);
public sealed record AliasUpsertResult(string AdvisoryKey, IReadOnlyList<AliasCollision> Collisions);
public interface IAliasStore
{
Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken);
Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken);
Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> entries, DateTimeOffset updatedAt, CancellationToken cancellationToken);
}
public sealed class InMemoryAliasStore : IAliasStore
public sealed class AliasStore : InMemoryAliasStore
{
public AliasStore()
{
}
public AliasStore(object? database, object? options)
{
}
}
public class InMemoryAliasStore : IAliasStore
{
private readonly ConcurrentDictionary<string, List<AliasRecord>> _byAdvisory = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<(string Scheme, string Value), List<AliasRecord>> _byAlias = new();
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> entries, DateTimeOffset updatedAt, CancellationToken cancellationToken)
{
var records = entries.Select(e => new AliasRecord(advisoryKey, e.Scheme, e.Value, updatedAt)).ToList();
_byAdvisory[advisoryKey] = records;
foreach (var record in records)
{
var list = _byAlias.GetOrAdd((record.Scheme, record.Value), _ => new List<AliasRecord>());
list.RemoveAll(r => string.Equals(r.AdvisoryKey, advisoryKey, StringComparison.OrdinalIgnoreCase));
list.Add(record);
}
var collisions = _byAlias.Values
.Where(list => list.Count > 1)
.Select(list => new AliasCollision(list[0].Scheme, list[0].Value, list.Select(r => r.AdvisoryKey).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()))
.ToArray();
return Task.FromResult(new AliasUpsertResult(advisoryKey, collisions));
}
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
{
_byAdvisory.TryGetValue(advisoryKey, out var records);
@@ -400,11 +518,16 @@ namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory
string Snapshot,
string PreviousSnapshot,
IReadOnlyList<ChangeHistoryFieldChange> Changes,
DateTimeOffset CreatedAt);
DateTimeOffset CreatedAt)
{
public string? PreviousHash => PreviousSnapshotHash;
public string? CurrentHash => SnapshotHash;
}
public interface IChangeHistoryStore
{
Task AddAsync(ChangeHistoryRecord record, CancellationToken cancellationToken);
Task<IReadOnlyList<ChangeHistoryRecord>> GetRecentAsync(string sourceName, string advisoryKey, int limit, CancellationToken cancellationToken);
}
public sealed class InMemoryChangeHistoryStore : IChangeHistoryStore
@@ -415,6 +538,18 @@ namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory
_records.Add(record);
return Task.CompletedTask;
}
public Task<IReadOnlyList<ChangeHistoryRecord>> GetRecentAsync(string sourceName, string advisoryKey, int limit, CancellationToken cancellationToken)
{
var matches = _records
.Where(r =>
string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(r.AdvisoryKey, advisoryKey, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(r => r.CreatedAt)
.Take(limit)
.ToArray();
return Task.FromResult<IReadOnlyList<ChangeHistoryRecord>>(matches);
}
}
}
@@ -597,6 +732,25 @@ namespace StellaOps.Concelier.Storage.Mongo.MergeEvents
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(records);
}
}
public sealed class MergeEventStore : IMergeEventStore
{
private readonly InMemoryMergeEventStore _inner = new();
public MergeEventStore()
{
}
public MergeEventStore(object? database, object? logger)
{
}
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
=> _inner.AppendAsync(record, cancellationToken);
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
=> _inner.GetRecentAsync(advisoryKey, limit, cancellationToken);
}
}
namespace StellaOps.Concelier.Storage.Mongo.Documents
@@ -617,12 +771,16 @@ namespace StellaOps.Concelier.Storage.Mongo.Dtos
namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags
{
public sealed record PsirtFlagRecord(string AdvisoryId, string Vendor, string SourceName, string? ExternalId, DateTimeOffset RecordedAt);
public sealed record PsirtFlagRecord(string AdvisoryId, string Vendor, string SourceName, string? ExternalId, DateTimeOffset RecordedAt)
{
public string AdvisoryKey => AdvisoryId;
}
public interface IPsirtFlagStore
{
Task UpsertAsync(PsirtFlagRecord flag, CancellationToken cancellationToken);
Task<IReadOnlyList<PsirtFlagRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken);
Task<PsirtFlagRecord?> FindAsync(string advisoryKey, CancellationToken cancellationToken);
}
public sealed class InMemoryPsirtFlagStore : IPsirtFlagStore
@@ -645,6 +803,94 @@ namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags
return Task.FromResult<IReadOnlyList<PsirtFlagRecord>>(records);
}
public Task<PsirtFlagRecord?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
_records.TryGetValue(advisoryKey, out var flag);
return Task.FromResult<PsirtFlagRecord?>(flag);
}
}
}
namespace StellaOps.Concelier.Storage.Mongo.Observations
{
public sealed class AdvisoryObservationDocument
{
public string Id { get; set; } = string.Empty;
public string Tenant { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public AdvisoryObservationSourceDocument Source { get; set; } = new();
public AdvisoryObservationUpstreamDocument Upstream { get; set; } = new();
public AdvisoryObservationContentDocument Content { get; set; } = new();
public AdvisoryObservationLinksetDocument Linkset { get; set; } = new();
public IDictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
}
public sealed class AdvisoryObservationSourceDocument
{
public string Vendor { get; set; } = string.Empty;
public string Stream { get; set; } = string.Empty;
public string Api { get; set; } = string.Empty;
}
public sealed class AdvisoryObservationUpstreamDocument
{
public string UpstreamId { get; set; } = string.Empty;
public string? DocumentVersion { get; set; }
public DateTime FetchedAt { get; set; }
public DateTime ReceivedAt { get; set; }
public string ContentHash { get; set; } = string.Empty;
public AdvisoryObservationSignatureDocument Signature { get; set; } = new();
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
}
public sealed class AdvisoryObservationSignatureDocument
{
public bool Present { get; set; }
public string? Format { get; set; }
public string? KeyId { get; set; }
public string? Signature { get; set; }
}
public sealed class AdvisoryObservationContentDocument
{
public string Format { get; set; } = string.Empty;
public string SpecVersion { get; set; } = string.Empty;
public BsonDocument Raw { get; set; } = new();
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
}
public sealed class AdvisoryObservationLinksetDocument
{
public List<string>? Aliases { get; set; }
public List<string>? Purls { get; set; }
public List<string>? Cpes { get; set; }
public List<AdvisoryObservationReferenceDocument> References { get; set; } = new();
}
public sealed class AdvisoryObservationReferenceDocument
{
public string Type { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
}
namespace StellaOps.Concelier.Storage.Mongo.Linksets
{
public sealed class AdvisoryLinksetDocument
{
public string TenantId { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string AdvisoryId { get; set; } = string.Empty;
public IReadOnlyList<string> Observations { get; set; } = Array.Empty<string>();
public DateTime CreatedAt { get; set; }
public AdvisoryLinksetNormalizedDocument Normalized { get; set; } = new();
}
public sealed class AdvisoryLinksetNormalizedDocument
{
public IReadOnlyList<string> Purls { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> Versions { get; set; } = Array.Empty<string>();
}
}

View File

@@ -88,8 +88,10 @@ public sealed class CertCcMapperTests
Id: Guid.NewGuid(),
DocumentId: document.Id,
SourceName: "cert-cc",
Format: "certcc.vince.note.v1",
SchemaVersion: "certcc.vince.note.v1",
Payload: new BsonDocument(),
CreatedAt: PublishedAt,
ValidatedAt: PublishedAt.AddMinutes(1));
var advisory = CertCcMapper.Map(dto, document, dtoRecord, "cert-cc");

View File

@@ -190,11 +190,11 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(refreshedRecord);
Assert.Equal(documentId, refreshedRecord!.Id);
Assert.NotNull(refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId, refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId?.ToString(), refreshedRecord.PayloadId?.ToString());
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
Assert.Single(files);
Assert.NotEqual(previousGridId, files[0]["_id"].AsObjectId);
Assert.NotEqual(previousGridId?.ToString(), files[0]["_id"].AsObjectId.ToString());
}
private SourceStateSeedProcessor CreateProcessor()

View File

@@ -34,7 +34,7 @@ public sealed class SuseMapperTests
},
Etag: "adv-1",
LastModified: DateTimeOffset.UtcNow,
PayloadId: ObjectId.Empty);
PayloadId: Guid.Empty);
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);

View File

@@ -97,8 +97,10 @@ public sealed class OsvConflictFixtureTests
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
DocumentId: document.Id,
SourceName: OsvConnectorPlugin.SourceName,
Format: "osv.v1",
SchemaVersion: "osv.v1",
Payload: new BsonDocument("id", dto.Id),
CreatedAt: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
ValidatedAt: new DateTimeOffset(2025, 3, 6, 12, 5, 0, TimeSpan.Zero));
var advisory = OsvMapper.Map(dto, document, dtoRecord, "npm");

View File

@@ -65,7 +65,7 @@ public sealed class RuBduMapperTests
null,
null,
dto.IdentifyDate,
ObjectId.GenerateNewId());
PayloadId: Guid.NewGuid());
var advisory = RuBduMapper.Map(dto, document, dto.IdentifyDate!.Value);

View File

@@ -56,7 +56,7 @@ public sealed class RuNkckiMapperTests
null,
null,
dto.DateUpdated,
ObjectId.GenerateNewId());
PayloadId: Guid.NewGuid());
Assert.Equal("КРИТИЧЕСКИЙ", dto.CvssRating);
var normalizeSeverity = typeof(RuNkckiMapper).GetMethod("NormalizeSeverity", BindingFlags.NonPublic | BindingFlags.Static)!;

View File

@@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Collections.Immutable;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Exporter.Json;
@@ -15,15 +14,16 @@ using StellaOps.Concelier.Storage.Mongo.Exporting;
using StellaOps.Concelier.Models;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
namespace StellaOps.Concelier.Exporter.Json.Tests;
public sealed class JsonExporterDependencyInjectionRoutineTests
{
[Fact]
public void Register_AddsJobDefinitionAndServices()
{
var services = new ServiceCollection();
using StellaOps.Provenance.Mongo;
namespace StellaOps.Concelier.Exporter.Json.Tests;
public sealed class JsonExporterDependencyInjectionRoutineTests
{
[Fact]
public void Register_AddsJobDefinitionAndServices()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IAdvisoryStore, StubAdvisoryStore>();
services.AddSingleton<IExportStateStore, StubExportStateStore>();
@@ -32,64 +32,60 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
services.AddOptions<JobSchedulerOptions>();
services.Configure<CryptoHashOptions>(_ => { });
services.AddStellaOpsCrypto();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var routine = new JsonExporterDependencyInjectionRoutine();
routine.Register(services, configuration);
using var provider = services.BuildServiceProvider();
var optionsAccessor = provider.GetRequiredService<IOptions<JobSchedulerOptions>>();
var options = optionsAccessor.Value;
Assert.True(options.Definitions.TryGetValue(JsonExportJob.JobKind, out var definition));
Assert.Equal(typeof(JsonExportJob), definition.JobType);
Assert.True(definition.Enabled);
var exporter = provider.GetRequiredService<JsonFeedExporter>();
Assert.NotNull(exporter);
}
private sealed class StubAdvisoryStore : IAdvisoryStore
{
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var routine = new JsonExporterDependencyInjectionRoutine();
routine.Register(services, configuration);
using var provider = services.BuildServiceProvider();
var optionsAccessor = provider.GetRequiredService<IOptions<JobSchedulerOptions>>();
var options = optionsAccessor.Value;
Assert.True(options.Definitions.TryGetValue(JsonExportJob.JobKind, out var definition));
Assert.Equal(typeof(JsonExportJob), definition.JobType);
Assert.True(definition.Enabled);
var exporter = provider.GetRequiredService<JsonFeedExporter>();
Assert.NotNull(exporter);
}
private sealed class StubAdvisoryStore : IAdvisoryStore
{
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
{
_ = session;
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
_ = session;
return Task.FromResult<Advisory?>(null);
}
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
{
_ = session;
return Task.CompletedTask;
}
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
{
_ = session;
return Enumerate(cancellationToken);
static async IAsyncEnumerable<Advisory> Enumerate([EnumeratorCancellation] CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await Task.Yield();
yield break;
}
}
}
ct.ThrowIfCancellationRequested();
await Task.Yield();
yield break;
}
}
}
private sealed class StubExportStateStore : IExportStateStore
{
private ExportStateRecord? _record;
public Task<ExportStateRecord?> FindAsync(string id, CancellationToken cancellationToken)
{
return Task.FromResult(_record);
@@ -107,6 +103,9 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
=> throw new NotSupportedException();
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
{
return ValueTask.FromResult(new AdvisoryReplay(

View File

@@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Exporter.TrivyDb;
using StellaOps.Concelier.Models;
@@ -883,27 +882,23 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
_advisories = advisories;
}
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
{
_ = session;
return Task.FromResult(_advisories);
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
_ = session;
return Task.FromResult<Advisory?>(_advisories.FirstOrDefault(a => a.AdvisoryKey == advisoryKey));
}
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
{
_ = session;
return Task.CompletedTask;
}
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
{
_ = session;
return EnumerateAsync(cancellationToken);
async IAsyncEnumerable<Advisory> EnumerateAsync([EnumeratorCancellation] CancellationToken ct)

View File

@@ -2,109 +2,109 @@ using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using StellaOps.Concelier.Core;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Aliases;
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
namespace StellaOps.Concelier.Merge.Tests;
public sealed class AdvisoryMergeServiceTests
{
[Fact]
public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
{
var aliasStore = new FakeAliasStore();
aliasStore.Register("GHSA-aaaa-bbbb-cccc",
(AliasSchemes.Ghsa, "GHSA-aaaa-bbbb-cccc"),
(AliasSchemes.Cve, "CVE-2025-4242"));
aliasStore.Register("CVE-2025-4242",
(AliasSchemes.Cve, "CVE-2025-4242"));
aliasStore.Register("OSV-2025-xyz",
(AliasSchemes.OsV, "OSV-2025-xyz"),
(AliasSchemes.Cve, "CVE-2025-4242"));
var advisoryStore = new FakeAdvisoryStore();
advisoryStore.Seed(CreateGhsaAdvisory(), CreateNvdAdvisory(), CreateOsvAdvisory());
var mergeEventStore = new InMemoryMergeEventStore();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 4, 1, 0, 0, 0, TimeSpan.Zero));
var writer = new MergeEventWriter(mergeEventStore, new CanonicalHashCalculator(), timeProvider, NullLogger<MergeEventWriter>.Instance);
var precedenceMerger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
var aliasResolver = new AliasGraphResolver(aliasStore);
var canonicalMerger = new CanonicalMerger(timeProvider);
var eventLog = new RecordingAdvisoryEventLog();
var service = new AdvisoryMergeService(aliasResolver, advisoryStore, precedenceMerger, writer, canonicalMerger, eventLog, timeProvider, NullLogger<AdvisoryMergeService>.Instance);
var result = await service.MergeAsync("GHSA-aaaa-bbbb-cccc", CancellationToken.None);
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Core;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Aliases;
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
using StellaOps.Provenance.Mongo;
namespace StellaOps.Concelier.Merge.Tests;
public sealed class AdvisoryMergeServiceTests
{
[Fact]
public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
{
var aliasStore = new FakeAliasStore();
aliasStore.Register("GHSA-aaaa-bbbb-cccc",
(AliasSchemes.Ghsa, "GHSA-aaaa-bbbb-cccc"),
(AliasSchemes.Cve, "CVE-2025-4242"));
aliasStore.Register("CVE-2025-4242",
(AliasSchemes.Cve, "CVE-2025-4242"));
aliasStore.Register("OSV-2025-xyz",
(AliasSchemes.OsV, "OSV-2025-xyz"),
(AliasSchemes.Cve, "CVE-2025-4242"));
var advisoryStore = new FakeAdvisoryStore();
advisoryStore.Seed(CreateGhsaAdvisory(), CreateNvdAdvisory(), CreateOsvAdvisory());
var mergeEventStore = new InMemoryMergeEventStore();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 4, 1, 0, 0, 0, TimeSpan.Zero));
var writer = new MergeEventWriter(mergeEventStore, new CanonicalHashCalculator(), timeProvider, NullLogger<MergeEventWriter>.Instance);
var precedenceMerger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
var aliasResolver = new AliasGraphResolver(aliasStore);
var canonicalMerger = new CanonicalMerger(timeProvider);
var eventLog = new RecordingAdvisoryEventLog();
var service = new AdvisoryMergeService(aliasResolver, advisoryStore, precedenceMerger, writer, canonicalMerger, eventLog, timeProvider, NullLogger<AdvisoryMergeService>.Instance);
var result = await service.MergeAsync("GHSA-aaaa-bbbb-cccc", CancellationToken.None);
Assert.NotNull(result.Merged);
Assert.Equal("OSV summary overrides", result.Merged!.Summary);
Assert.Empty(result.Conflicts);
var upserted = advisoryStore.LastUpserted;
Assert.NotNull(upserted);
Assert.Equal("CVE-2025-4242", upserted!.AdvisoryKey);
Assert.Equal("OSV summary overrides", upserted.Summary);
var mergeRecord = mergeEventStore.LastRecord;
Assert.NotNull(mergeRecord);
var summaryDecision = Assert.Single(mergeRecord!.FieldDecisions, decision => decision.Field == "summary");
Assert.Equal("osv", summaryDecision.SelectedSource);
Assert.Equal("freshness_override", summaryDecision.DecisionReason);
var appendRequest = eventLog.LastRequest;
Assert.NotNull(appendRequest);
Assert.Contains(appendRequest!.Statements, statement => string.Equals(statement.Advisory.AdvisoryKey, "CVE-2025-4242", StringComparison.OrdinalIgnoreCase));
Assert.True(appendRequest.Conflicts is null || appendRequest.Conflicts.Count == 0);
}
private static Advisory CreateGhsaAdvisory()
{
var recorded = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-aaaa-bbbb-cccc", recorded, new[] { ProvenanceFieldMasks.Advisory });
return new Advisory(
"GHSA-aaaa-bbbb-cccc",
"Container escape",
"Initial GHSA summary.",
"en",
recorded,
recorded,
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-4242", "GHSA-aaaa-bbbb-cccc" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
private static Advisory CreateNvdAdvisory()
{
var recorded = DateTimeOffset.Parse("2025-03-02T00:00:00Z");
var provenance = new AdvisoryProvenance("nvd", "map", "CVE-2025-4242", recorded, new[] { ProvenanceFieldMasks.Advisory });
return new Advisory(
"CVE-2025-4242",
"CVE-2025-4242",
"Baseline NVD summary.",
"en",
recorded,
recorded,
"high",
exploitKnown: false,
aliases: new[] { "CVE-2025-4242" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
var upserted = advisoryStore.LastUpserted;
Assert.NotNull(upserted);
Assert.Equal("CVE-2025-4242", upserted!.AdvisoryKey);
Assert.Equal("OSV summary overrides", upserted.Summary);
var mergeRecord = mergeEventStore.LastRecord;
Assert.NotNull(mergeRecord);
var summaryDecision = Assert.Single(mergeRecord!.FieldDecisions, decision => decision.Field == "summary");
Assert.Equal("osv", summaryDecision.SelectedSource);
Assert.Equal("freshness_override", summaryDecision.DecisionReason);
var appendRequest = eventLog.LastRequest;
Assert.NotNull(appendRequest);
Assert.Contains(appendRequest!.Statements, statement => string.Equals(statement.Advisory.AdvisoryKey, "CVE-2025-4242", StringComparison.OrdinalIgnoreCase));
Assert.True(appendRequest.Conflicts is null || appendRequest.Conflicts.Count == 0);
}
private static Advisory CreateGhsaAdvisory()
{
var recorded = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-aaaa-bbbb-cccc", recorded, new[] { ProvenanceFieldMasks.Advisory });
return new Advisory(
"GHSA-aaaa-bbbb-cccc",
"Container escape",
"Initial GHSA summary.",
"en",
recorded,
recorded,
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-4242", "GHSA-aaaa-bbbb-cccc" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
private static Advisory CreateNvdAdvisory()
{
var recorded = DateTimeOffset.Parse("2025-03-02T00:00:00Z");
var provenance = new AdvisoryProvenance("nvd", "map", "CVE-2025-4242", recorded, new[] { ProvenanceFieldMasks.Advisory });
return new Advisory(
"CVE-2025-4242",
"CVE-2025-4242",
"Baseline NVD summary.",
"en",
recorded,
recorded,
"high",
exploitKnown: false,
aliases: new[] { "CVE-2025-4242" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
private static Advisory CreateOsvAdvisory()
{
var recorded = DateTimeOffset.Parse("2025-03-05T12:00:00Z");
@@ -207,120 +207,119 @@ public sealed class AdvisoryMergeServiceTests
Assert.Equal(conflict.ConflictId, appendedConflict.ConflictId);
Assert.Equal(conflict.StatementIds, appendedConflict.StatementIds.ToImmutableArray());
}
private sealed class RecordingAdvisoryEventLog : IAdvisoryEventLog
{
public AdvisoryEventAppendRequest? LastRequest { get; private set; }
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
{
LastRequest = request;
return ValueTask.CompletedTask;
}
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
}
private sealed class FakeAliasStore : IAliasStore
{
private readonly ConcurrentDictionary<string, List<AliasRecord>> _records = new(StringComparer.OrdinalIgnoreCase);
public void Register(string advisoryKey, params (string Scheme, string Value)[] aliases)
{
var list = new List<AliasRecord>();
foreach (var (scheme, value) in aliases)
{
list.Add(new AliasRecord(advisoryKey, scheme, value, DateTimeOffset.UtcNow));
}
_records[advisoryKey] = list;
}
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> aliases, DateTimeOffset updatedAt, CancellationToken cancellationToken)
{
return Task.FromResult(new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>()));
}
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
{
var matches = _records.Values
.SelectMany(static records => records)
.Where(record => string.Equals(record.Scheme, scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(record.Value, value, StringComparison.OrdinalIgnoreCase))
.ToList();
return Task.FromResult<IReadOnlyList<AliasRecord>>(matches);
}
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
{
if (_records.TryGetValue(advisoryKey, out var records))
{
return Task.FromResult<IReadOnlyList<AliasRecord>>(records);
}
return Task.FromResult<IReadOnlyList<AliasRecord>>(Array.Empty<AliasRecord>());
}
}
private sealed class FakeAdvisoryStore : IAdvisoryStore
{
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
public Advisory? LastUpserted { get; private set; }
public void Seed(params Advisory[] advisories)
{
foreach (var advisory in advisories)
{
_advisories[advisory.AdvisoryKey] = advisory;
}
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
{
_ = session;
_advisories.TryGetValue(advisoryKey, out var advisory);
return Task.FromResult(advisory);
}
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
{
_ = session;
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
}
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
{
_ = session;
_advisories[advisory.AdvisoryKey] = advisory;
LastUpserted = advisory;
return Task.CompletedTask;
}
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
{
_ = session;
return AsyncEnumerable.Empty<Advisory>();
}
}
private sealed class InMemoryMergeEventStore : IMergeEventStore
{
public MergeEventRecord? LastRecord { get; private set; }
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
{
LastRecord = record;
return Task.CompletedTask;
}
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
}
}
}
private sealed class RecordingAdvisoryEventLog : IAdvisoryEventLog
{
public AdvisoryEventAppendRequest? LastRequest { get; private set; }
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
{
LastRequest = request;
return ValueTask.CompletedTask;
}
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
}
private sealed class FakeAliasStore : IAliasStore
{
private readonly ConcurrentDictionary<string, List<AliasRecord>> _records = new(StringComparer.OrdinalIgnoreCase);
public void Register(string advisoryKey, params (string Scheme, string Value)[] aliases)
{
var list = new List<AliasRecord>();
foreach (var (scheme, value) in aliases)
{
list.Add(new AliasRecord(advisoryKey, scheme, value, DateTimeOffset.UtcNow));
}
_records[advisoryKey] = list;
}
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> aliases, DateTimeOffset updatedAt, CancellationToken cancellationToken)
{
return Task.FromResult(new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>()));
}
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
{
var matches = _records.Values
.SelectMany(static records => records)
.Where(record => string.Equals(record.Scheme, scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(record.Value, value, StringComparison.OrdinalIgnoreCase))
.ToList();
return Task.FromResult<IReadOnlyList<AliasRecord>>(matches);
}
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
{
if (_records.TryGetValue(advisoryKey, out var records))
{
return Task.FromResult<IReadOnlyList<AliasRecord>>(records);
}
return Task.FromResult<IReadOnlyList<AliasRecord>>(Array.Empty<AliasRecord>());
}
}
private sealed class FakeAdvisoryStore : IAdvisoryStore
{
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
public Advisory? LastUpserted { get; private set; }
public void Seed(params Advisory[] advisories)
{
foreach (var advisory in advisories)
{
_advisories[advisory.AdvisoryKey] = advisory;
}
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
_advisories.TryGetValue(advisoryKey, out var advisory);
return Task.FromResult(advisory);
}
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
}
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
{
_advisories[advisory.AdvisoryKey] = advisory;
LastUpserted = advisory;
return Task.CompletedTask;
}
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
{
return AsyncEnumerable.Empty<Advisory>();
}
}
private sealed class InMemoryMergeEventStore : IMergeEventStore
{
public MergeEventRecord? LastRecord { get; private set; }
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
{
LastRecord = record;
return Task.CompletedTask;
}
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
}
}
}

View File

@@ -12,8 +12,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
@@ -24,4 +24,4 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.RawModels/StellaOps.Concelier.RawModels.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
using OptionsFactory = Microsoft.Extensions.Options.Options;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
@@ -194,7 +195,7 @@ public sealed class AocVerifyRegressionTests
public void Verify_MapperGuardParity_ValidationResultsMatch()
{
var guard = new AocWriteGuard();
var validator = new AdvisorySchemaValidator(guard, Options.Create(GuardOptions));
var validator = new AdvisorySchemaValidator(guard, OptionsFactory.Create(GuardOptions));
// Create document with forbidden field
var json = CreateJsonWithForbiddenField("severity", "high");

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Text.Json;
using Microsoft.Extensions.Options;
using OptionsFactory = Microsoft.Extensions.Options.Options;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.RawModels;
@@ -43,7 +44,7 @@ public sealed class LargeBatchIngestTests
for (int i = 0; i < results1.Count; i++)
{
Assert.Equal(results1[i].IsValid, results2[i].IsValid);
Assert.Equal(results1[i].Violations.Count, results2[i].Violations.Count);
Assert.Equal(results1[i].Violations.Length, results2[i].Violations.Length);
}
}
@@ -63,8 +64,8 @@ public sealed class LargeBatchIngestTests
var violations1 = results1[i].Violations;
var violations2 = results2[i].Violations;
Assert.Equal(violations1.Count, violations2.Count);
for (int j = 0; j < violations1.Count; j++)
Assert.Equal(violations1.Length, violations2.Length);
for (int j = 0; j < violations1.Length; j++)
{
Assert.Equal(violations1[j].ErrorCode, violations2[j].ErrorCode);
Assert.Equal(violations1[j].Path, violations2[j].Path);
@@ -150,15 +151,15 @@ public sealed class LargeBatchIngestTests
// Same generation should produce same violation counts
var validCount1 = results1.Count(r => r.IsValid);
var validCount2 = results2.Count(r => r.IsValid);
var violationCount1 = results1.Sum(r => r.Violations.Count);
var violationCount2 = results2.Sum(r => r.Violations.Count);
var violationCount1 = results1.Sum(r => r.Violations.Length);
var violationCount2 = results2.Sum(r => r.Violations.Length);
Assert.Equal(validCount1, validCount2);
Assert.Equal(violationCount1, violationCount2);
}
private static AdvisorySchemaValidator CreateValidator()
=> new(new AocWriteGuard(), Options.Create(GuardOptions));
=> new(new AocWriteGuard(), OptionsFactory.Create(GuardOptions));
private static List<AdvisoryRawDocument> GenerateValidDocuments(int count)
{

View File

@@ -171,5 +171,27 @@ public sealed class AdvisoryChunkBuilderTests
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHash(data, purpose);
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHashHex(data, purpose);
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHashBase64(data, purpose);
public ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
=> ComputeHashAsync(stream, purpose, cancellationToken);
public ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
=> ComputeHashHexAsync(stream, purpose, cancellationToken);
public string GetAlgorithmForPurpose(string purpose) => purpose ?? "sha256";
public string GetHashPrefix(string purpose) => $"{(purpose ?? "sha256").ToLowerInvariant()}:";
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
{
var hash = ComputeHashHexForPurpose(data, purpose);
return $"{GetHashPrefix(purpose)}{hash}";
}
}
}