From 4b124fb05646d0c90bdc2cc8a2e206d30b8f43ed Mon Sep 17 00:00:00 2001 From: StellaOps Bot Date: Sun, 7 Dec 2025 22:51:01 +0200 Subject: [PATCH] update exportcenter_ii and fix Sm2AttestorTests --- .../SPRINT_0163_0001_0001_exportcenter_ii.md | 3 +- .../Signing/Sm2AttestorTests.cs | 4 +- .../MongoCompat/Bson.cs | 176 +++++++++++++++++- 3 files changed, 174 insertions(+), 9 deletions(-) diff --git a/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md b/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md index cc4c24cbc..aa2f66a5e 100644 --- a/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md +++ b/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md @@ -40,7 +40,7 @@ | 6 | EXPORT-OBS-54-001 | DONE | Depends on EXPORT-OBS-53-001. | Exporter Service · Provenance Guild | Produce DSSE attestations per export artifact/target; expose `/exports/{id}/attestation`; integrate with CLI verify path. | | 7 | EXPORT-OBS-54-002 | DONE | Depends on EXPORT-OBS-54-001 and PROV-OBS-53-003. | Exporter Service · Provenance Guild | Add promotion attestation assembly; include SBOM/VEX digests, Rekor proofs, DSSE envelopes for Offline Kit. | | 8 | EXPORT-OBS-55-001 | DONE | Depends on EXPORT-OBS-54-001. | Exporter Service · DevOps | Incident mode enhancements; emit incident activation events to timeline + notifier. | -| 9 | EXPORT-RISK-69-001 | TODO | Schema blockers resolved; AdvisoryAI evidence bundle schema available. | Exporter Service · Risk Bundle Export Guild | Add `risk-bundle` job handler with provider selection, manifest signing, audit logging. | +| 9 | EXPORT-RISK-69-001 | DONE | Schema blockers resolved; AdvisoryAI evidence bundle schema available. | Exporter Service · Risk Bundle Export Guild | Add `risk-bundle` job handler with provider selection, manifest signing, audit logging. | | 10 | EXPORT-RISK-69-002 | TODO | Depends on EXPORT-RISK-69-001. | Exporter Service · Risk Engine Guild | Enable simulation report exports with scored data + explainability snapshots. | | 11 | EXPORT-RISK-70-001 | TODO | Depends on EXPORT-RISK-69-002. | Exporter Service · DevOps | Integrate risk bundle builds into offline kit packaging with checksum verification. | | 12 | EXPORT-SVC-35-001 | TODO | Schema blockers resolved; EvidenceLocker bundle spec available. | Exporter Service | Bootstrap exporter service project, config, Postgres migrations for `export_profiles/runs/inputs/distributions` with tenant scoping + tests. | @@ -93,6 +93,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-07 | **EXPORT-RISK-69-001 DONE:** Implemented risk-bundle job handler with provider selection, manifest signing, and audit logging. Created `RiskBundle/` namespace with: `RiskBundleJobModels.cs` (RiskBundleJobSubmitRequest/Result, RiskBundleJobStatus enum, RiskBundleJobStatusDetail, RiskBundleProviderOverride, RiskBundleProviderResult, RiskBundleOutcomeSummary, RiskBundleAuditEvent, RiskBundleAvailableProvider, RiskBundleProvidersResponse), `IRiskBundleJobHandler` interface, `RiskBundleJobHandler` implementation with in-memory job store, provider selection (mandatory: cisa-kev; optional: nvd, osv, ghsa, epss), timeline audit event publishing, background job execution. Created `RiskBundleEndpoints.cs` with REST API: `GET /v1/risk-bundles/providers`, `POST /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs/{jobId}`, `POST /v1/risk-bundles/jobs/{jobId}/cancel`. Added telemetry metrics: `export_risk_bundle_jobs_submitted_total`, `export_risk_bundle_jobs_completed_total`, `export_risk_bundle_job_duration_seconds`. Build succeeded with 0 errors. | Implementer | | 2025-12-07 | **EXPORT-OBS-55-001 DONE:** Implemented incident mode enhancements for ExportCenter. Created `Incident/` namespace with: `ExportIncidentModels.cs` (severity levels Info→Emergency, status Active→Resolved→FalsePositive, types ExportFailure/LatencyDegradation/StorageCapacity/DependencyFailure/IntegrityIssue/SecurityIncident/ConfigurationError/RateLimiting), `ExportIncidentEvents.cs` (IncidentActivated/Updated/Escalated/Deescalated/Resolved events), `IExportIncidentManager` interface and `ExportIncidentManager` implementation with in-memory store. `IExportNotificationEmitter` interface with `LoggingNotificationEmitter` for timeline + notifier integration. Added `PublishIncidentEventAsync` to `IExportTimelinePublisher`. REST endpoints at `/v1/incidents/*`: GET status, GET active, GET recent, GET {id}, POST activate, PATCH {id} update, POST {id}/resolve. Added metrics: `export_incidents_activated_total`, `export_incidents_resolved_total`, `export_incidents_escalated_total`, `export_incidents_deescalated_total`, `export_notifications_emitted_total`, `export_incident_duration_seconds`. | Implementer | | 2025-12-07 | **EXPORT-OBS-54-002 DONE:** Implemented promotion attestation assembly for Offline Kit delivery. Created `PromotionAttestationModels.cs` with models for SBOM/VEX digest references, Rekor proof entries (with inclusion proofs), DSSE envelope references, promotion predicates. Created `IPromotionAttestationAssembler` interface and `PromotionAttestationAssembler` implementation that: builds in-toto statements with promotion predicates, computes root hash from all artifact digests, signs with DSSE PAE encoding, exports to portable gzipped tar bundles with deterministic timestamps, includes verification scripts. Created `PromotionAttestationEndpoints.cs` with REST endpoints: `POST /v1/promotions/attestations`, `GET /v1/promotions/attestations/{id}`, `GET /v1/promotions/{promotionId}/attestations`, `POST /v1/promotions/attestations/{id}/verify`, `GET /v1/promotions/attestations/{id}/bundle`. Bundle export includes promotion-assembly.json, promotion.dsse.json, rekor-proofs.ndjson, envelopes/, checksums.txt, verify-promotion.sh. | Implementer | | 2025-12-07 | **EXPORT-OBS-54-001 DONE:** Implemented DSSE attestation service for export artifacts. Created `Attestation/` namespace with `ExportAttestationModels.cs` (DSSE envelope, in-toto statement, predicates, subjects, verification info), `IExportAttestationService` interface, `ExportAttestationService` implementation. Created `IExportAttestationSigner` interface and `ExportAttestationSigner` implementing DSSE PAE (Pre-Authentication Encoding) per spec with ECDSA-P256-SHA256 signing. REST endpoints at `/v1/exports/{id}/attestation` (GET), `/v1/exports/attestations/{attestationId}` (GET), `/v1/exports/{id}/attestation/verify` (POST). Includes base64url encoding, key ID computation, public key PEM export for verification. | Implementer | diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Signing/Sm2AttestorTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Signing/Sm2AttestorTests.cs index 5d353cac8..eedb08b45 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Signing/Sm2AttestorTests.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Signing/Sm2AttestorTests.cs @@ -107,9 +107,9 @@ internal static class Sm2TestKeyFactory var curve = Org.BouncyCastle.Asn1.GM.GMNamedCurves.GetByName("SM2P256V1"); var domain = new Org.BouncyCastle.Crypto.Parameters.ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed()); var generator = new Org.BouncyCastle.Crypto.Generators.ECKeyPairGenerator("EC"); - generator.Init(new Org.BouncyCastle.Crypto.Generators.ECKeyGenerationParameters(domain, new Org.BouncyCastle.Security.SecureRandom())); + generator.Init(new Org.BouncyCastle.Crypto.Parameters.ECKeyGenerationParameters(domain, new Org.BouncyCastle.Security.SecureRandom())); var pair = generator.GenerateKeyPair(); - var privInfo = Org.BouncyCastle.Asn1.Pkcs.PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private); + var privInfo = Org.BouncyCastle.Pkcs.PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private); var pem = Convert.ToBase64String(privInfo.GetDerEncoded()); var path = System.IO.Path.GetTempFileName(); System.IO.File.WriteAllText(path, "-----BEGIN PRIVATE KEY-----\n" + pem + "\n-----END PRIVATE KEY-----\n"); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs index 6e0b4856e..c19cbaa58 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs @@ -1,9 +1,25 @@ using System.Collections; using System.Globalization; +using System.Linq; using System.Text.Json; namespace MongoDB.Bson { + public enum BsonType + { + Double, + String, + Document, + Array, + Binary, + ObjectId, + Boolean, + DateTime, + Null, + Int32, + Int64 + } + public class BsonValue : IEquatable { protected object? RawValue; @@ -13,16 +29,38 @@ namespace MongoDB.Bson RawValue = value; } - 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 virtual BsonType BsonType => RawValue switch + { + null => BsonType.Null, + BsonDocument => BsonType.Document, + BsonArray => BsonType.Array, + string => BsonType.String, + bool => BsonType.Boolean, + int => BsonType.Int32, + long => BsonType.Int64, + double or float or decimal => BsonType.Double, + DateTime or DateTimeOffset => BsonType.DateTime, + ObjectId => BsonType.ObjectId, + byte[] => BsonType.Binary, + Guid => BsonType.String, + _ => BsonType.String + }; + + public bool IsString => BsonType == BsonType.String; + public bool IsBoolean => BsonType == BsonType.Boolean; + public bool IsBsonDocument => BsonType == BsonType.Document; + public bool IsBsonArray => BsonType == BsonType.Array; + public bool IsBsonNull => BsonType == BsonType.Null; + public bool IsBsonDateTime => BsonType == BsonType.DateTime; + public bool IsInt32 => BsonType == BsonType.Int32; + public bool IsInt64 => BsonType == BsonType.Int64; public string AsString => RawValue switch { null => string.Empty, string s => s, Guid g => g.ToString(), + ObjectId o => o.ToString(), _ => Convert.ToString(RawValue, CultureInfo.InvariantCulture) ?? string.Empty }; @@ -44,6 +82,17 @@ namespace MongoDB.Bson _ => 0 }; + public int AsInt32 => ToInt32(); + + public long AsInt64 => RawValue switch + { + long l => l, + int i => i, + double d => (long)d, + string s when long.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var l) => l, + _ => 0L + }; + public Guid AsGuid => RawValue switch { Guid g => g, @@ -58,6 +107,24 @@ namespace MongoDB.Bson _ => ObjectId.Empty }; + public byte[]? AsByteArray => RawValue as byte[]; + + public DateTimeOffset AsDateTimeOffset => RawValue switch + { + DateTimeOffset dto => dto.ToUniversalTime(), + DateTime dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc), + string s when DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dto) => dto.ToUniversalTime(), + _ => DateTimeOffset.MinValue + }; + + public DateTime ToUniversalTime() => RawValue switch + { + DateTime dt => dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime(), + DateTimeOffset dto => dto.UtcDateTime, + string s when DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dto) => dto.UtcDateTime, + _ => DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc) + }; + public BsonDocument AsBsonDocument => RawValue as BsonDocument ?? (this as BsonDocument ?? new BsonDocument()); public BsonArray AsBsonArray => RawValue as BsonArray ?? (this as BsonArray ?? new BsonArray()); @@ -69,6 +136,8 @@ namespace MongoDB.Bson public override bool Equals(object? obj) => obj is BsonValue other && Equals(other); public override int GetHashCode() => RawValue?.GetHashCode() ?? 0; + public static BsonValue Create(object? value) => BsonDocument.ToBsonValue(value); + 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); @@ -76,6 +145,38 @@ namespace MongoDB.Bson 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 static implicit operator BsonValue(DateTime value) => new(value); + public static implicit operator BsonValue(byte[] value) => new(value); + } + + public sealed class BsonNull : BsonValue + { + public static BsonNull Value { get; } = new(); + + private BsonNull() + : base(null) + { + } + + public override BsonType BsonType => BsonType.Null; + } + + public sealed class BsonString : BsonValue + { + public BsonString(string value) : base(value) + { + } + } + + public sealed class BsonBinaryData : BsonValue + { + public BsonBinaryData(byte[] bytes) + : base(bytes) + { + Bytes = bytes ?? Array.Empty(); + } + + public byte[] Bytes { get; } } public sealed class BsonDocument : BsonValue, IDictionary @@ -97,6 +198,27 @@ namespace MongoDB.Bson } } + public BsonDocument(IEnumerable> values) + : this() + { + foreach (var kvp in values) + { + _values[kvp.Key] = kvp.Value ?? new BsonValue(); + } + } + + public BsonDocument(string key, BsonValue value) + : this() + { + Add(key, value); + } + + public BsonDocument(string key, object? value) + : this() + { + Add(key, value); + } + public int ElementCount => _values.Count; public BsonValue this[string key] @@ -117,6 +239,7 @@ namespace MongoDB.Bson public void Clear() => _values.Clear(); public bool Contains(KeyValuePair item) => _values.Contains(item); public bool ContainsKey(string key) => _values.ContainsKey(key); + public bool Contains(string key) => ContainsKey(key); public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary)_values).CopyTo(array, arrayIndex); public IEnumerator> GetEnumerator() => _values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator(); @@ -126,6 +249,9 @@ namespace MongoDB.Bson public BsonValue GetValue(string key) => _values[key]; + public BsonValue GetValue(string key, BsonValue defaultValue) + => _values.TryGetValue(key, out var value) ? value : defaultValue; + public BsonDocument DeepClone() { var copy = new BsonDocument(); @@ -177,7 +303,7 @@ namespace MongoDB.Bson return array; } - internal static BsonValue ToBsonValue(object? value) + public static BsonValue ToBsonValue(object? value) { return value switch { @@ -190,9 +316,11 @@ namespace MongoDB.Bson bool b => new BsonValue(b), double d => new BsonValue(d), float f => new BsonValue(f), + decimal dec => new BsonValue((double)dec), DateTime dt => new BsonValue(dt), DateTimeOffset dto => new BsonValue(dto), - IEnumerable enumerable => new BsonArray(enumerable.Select(ToBsonValue)), + byte[] bytes => new BsonBinaryData(bytes), + IEnumerable enumerable when value is not string => new BsonArray(enumerable.Cast().Select(ToBsonValue)), _ => new BsonValue(value) }; } @@ -216,6 +344,15 @@ namespace MongoDB.Bson _items.AddRange(items); } + public BsonArray(IEnumerable items) + : this() + { + foreach (var item in items) + { + Add(item); + } + } + public BsonValue this[int index] { get => _items[index]; @@ -227,6 +364,13 @@ namespace MongoDB.Bson public void Add(BsonValue item) => _items.Add(item ?? new BsonValue()); public void Add(object? item) => _items.Add(BsonDocument.ToBsonValue(item)); + public void AddRange(IEnumerable items) + { + foreach (var item in items) + { + Add(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); @@ -259,6 +403,26 @@ namespace MongoDB.Bson public override bool Equals(object? obj) => obj is ObjectId other && Equals(other); public override int GetHashCode() => _value?.GetHashCode(StringComparison.Ordinal) ?? 0; } + + public static class BsonTypeMapper + { + public static object? MapToDotNetValue(BsonValue value) + { + if (value is null) return null; + + return value.BsonType switch + { + BsonType.Document => value.AsBsonDocument.ToDictionary(static kvp => kvp.Key, static kvp => MapToDotNetValue(kvp.Value)), + BsonType.Array => value.AsBsonArray.Select(MapToDotNetValue).ToArray(), + BsonType.Null => null, + BsonType.Boolean => value.AsBoolean, + BsonType.Int32 => value.AsInt32, + BsonType.Int64 => value.AsInt64, + BsonType.DateTime => value.ToUniversalTime(), + _ => value.AsString + }; + } + } } namespace MongoDB.Bson.Serialization.Attributes