Refactor and update test projects, remove obsolete tests, and upgrade dependencies

- Deleted obsolete test files for SchedulerAuditService and SchedulerMongoSessionFactory.
- Removed unused TestDataFactory class.
- Updated project files for Mongo.Tests to remove references to deleted files.
- Upgraded BouncyCastle.Cryptography package to version 2.6.2 across multiple projects.
- Replaced Microsoft.Extensions.Http.Polly with Microsoft.Extensions.Http.Resilience in Zastava.Webhook project.
- Updated NetEscapades.Configuration.Yaml package to version 3.1.0 in Configuration library.
- Upgraded Pkcs11Interop package to version 5.1.2 in Cryptography libraries.
- Refactored Argon2idPasswordHasher to use BouncyCastle for hashing instead of Konscious.
- Updated JsonSchema.Net package to version 7.3.2 in Microservice project.
- Updated global.json to use .NET SDK version 10.0.101.
This commit is contained in:
master
2025-12-10 19:13:29 +02:00
parent a3c7fe5e88
commit b7059d523e
369 changed files with 11125 additions and 14245 deletions

View File

@@ -15,18 +15,17 @@ using StellaOps.Policy.Engine.BatchEvaluation;
using StellaOps.Policy.Engine.DependencyInjection;
using StellaOps.PolicyDsl;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Workers;
using StellaOps.Policy.Engine.Streaming;
using StellaOps.Policy.Engine.Telemetry;
using StellaOps.Policy.Engine.ConsoleSurface;
using StellaOps.AirGap.Policy;
using StellaOps.Policy.Engine.Orchestration;
using StellaOps.Policy.Engine.ReachabilityFacts;
using StellaOps.Policy.Engine.Storage.InMemory;
using StellaOps.Policy.Engine.Storage.Mongo.Repositories;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Receipts;
using StellaOps.Policy.Storage.Postgres;
using StellaOps.Policy.Engine.Workers;
using StellaOps.Policy.Engine.Streaming;
using StellaOps.Policy.Engine.Telemetry;
using StellaOps.Policy.Engine.ConsoleSurface;
using StellaOps.AirGap.Policy;
using StellaOps.Policy.Engine.Orchestration;
using StellaOps.Policy.Engine.ReachabilityFacts;
using StellaOps.Policy.Engine.Storage.InMemory;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Receipts;
using StellaOps.Policy.Storage.Postgres;
var builder = WebApplication.CreateBuilder(args);
@@ -95,16 +94,16 @@ var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyEngineOptions>(op
builder.Configuration.AddConfiguration(bootstrap.Configuration);
builder.ConfigurePolicyEngineTelemetry(bootstrap.Options);
builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap");
// CVSS receipts rely on PostgreSQL storage for deterministic persistence.
builder.Services.AddPolicyPostgresStorage(builder.Configuration, sectionName: "Postgres:Policy");
builder.Services.AddSingleton<ICvssV4Engine, CvssV4Engine>();
builder.Services.AddScoped<IReceiptBuilder, ReceiptBuilder>();
builder.Services.AddScoped<IReceiptHistoryService, ReceiptHistoryService>();
builder.ConfigurePolicyEngineTelemetry(bootstrap.Options);
builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap");
// CVSS receipts rely on PostgreSQL storage for deterministic persistence.
builder.Services.AddPolicyPostgresStorage(builder.Configuration, sectionName: "Postgres:Policy");
builder.Services.AddSingleton<ICvssV4Engine, CvssV4Engine>();
builder.Services.AddScoped<IReceiptBuilder, ReceiptBuilder>();
builder.Services.AddScoped<IReceiptHistoryService, ReceiptHistoryService>();
builder.Services.AddOptions<PolicyEngineOptions>()
.Bind(builder.Configuration.GetSection(PolicyEngineOptions.SectionName))
@@ -324,30 +323,30 @@ app.MapAdvisoryAiKnobs();
app.MapBatchContext();
app.MapOrchestratorJobs();
app.MapPolicyWorker();
app.MapLedgerExport();
app.MapConsoleExportJobs(); // CONTRACT-EXPORT-BUNDLE-009
app.MapPolicyPackBundles(); // CONTRACT-MIRROR-BUNDLE-003
app.MapSealedMode(); // CONTRACT-SEALED-MODE-004
app.MapStalenessSignaling(); // CONTRACT-SEALED-MODE-004 staleness
app.MapAirGapNotifications(); // Air-gap notifications
app.MapPolicyLint(); // POLICY-AOC-19-001 determinism linting
app.MapVerificationPolicies(); // CONTRACT-VERIFICATION-POLICY-006 attestation policies
app.MapVerificationPolicyEditor(); // CONTRACT-VERIFICATION-POLICY-006 editor DTOs/validation
app.MapAttestationReports(); // CONTRACT-VERIFICATION-POLICY-006 attestation reports
app.MapConsoleAttestationReports(); // CONTRACT-VERIFICATION-POLICY-006 Console integration
app.MapSnapshots();
app.MapViolations();
app.MapPolicyDecisions();
app.MapRiskProfiles();
app.MapRiskProfileSchema();
app.MapScopeAttachments();
app.MapEffectivePolicies(); // CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008
app.MapRiskSimulation();
app.MapOverrides();
app.MapProfileExport();
app.MapRiskProfileAirGap(); // CONTRACT-MIRROR-BUNDLE-003 risk profile air-gap
app.MapProfileEvents();
app.MapCvssReceipts(); // CVSS v4 receipt CRUD & history
app.MapLedgerExport();
app.MapConsoleExportJobs(); // CONTRACT-EXPORT-BUNDLE-009
app.MapPolicyPackBundles(); // CONTRACT-MIRROR-BUNDLE-003
app.MapSealedMode(); // CONTRACT-SEALED-MODE-004
app.MapStalenessSignaling(); // CONTRACT-SEALED-MODE-004 staleness
app.MapAirGapNotifications(); // Air-gap notifications
app.MapPolicyLint(); // POLICY-AOC-19-001 determinism linting
app.MapVerificationPolicies(); // CONTRACT-VERIFICATION-POLICY-006 attestation policies
app.MapVerificationPolicyEditor(); // CONTRACT-VERIFICATION-POLICY-006 editor DTOs/validation
app.MapAttestationReports(); // CONTRACT-VERIFICATION-POLICY-006 attestation reports
app.MapConsoleAttestationReports(); // CONTRACT-VERIFICATION-POLICY-006 Console integration
app.MapSnapshots();
app.MapViolations();
app.MapPolicyDecisions();
app.MapRiskProfiles();
app.MapRiskProfileSchema();
app.MapScopeAttachments();
app.MapEffectivePolicies(); // CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008
app.MapRiskSimulation();
app.MapOverrides();
app.MapProfileExport();
app.MapRiskProfileAirGap(); // CONTRACT-MIRROR-BUNDLE-003 risk profile air-gap
app.MapProfileEvents();
app.MapCvssReceipts(); // CVSS v4 receipt CRUD & history
// Phase 5: Multi-tenant PostgreSQL-backed API endpoints
app.MapPolicySnapshotsApi();

View File

@@ -1,325 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document representing an effective finding after policy evaluation.
/// Collection: effective_finding_{policyId}
/// Tenant-scoped with unique constraint on (tenantId, componentPurl, advisoryId).
/// </summary>
[BsonIgnoreExtraElements]
public sealed class EffectiveFindingDocument
{
/// <summary>
/// Unique identifier: sha256:{hash of tenantId|policyId|componentPurl|advisoryId}
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier (normalized to lowercase).
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Policy identifier.
/// </summary>
[BsonElement("policyId")]
public string PolicyId { get; set; } = string.Empty;
/// <summary>
/// Policy version at time of evaluation.
/// </summary>
[BsonElement("policyVersion")]
public int PolicyVersion { get; set; }
/// <summary>
/// Component PURL from the SBOM.
/// </summary>
[BsonElement("componentPurl")]
public string ComponentPurl { get; set; } = string.Empty;
/// <summary>
/// Component name.
/// </summary>
[BsonElement("componentName")]
public string ComponentName { get; set; } = string.Empty;
/// <summary>
/// Component version.
/// </summary>
[BsonElement("componentVersion")]
public string ComponentVersion { get; set; } = string.Empty;
/// <summary>
/// Package ecosystem (npm, maven, pypi, etc.).
/// </summary>
[BsonElement("ecosystem")]
[BsonIgnoreIfNull]
public string? Ecosystem { get; set; }
/// <summary>
/// Advisory identifier (CVE, GHSA, etc.).
/// </summary>
[BsonElement("advisoryId")]
public string AdvisoryId { get; set; } = string.Empty;
/// <summary>
/// Advisory source.
/// </summary>
[BsonElement("advisorySource")]
public string AdvisorySource { get; set; } = string.Empty;
/// <summary>
/// Vulnerability ID (may differ from advisory ID).
/// </summary>
[BsonElement("vulnerabilityId")]
[BsonIgnoreIfNull]
public string? VulnerabilityId { get; set; }
/// <summary>
/// Policy evaluation status (affected, blocked, suppressed, etc.).
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = string.Empty;
/// <summary>
/// Normalized severity (Critical, High, Medium, Low, None).
/// </summary>
[BsonElement("severity")]
[BsonIgnoreIfNull]
public string? Severity { get; set; }
/// <summary>
/// CVSS score (if available).
/// </summary>
[BsonElement("cvssScore")]
[BsonIgnoreIfNull]
public double? CvssScore { get; set; }
/// <summary>
/// Rule name that matched.
/// </summary>
[BsonElement("ruleName")]
[BsonIgnoreIfNull]
public string? RuleName { get; set; }
/// <summary>
/// Rule priority.
/// </summary>
[BsonElement("rulePriority")]
[BsonIgnoreIfNull]
public int? RulePriority { get; set; }
/// <summary>
/// VEX status overlay (if VEX was applied).
/// </summary>
[BsonElement("vexStatus")]
[BsonIgnoreIfNull]
public string? VexStatus { get; set; }
/// <summary>
/// VEX justification (if VEX was applied).
/// </summary>
[BsonElement("vexJustification")]
[BsonIgnoreIfNull]
public string? VexJustification { get; set; }
/// <summary>
/// VEX provider/vendor.
/// </summary>
[BsonElement("vexVendor")]
[BsonIgnoreIfNull]
public string? VexVendor { get; set; }
/// <summary>
/// Whether a VEX override was applied.
/// </summary>
[BsonElement("isVexOverride")]
public bool IsVexOverride { get; set; }
/// <summary>
/// SBOM ID where component was found.
/// </summary>
[BsonElement("sbomId")]
[BsonIgnoreIfNull]
public string? SbomId { get; set; }
/// <summary>
/// Product key associated with the SBOM.
/// </summary>
[BsonElement("productKey")]
[BsonIgnoreIfNull]
public string? ProductKey { get; set; }
/// <summary>
/// Policy evaluation annotations.
/// </summary>
[BsonElement("annotations")]
public Dictionary<string, string> Annotations { get; set; } = new();
/// <summary>
/// Current history version (incremented on each update).
/// </summary>
[BsonElement("historyVersion")]
public long HistoryVersion { get; set; }
/// <summary>
/// Reference to the policy run that produced this finding.
/// </summary>
[BsonElement("policyRunId")]
[BsonIgnoreIfNull]
public string? PolicyRunId { get; set; }
/// <summary>
/// Trace ID for distributed tracing.
/// </summary>
[BsonElement("traceId")]
[BsonIgnoreIfNull]
public string? TraceId { get; set; }
/// <summary>
/// Span ID for distributed tracing.
/// </summary>
[BsonElement("spanId")]
[BsonIgnoreIfNull]
public string? SpanId { get; set; }
/// <summary>
/// When this finding was first created.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// When this finding was last updated.
/// </summary>
[BsonElement("updatedAt")]
public DateTimeOffset UpdatedAt { get; set; }
/// <summary>
/// Content hash for deduplication and change detection.
/// </summary>
[BsonElement("contentHash")]
public string ContentHash { get; set; } = string.Empty;
}
/// <summary>
/// MongoDB document for effective finding history (append-only).
/// Collection: effective_finding_history_{policyId}
/// </summary>
[BsonIgnoreExtraElements]
public sealed class EffectiveFindingHistoryDocument
{
/// <summary>
/// Unique identifier: {findingId}:v{version}
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Reference to the effective finding.
/// </summary>
[BsonElement("findingId")]
public string FindingId { get; set; } = string.Empty;
/// <summary>
/// Policy identifier.
/// </summary>
[BsonElement("policyId")]
public string PolicyId { get; set; } = string.Empty;
/// <summary>
/// History version number (monotonically increasing).
/// </summary>
[BsonElement("version")]
public long Version { get; set; }
/// <summary>
/// Type of change (Created, StatusChanged, SeverityChanged, VexApplied, etc.).
/// </summary>
[BsonElement("changeType")]
public string ChangeType { get; set; } = string.Empty;
/// <summary>
/// Previous status (for status changes).
/// </summary>
[BsonElement("previousStatus")]
[BsonIgnoreIfNull]
public string? PreviousStatus { get; set; }
/// <summary>
/// New status.
/// </summary>
[BsonElement("newStatus")]
public string NewStatus { get; set; } = string.Empty;
/// <summary>
/// Previous severity (for severity changes).
/// </summary>
[BsonElement("previousSeverity")]
[BsonIgnoreIfNull]
public string? PreviousSeverity { get; set; }
/// <summary>
/// New severity.
/// </summary>
[BsonElement("newSeverity")]
[BsonIgnoreIfNull]
public string? NewSeverity { get; set; }
/// <summary>
/// Previous content hash.
/// </summary>
[BsonElement("previousContentHash")]
[BsonIgnoreIfNull]
public string? PreviousContentHash { get; set; }
/// <summary>
/// New content hash.
/// </summary>
[BsonElement("newContentHash")]
public string NewContentHash { get; set; } = string.Empty;
/// <summary>
/// Policy run that triggered this change.
/// </summary>
[BsonElement("policyRunId")]
[BsonIgnoreIfNull]
public string? PolicyRunId { get; set; }
/// <summary>
/// Trace ID for distributed tracing.
/// </summary>
[BsonElement("traceId")]
[BsonIgnoreIfNull]
public string? TraceId { get; set; }
/// <summary>
/// When this change occurred.
/// </summary>
[BsonElement("occurredAt")]
public DateTimeOffset OccurredAt { get; set; }
/// <summary>
/// TTL expiration timestamp for automatic cleanup.
/// </summary>
[BsonElement("expiresAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ExpiresAt { get; set; }
/// <summary>
/// Creates the composite ID for a history entry.
/// </summary>
public static string CreateId(string findingId, long version) => $"{findingId}:v{version}";
}

View File

@@ -1,157 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document for policy audit log entries.
/// Collection: policy_audit
/// Tracks all policy-related actions for compliance and debugging.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyAuditDocument
{
/// <summary>
/// Unique audit entry identifier.
/// </summary>
[BsonId]
[BsonElement("_id")]
public ObjectId Id { get; set; }
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Action type (PolicyCreated, PolicyUpdated, RevisionApproved, RunStarted, etc.).
/// </summary>
[BsonElement("action")]
public string Action { get; set; } = string.Empty;
/// <summary>
/// Resource type (Policy, Revision, Bundle, Run, Finding).
/// </summary>
[BsonElement("resourceType")]
public string ResourceType { get; set; } = string.Empty;
/// <summary>
/// Resource identifier.
/// </summary>
[BsonElement("resourceId")]
public string ResourceId { get; set; } = string.Empty;
/// <summary>
/// Actor identifier (user ID or service account).
/// </summary>
[BsonElement("actorId")]
[BsonIgnoreIfNull]
public string? ActorId { get; set; }
/// <summary>
/// Actor type (User, ServiceAccount, System).
/// </summary>
[BsonElement("actorType")]
public string ActorType { get; set; } = "System";
/// <summary>
/// Previous state snapshot (for update actions).
/// </summary>
[BsonElement("previousState")]
[BsonIgnoreIfNull]
public BsonDocument? PreviousState { get; set; }
/// <summary>
/// New state snapshot (for create/update actions).
/// </summary>
[BsonElement("newState")]
[BsonIgnoreIfNull]
public BsonDocument? NewState { get; set; }
/// <summary>
/// Additional context/metadata.
/// </summary>
[BsonElement("metadata")]
public Dictionary<string, string> Metadata { get; set; } = new();
/// <summary>
/// Correlation ID for distributed tracing.
/// </summary>
[BsonElement("correlationId")]
[BsonIgnoreIfNull]
public string? CorrelationId { get; set; }
/// <summary>
/// Trace ID for OpenTelemetry.
/// </summary>
[BsonElement("traceId")]
[BsonIgnoreIfNull]
public string? TraceId { get; set; }
/// <summary>
/// Client IP address.
/// </summary>
[BsonElement("clientIp")]
[BsonIgnoreIfNull]
public string? ClientIp { get; set; }
/// <summary>
/// User agent string.
/// </summary>
[BsonElement("userAgent")]
[BsonIgnoreIfNull]
public string? UserAgent { get; set; }
/// <summary>
/// When the action occurred.
/// </summary>
[BsonElement("occurredAt")]
public DateTimeOffset OccurredAt { get; set; }
}
/// <summary>
/// Audit action types for policy operations.
/// </summary>
public static class PolicyAuditActions
{
public const string PolicyCreated = "PolicyCreated";
public const string PolicyUpdated = "PolicyUpdated";
public const string PolicyDeleted = "PolicyDeleted";
public const string RevisionCreated = "RevisionCreated";
public const string RevisionApproved = "RevisionApproved";
public const string RevisionActivated = "RevisionActivated";
public const string RevisionArchived = "RevisionArchived";
public const string BundleCompiled = "BundleCompiled";
public const string RunStarted = "RunStarted";
public const string RunCompleted = "RunCompleted";
public const string RunFailed = "RunFailed";
public const string RunCancelled = "RunCancelled";
public const string FindingCreated = "FindingCreated";
public const string FindingUpdated = "FindingUpdated";
public const string SimulationStarted = "SimulationStarted";
public const string SimulationCompleted = "SimulationCompleted";
}
/// <summary>
/// Resource types for policy audit entries.
/// </summary>
public static class PolicyAuditResourceTypes
{
public const string Policy = "Policy";
public const string Revision = "Revision";
public const string Bundle = "Bundle";
public const string Run = "Run";
public const string Finding = "Finding";
public const string Simulation = "Simulation";
}
/// <summary>
/// Actor types for policy audit entries.
/// </summary>
public static class PolicyAuditActorTypes
{
public const string User = "User";
public const string ServiceAccount = "ServiceAccount";
public const string System = "System";
}

View File

@@ -1,343 +0,0 @@
using System.Collections.Immutable;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document representing a policy pack.
/// Collection: policies
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyDocument
{
/// <summary>
/// Unique identifier (packId).
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier (normalized to lowercase).
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Display name for the policy pack.
/// </summary>
[BsonElement("displayName")]
[BsonIgnoreIfNull]
public string? DisplayName { get; set; }
/// <summary>
/// Description of the policy pack.
/// </summary>
[BsonElement("description")]
[BsonIgnoreIfNull]
public string? Description { get; set; }
/// <summary>
/// Current active revision version (null if none active).
/// </summary>
[BsonElement("activeVersion")]
[BsonIgnoreIfNull]
public int? ActiveVersion { get; set; }
/// <summary>
/// Latest revision version.
/// </summary>
[BsonElement("latestVersion")]
public int LatestVersion { get; set; }
/// <summary>
/// Tags for categorization and filtering.
/// </summary>
[BsonElement("tags")]
public List<string> Tags { get; set; } = [];
/// <summary>
/// Creation timestamp.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Last update timestamp.
/// </summary>
[BsonElement("updatedAt")]
public DateTimeOffset UpdatedAt { get; set; }
/// <summary>
/// User who created the policy pack.
/// </summary>
[BsonElement("createdBy")]
[BsonIgnoreIfNull]
public string? CreatedBy { get; set; }
}
/// <summary>
/// MongoDB document representing a policy revision.
/// Collection: policy_revisions
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRevisionDocument
{
/// <summary>
/// Unique identifier: {packId}:{version}
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Reference to policy pack.
/// </summary>
[BsonElement("packId")]
public string PackId { get; set; } = string.Empty;
/// <summary>
/// Revision version number.
/// </summary>
[BsonElement("version")]
public int Version { get; set; }
/// <summary>
/// Revision status (Draft, Approved, Active, Archived).
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = "Draft";
/// <summary>
/// Whether two-person approval is required.
/// </summary>
[BsonElement("requiresTwoPersonApproval")]
public bool RequiresTwoPersonApproval { get; set; }
/// <summary>
/// Approval records.
/// </summary>
[BsonElement("approvals")]
public List<PolicyApprovalRecord> Approvals { get; set; } = [];
/// <summary>
/// Reference to the compiled bundle.
/// </summary>
[BsonElement("bundleId")]
[BsonIgnoreIfNull]
public string? BundleId { get; set; }
/// <summary>
/// SHA256 digest of the bundle.
/// </summary>
[BsonElement("bundleDigest")]
[BsonIgnoreIfNull]
public string? BundleDigest { get; set; }
/// <summary>
/// Creation timestamp.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Activation timestamp (when status became Active).
/// </summary>
[BsonElement("activatedAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ActivatedAt { get; set; }
/// <summary>
/// Creates the composite ID for a revision.
/// </summary>
public static string CreateId(string packId, int version) => $"{packId}:{version}";
}
/// <summary>
/// Embedded approval record for policy revisions.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyApprovalRecord
{
/// <summary>
/// User who approved.
/// </summary>
[BsonElement("actorId")]
public string ActorId { get; set; } = string.Empty;
/// <summary>
/// Approval timestamp.
/// </summary>
[BsonElement("approvedAt")]
public DateTimeOffset ApprovedAt { get; set; }
/// <summary>
/// Optional comment.
/// </summary>
[BsonElement("comment")]
[BsonIgnoreIfNull]
public string? Comment { get; set; }
}
/// <summary>
/// MongoDB document for compiled policy bundles.
/// Collection: policy_bundles
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyBundleDocument
{
/// <summary>
/// Unique identifier (SHA256 digest).
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Reference to policy pack.
/// </summary>
[BsonElement("packId")]
public string PackId { get; set; } = string.Empty;
/// <summary>
/// Revision version.
/// </summary>
[BsonElement("version")]
public int Version { get; set; }
/// <summary>
/// Cryptographic signature.
/// </summary>
[BsonElement("signature")]
public string Signature { get; set; } = string.Empty;
/// <summary>
/// Bundle size in bytes.
/// </summary>
[BsonElement("sizeBytes")]
public int SizeBytes { get; set; }
/// <summary>
/// Compiled bundle payload (binary).
/// </summary>
[BsonElement("payload")]
public byte[] Payload { get; set; } = [];
/// <summary>
/// AOC metadata for compliance tracking.
/// </summary>
[BsonElement("aocMetadata")]
[BsonIgnoreIfNull]
public PolicyAocMetadataDocument? AocMetadata { get; set; }
/// <summary>
/// Creation timestamp.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
}
/// <summary>
/// Embedded AOC metadata document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyAocMetadataDocument
{
[BsonElement("compilationId")]
public string CompilationId { get; set; } = string.Empty;
[BsonElement("compilerVersion")]
public string CompilerVersion { get; set; } = string.Empty;
[BsonElement("compiledAt")]
public DateTimeOffset CompiledAt { get; set; }
[BsonElement("sourceDigest")]
public string SourceDigest { get; set; } = string.Empty;
[BsonElement("artifactDigest")]
public string ArtifactDigest { get; set; } = string.Empty;
[BsonElement("complexityScore")]
public double ComplexityScore { get; set; }
[BsonElement("ruleCount")]
public int RuleCount { get; set; }
[BsonElement("durationMilliseconds")]
public long DurationMilliseconds { get; set; }
[BsonElement("provenance")]
[BsonIgnoreIfNull]
public PolicyProvenanceDocument? Provenance { get; set; }
[BsonElement("attestationRef")]
[BsonIgnoreIfNull]
public PolicyAttestationRefDocument? AttestationRef { get; set; }
}
/// <summary>
/// Embedded provenance document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyProvenanceDocument
{
[BsonElement("sourceType")]
public string SourceType { get; set; } = string.Empty;
[BsonElement("sourceUrl")]
[BsonIgnoreIfNull]
public string? SourceUrl { get; set; }
[BsonElement("submitter")]
[BsonIgnoreIfNull]
public string? Submitter { get; set; }
[BsonElement("commitSha")]
[BsonIgnoreIfNull]
public string? CommitSha { get; set; }
[BsonElement("branch")]
[BsonIgnoreIfNull]
public string? Branch { get; set; }
[BsonElement("ingestedAt")]
public DateTimeOffset IngestedAt { get; set; }
}
/// <summary>
/// Embedded attestation reference document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyAttestationRefDocument
{
[BsonElement("attestationId")]
public string AttestationId { get; set; } = string.Empty;
[BsonElement("envelopeDigest")]
public string EnvelopeDigest { get; set; } = string.Empty;
[BsonElement("uri")]
[BsonIgnoreIfNull]
public string? Uri { get; set; }
[BsonElement("signingKeyId")]
[BsonIgnoreIfNull]
public string? SigningKeyId { get; set; }
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
}

View File

@@ -1,482 +0,0 @@
using System.Collections.Immutable;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document representing a policy exception.
/// Collection: exceptions
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyExceptionDocument
{
/// <summary>
/// Unique identifier.
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier (normalized to lowercase).
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Human-readable name for the exception.
/// </summary>
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Description and justification for the exception.
/// </summary>
[BsonElement("description")]
[BsonIgnoreIfNull]
public string? Description { get; set; }
/// <summary>
/// Exception type: waiver, override, temporary, permanent.
/// </summary>
[BsonElement("exceptionType")]
public string ExceptionType { get; set; } = "waiver";
/// <summary>
/// Exception status: draft, pending_review, approved, active, expired, revoked.
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = "draft";
/// <summary>
/// Scope of the exception (e.g., advisory IDs, PURL patterns, CVE IDs).
/// </summary>
[BsonElement("scope")]
public ExceptionScopeDocument Scope { get; set; } = new();
/// <summary>
/// Risk assessment and mitigation details.
/// </summary>
[BsonElement("riskAssessment")]
[BsonIgnoreIfNull]
public ExceptionRiskAssessmentDocument? RiskAssessment { get; set; }
/// <summary>
/// Compensating controls in place while exception is active.
/// </summary>
[BsonElement("compensatingControls")]
public List<string> CompensatingControls { get; set; } = [];
/// <summary>
/// Tags for categorization and filtering.
/// </summary>
[BsonElement("tags")]
public List<string> Tags { get; set; } = [];
/// <summary>
/// Priority for conflict resolution (higher = more precedence).
/// </summary>
[BsonElement("priority")]
public int Priority { get; set; }
/// <summary>
/// When the exception becomes active (null = immediately upon approval).
/// </summary>
[BsonElement("effectiveFrom")]
[BsonIgnoreIfNull]
public DateTimeOffset? EffectiveFrom { get; set; }
/// <summary>
/// When the exception expires (null = no expiration).
/// </summary>
[BsonElement("expiresAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ExpiresAt { get; set; }
/// <summary>
/// User who created the exception.
/// </summary>
[BsonElement("createdBy")]
public string CreatedBy { get; set; } = string.Empty;
/// <summary>
/// Creation timestamp.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Last update timestamp.
/// </summary>
[BsonElement("updatedAt")]
public DateTimeOffset UpdatedAt { get; set; }
/// <summary>
/// When the exception was activated.
/// </summary>
[BsonElement("activatedAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ActivatedAt { get; set; }
/// <summary>
/// When the exception was revoked.
/// </summary>
[BsonElement("revokedAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? RevokedAt { get; set; }
/// <summary>
/// User who revoked the exception.
/// </summary>
[BsonElement("revokedBy")]
[BsonIgnoreIfNull]
public string? RevokedBy { get; set; }
/// <summary>
/// Reason for revocation.
/// </summary>
[BsonElement("revocationReason")]
[BsonIgnoreIfNull]
public string? RevocationReason { get; set; }
/// <summary>
/// Reference to the active review (if pending_review status).
/// </summary>
[BsonElement("activeReviewId")]
[BsonIgnoreIfNull]
public string? ActiveReviewId { get; set; }
/// <summary>
/// Correlation ID for tracing.
/// </summary>
[BsonElement("correlationId")]
[BsonIgnoreIfNull]
public string? CorrelationId { get; set; }
}
/// <summary>
/// Embedded document for exception scope definition.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExceptionScopeDocument
{
/// <summary>
/// Advisory IDs covered by this exception.
/// </summary>
[BsonElement("advisoryIds")]
public List<string> AdvisoryIds { get; set; } = [];
/// <summary>
/// CVE IDs covered by this exception.
/// </summary>
[BsonElement("cveIds")]
public List<string> CveIds { get; set; } = [];
/// <summary>
/// PURL patterns (supports wildcards) covered by this exception.
/// </summary>
[BsonElement("purlPatterns")]
public List<string> PurlPatterns { get; set; } = [];
/// <summary>
/// Specific asset IDs covered.
/// </summary>
[BsonElement("assetIds")]
public List<string> AssetIds { get; set; } = [];
/// <summary>
/// Repository IDs covered (scope limiter).
/// </summary>
[BsonElement("repositoryIds")]
public List<string> RepositoryIds { get; set; } = [];
/// <summary>
/// Snapshot IDs covered (scope limiter).
/// </summary>
[BsonElement("snapshotIds")]
public List<string> SnapshotIds { get; set; } = [];
/// <summary>
/// Severity levels to apply exception to.
/// </summary>
[BsonElement("severities")]
public List<string> Severities { get; set; } = [];
/// <summary>
/// Whether this exception applies to all assets (tenant-wide).
/// </summary>
[BsonElement("applyToAll")]
public bool ApplyToAll { get; set; }
}
/// <summary>
/// Embedded document for risk assessment.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExceptionRiskAssessmentDocument
{
/// <summary>
/// Original risk level being excepted.
/// </summary>
[BsonElement("originalRiskLevel")]
public string OriginalRiskLevel { get; set; } = string.Empty;
/// <summary>
/// Residual risk level after compensating controls.
/// </summary>
[BsonElement("residualRiskLevel")]
public string ResidualRiskLevel { get; set; } = string.Empty;
/// <summary>
/// Business justification for accepting the risk.
/// </summary>
[BsonElement("businessJustification")]
[BsonIgnoreIfNull]
public string? BusinessJustification { get; set; }
/// <summary>
/// Impact assessment if vulnerability is exploited.
/// </summary>
[BsonElement("impactAssessment")]
[BsonIgnoreIfNull]
public string? ImpactAssessment { get; set; }
/// <summary>
/// Exploitability assessment.
/// </summary>
[BsonElement("exploitability")]
[BsonIgnoreIfNull]
public string? Exploitability { get; set; }
}
/// <summary>
/// MongoDB document representing an exception review.
/// Collection: exception_reviews
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExceptionReviewDocument
{
/// <summary>
/// Unique identifier.
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Reference to the exception being reviewed.
/// </summary>
[BsonElement("exceptionId")]
public string ExceptionId { get; set; } = string.Empty;
/// <summary>
/// Review status: pending, approved, rejected.
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = "pending";
/// <summary>
/// Type of review: initial, renewal, modification.
/// </summary>
[BsonElement("reviewType")]
public string ReviewType { get; set; } = "initial";
/// <summary>
/// Whether multiple approvers are required.
/// </summary>
[BsonElement("requiresMultipleApprovers")]
public bool RequiresMultipleApprovers { get; set; }
/// <summary>
/// Minimum number of approvals required.
/// </summary>
[BsonElement("requiredApprovals")]
public int RequiredApprovals { get; set; } = 1;
/// <summary>
/// Designated reviewers (user or group IDs).
/// </summary>
[BsonElement("designatedReviewers")]
public List<string> DesignatedReviewers { get; set; } = [];
/// <summary>
/// Individual approval/rejection decisions.
/// </summary>
[BsonElement("decisions")]
public List<ReviewDecisionDocument> Decisions { get; set; } = [];
/// <summary>
/// User who requested the review.
/// </summary>
[BsonElement("requestedBy")]
public string RequestedBy { get; set; } = string.Empty;
/// <summary>
/// When the review was requested.
/// </summary>
[BsonElement("requestedAt")]
public DateTimeOffset RequestedAt { get; set; }
/// <summary>
/// When the review was completed.
/// </summary>
[BsonElement("completedAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>
/// Review deadline.
/// </summary>
[BsonElement("deadline")]
[BsonIgnoreIfNull]
public DateTimeOffset? Deadline { get; set; }
/// <summary>
/// Notes or comments on the review.
/// </summary>
[BsonElement("notes")]
[BsonIgnoreIfNull]
public string? Notes { get; set; }
/// <summary>
/// Creates the composite ID for a review.
/// </summary>
public static string CreateId(string exceptionId, string reviewType, DateTimeOffset timestamp)
=> $"{exceptionId}:{reviewType}:{timestamp:yyyyMMddHHmmss}";
}
/// <summary>
/// Embedded document for an individual reviewer's decision.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ReviewDecisionDocument
{
/// <summary>
/// Reviewer ID (user or service account).
/// </summary>
[BsonElement("reviewerId")]
public string ReviewerId { get; set; } = string.Empty;
/// <summary>
/// Decision: approved, rejected, abstained.
/// </summary>
[BsonElement("decision")]
public string Decision { get; set; } = string.Empty;
/// <summary>
/// Timestamp of the decision.
/// </summary>
[BsonElement("decidedAt")]
public DateTimeOffset DecidedAt { get; set; }
/// <summary>
/// Comment explaining the decision.
/// </summary>
[BsonElement("comment")]
[BsonIgnoreIfNull]
public string? Comment { get; set; }
/// <summary>
/// Conditions attached to approval.
/// </summary>
[BsonElement("conditions")]
public List<string> Conditions { get; set; } = [];
}
/// <summary>
/// MongoDB document representing an exception binding to specific assets.
/// Collection: exception_bindings
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExceptionBindingDocument
{
/// <summary>
/// Unique identifier: {exceptionId}:{assetId}:{advisoryId}
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Reference to the exception.
/// </summary>
[BsonElement("exceptionId")]
public string ExceptionId { get; set; } = string.Empty;
/// <summary>
/// Asset ID (PURL or other identifier) this binding applies to.
/// </summary>
[BsonElement("assetId")]
public string AssetId { get; set; } = string.Empty;
/// <summary>
/// Advisory ID this binding covers.
/// </summary>
[BsonElement("advisoryId")]
[BsonIgnoreIfNull]
public string? AdvisoryId { get; set; }
/// <summary>
/// CVE ID this binding covers.
/// </summary>
[BsonElement("cveId")]
[BsonIgnoreIfNull]
public string? CveId { get; set; }
/// <summary>
/// Snapshot ID where binding was created.
/// </summary>
[BsonElement("snapshotId")]
[BsonIgnoreIfNull]
public string? SnapshotId { get; set; }
/// <summary>
/// Binding status: active, expired, revoked.
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = "active";
/// <summary>
/// Policy decision override applied by this binding.
/// </summary>
[BsonElement("decisionOverride")]
public string DecisionOverride { get; set; } = "allow";
/// <summary>
/// When the binding becomes effective.
/// </summary>
[BsonElement("effectiveFrom")]
public DateTimeOffset EffectiveFrom { get; set; }
/// <summary>
/// When the binding expires.
/// </summary>
[BsonElement("expiresAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ExpiresAt { get; set; }
/// <summary>
/// When the binding was created.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Creates the composite ID for a binding.
/// </summary>
public static string CreateId(string exceptionId, string assetId, string? advisoryId)
=> $"{exceptionId}:{assetId}:{advisoryId ?? "all"}";
}

View File

@@ -1,383 +0,0 @@
using System.Collections.Immutable;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document for storing policy explain traces.
/// Collection: policy_explains
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyExplainDocument
{
/// <summary>
/// Unique identifier (combination of runId and subjectHash).
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Policy run identifier.
/// </summary>
[BsonElement("runId")]
public string RunId { get; set; } = string.Empty;
/// <summary>
/// Policy pack identifier.
/// </summary>
[BsonElement("policyId")]
public string PolicyId { get; set; } = string.Empty;
/// <summary>
/// Policy version at time of evaluation.
/// </summary>
[BsonElement("policyVersion")]
[BsonIgnoreIfNull]
public int? PolicyVersion { get; set; }
/// <summary>
/// Hash of the evaluation subject (component + advisory).
/// </summary>
[BsonElement("subjectHash")]
public string SubjectHash { get; set; } = string.Empty;
/// <summary>
/// Hash of the policy bundle used.
/// </summary>
[BsonElement("bundleDigest")]
[BsonIgnoreIfNull]
public string? BundleDigest { get; set; }
/// <summary>
/// Evaluation timestamp (deterministic).
/// </summary>
[BsonElement("evaluatedAt")]
public DateTimeOffset EvaluatedAt { get; set; }
/// <summary>
/// Evaluation duration in milliseconds.
/// </summary>
[BsonElement("durationMs")]
public long DurationMs { get; set; }
/// <summary>
/// Final outcome of the evaluation.
/// </summary>
[BsonElement("finalOutcome")]
public string FinalOutcome { get; set; } = string.Empty;
/// <summary>
/// Input context information.
/// </summary>
[BsonElement("inputContext")]
public ExplainInputContextDocument InputContext { get; set; } = new();
/// <summary>
/// Rule evaluation steps.
/// </summary>
[BsonElement("ruleSteps")]
public List<ExplainRuleStepDocument> RuleSteps { get; set; } = [];
/// <summary>
/// VEX evidence applied.
/// </summary>
[BsonElement("vexEvidence")]
public List<ExplainVexEvidenceDocument> VexEvidence { get; set; } = [];
/// <summary>
/// Statistics summary.
/// </summary>
[BsonElement("statistics")]
public ExplainStatisticsDocument Statistics { get; set; } = new();
/// <summary>
/// Determinism hash for reproducibility verification.
/// </summary>
[BsonElement("determinismHash")]
[BsonIgnoreIfNull]
public string? DeterminismHash { get; set; }
/// <summary>
/// Reference to AOC chain for this evaluation.
/// </summary>
[BsonElement("aocChain")]
[BsonIgnoreIfNull]
public ExplainAocChainDocument? AocChain { get; set; }
/// <summary>
/// Additional metadata.
/// </summary>
[BsonElement("metadata")]
public Dictionary<string, string> Metadata { get; set; } = new();
/// <summary>
/// Creation timestamp.
/// </summary>
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// TTL expiration timestamp for automatic cleanup.
/// </summary>
[BsonElement("expiresAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ExpiresAt { get; set; }
/// <summary>
/// Creates the composite ID for an explain trace.
/// </summary>
public static string CreateId(string runId, string subjectHash) => $"{runId}:{subjectHash}";
}
/// <summary>
/// Input context embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainInputContextDocument
{
[BsonElement("componentPurl")]
[BsonIgnoreIfNull]
public string? ComponentPurl { get; set; }
[BsonElement("componentName")]
[BsonIgnoreIfNull]
public string? ComponentName { get; set; }
[BsonElement("componentVersion")]
[BsonIgnoreIfNull]
public string? ComponentVersion { get; set; }
[BsonElement("advisoryId")]
[BsonIgnoreIfNull]
public string? AdvisoryId { get; set; }
[BsonElement("vulnerabilityId")]
[BsonIgnoreIfNull]
public string? VulnerabilityId { get; set; }
[BsonElement("inputSeverity")]
[BsonIgnoreIfNull]
public string? InputSeverity { get; set; }
[BsonElement("inputCvssScore")]
[BsonIgnoreIfNull]
public decimal? InputCvssScore { get; set; }
[BsonElement("environment")]
public Dictionary<string, string> Environment { get; set; } = new();
[BsonElement("sbomTags")]
public List<string> SbomTags { get; set; } = [];
[BsonElement("reachabilityState")]
[BsonIgnoreIfNull]
public string? ReachabilityState { get; set; }
[BsonElement("reachabilityConfidence")]
[BsonIgnoreIfNull]
public double? ReachabilityConfidence { get; set; }
}
/// <summary>
/// Rule step embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainRuleStepDocument
{
[BsonElement("stepNumber")]
public int StepNumber { get; set; }
[BsonElement("ruleName")]
public string RuleName { get; set; } = string.Empty;
[BsonElement("rulePriority")]
public int RulePriority { get; set; }
[BsonElement("ruleCategory")]
[BsonIgnoreIfNull]
public string? RuleCategory { get; set; }
[BsonElement("expression")]
[BsonIgnoreIfNull]
public string? Expression { get; set; }
[BsonElement("matched")]
public bool Matched { get; set; }
[BsonElement("outcome")]
[BsonIgnoreIfNull]
public string? Outcome { get; set; }
[BsonElement("assignedSeverity")]
[BsonIgnoreIfNull]
public string? AssignedSeverity { get; set; }
[BsonElement("isFinalMatch")]
public bool IsFinalMatch { get; set; }
[BsonElement("explanation")]
[BsonIgnoreIfNull]
public string? Explanation { get; set; }
[BsonElement("evaluationMicroseconds")]
public long EvaluationMicroseconds { get; set; }
[BsonElement("intermediateValues")]
public Dictionary<string, string> IntermediateValues { get; set; } = new();
}
/// <summary>
/// VEX evidence embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainVexEvidenceDocument
{
[BsonElement("vendor")]
public string Vendor { get; set; } = string.Empty;
[BsonElement("status")]
public string Status { get; set; } = string.Empty;
[BsonElement("justification")]
[BsonIgnoreIfNull]
public string? Justification { get; set; }
[BsonElement("confidence")]
[BsonIgnoreIfNull]
public double? Confidence { get; set; }
[BsonElement("wasApplied")]
public bool WasApplied { get; set; }
[BsonElement("explanation")]
[BsonIgnoreIfNull]
public string? Explanation { get; set; }
}
/// <summary>
/// Statistics embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainStatisticsDocument
{
[BsonElement("totalRulesEvaluated")]
public int TotalRulesEvaluated { get; set; }
[BsonElement("totalRulesFired")]
public int TotalRulesFired { get; set; }
[BsonElement("totalVexOverrides")]
public int TotalVexOverrides { get; set; }
[BsonElement("totalEvaluationMs")]
public long TotalEvaluationMs { get; set; }
[BsonElement("averageRuleEvaluationMicroseconds")]
public double AverageRuleEvaluationMicroseconds { get; set; }
[BsonElement("rulesFiredByCategory")]
public Dictionary<string, int> RulesFiredByCategory { get; set; } = new();
[BsonElement("rulesFiredByOutcome")]
public Dictionary<string, int> RulesFiredByOutcome { get; set; } = new();
}
/// <summary>
/// AOC chain reference for linking decisions to attestations.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainAocChainDocument
{
/// <summary>
/// Compilation ID that produced the policy bundle.
/// </summary>
[BsonElement("compilationId")]
public string CompilationId { get; set; } = string.Empty;
/// <summary>
/// Compiler version used.
/// </summary>
[BsonElement("compilerVersion")]
public string CompilerVersion { get; set; } = string.Empty;
/// <summary>
/// Source digest of the policy document.
/// </summary>
[BsonElement("sourceDigest")]
public string SourceDigest { get; set; } = string.Empty;
/// <summary>
/// Artifact digest of the compiled bundle.
/// </summary>
[BsonElement("artifactDigest")]
public string ArtifactDigest { get; set; } = string.Empty;
/// <summary>
/// Reference to the signed attestation.
/// </summary>
[BsonElement("attestationRef")]
[BsonIgnoreIfNull]
public ExplainAttestationRefDocument? AttestationRef { get; set; }
/// <summary>
/// Provenance information.
/// </summary>
[BsonElement("provenance")]
[BsonIgnoreIfNull]
public ExplainProvenanceDocument? Provenance { get; set; }
}
/// <summary>
/// Attestation reference embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainAttestationRefDocument
{
[BsonElement("attestationId")]
public string AttestationId { get; set; } = string.Empty;
[BsonElement("envelopeDigest")]
public string EnvelopeDigest { get; set; } = string.Empty;
[BsonElement("uri")]
[BsonIgnoreIfNull]
public string? Uri { get; set; }
[BsonElement("signingKeyId")]
[BsonIgnoreIfNull]
public string? SigningKeyId { get; set; }
}
/// <summary>
/// Provenance embedded document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class ExplainProvenanceDocument
{
[BsonElement("sourceType")]
public string SourceType { get; set; } = string.Empty;
[BsonElement("sourceUrl")]
[BsonIgnoreIfNull]
public string? SourceUrl { get; set; }
[BsonElement("submitter")]
[BsonIgnoreIfNull]
public string? Submitter { get; set; }
[BsonElement("commitSha")]
[BsonIgnoreIfNull]
public string? CommitSha { get; set; }
[BsonElement("branch")]
[BsonIgnoreIfNull]
public string? Branch { get; set; }
}

View File

@@ -1,319 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Documents;
/// <summary>
/// MongoDB document representing a policy evaluation run.
/// Collection: policy_runs
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRunDocument
{
/// <summary>
/// Unique run identifier.
/// </summary>
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Tenant identifier.
/// </summary>
[BsonElement("tenantId")]
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// Policy pack identifier.
/// </summary>
[BsonElement("policyId")]
public string PolicyId { get; set; } = string.Empty;
/// <summary>
/// Policy version evaluated.
/// </summary>
[BsonElement("policyVersion")]
public int PolicyVersion { get; set; }
/// <summary>
/// Run mode (full, incremental, simulation, batch).
/// </summary>
[BsonElement("mode")]
public string Mode { get; set; } = "full";
/// <summary>
/// Run status (pending, running, completed, failed, cancelled).
/// </summary>
[BsonElement("status")]
public string Status { get; set; } = "pending";
/// <summary>
/// Trigger type (scheduled, manual, event, api).
/// </summary>
[BsonElement("triggerType")]
public string TriggerType { get; set; } = "manual";
/// <summary>
/// Correlation ID for distributed tracing.
/// </summary>
[BsonElement("correlationId")]
[BsonIgnoreIfNull]
public string? CorrelationId { get; set; }
/// <summary>
/// Trace ID for OpenTelemetry.
/// </summary>
[BsonElement("traceId")]
[BsonIgnoreIfNull]
public string? TraceId { get; set; }
/// <summary>
/// Parent span ID if part of larger operation.
/// </summary>
[BsonElement("parentSpanId")]
[BsonIgnoreIfNull]
public string? ParentSpanId { get; set; }
/// <summary>
/// User or service that initiated the run.
/// </summary>
[BsonElement("initiatedBy")]
[BsonIgnoreIfNull]
public string? InitiatedBy { get; set; }
/// <summary>
/// Deterministic evaluation timestamp used for this run.
/// </summary>
[BsonElement("evaluationTimestamp")]
public DateTimeOffset EvaluationTimestamp { get; set; }
/// <summary>
/// When the run started.
/// </summary>
[BsonElement("startedAt")]
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// When the run completed (null if still running).
/// </summary>
[BsonElement("completedAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>
/// Run metrics and statistics.
/// </summary>
[BsonElement("metrics")]
public PolicyRunMetricsDocument Metrics { get; set; } = new();
/// <summary>
/// Input parameters for the run.
/// </summary>
[BsonElement("input")]
[BsonIgnoreIfNull]
public PolicyRunInputDocument? Input { get; set; }
/// <summary>
/// Run outcome summary.
/// </summary>
[BsonElement("outcome")]
[BsonIgnoreIfNull]
public PolicyRunOutcomeDocument? Outcome { get; set; }
/// <summary>
/// Error information if run failed.
/// </summary>
[BsonElement("error")]
[BsonIgnoreIfNull]
public PolicyRunErrorDocument? Error { get; set; }
/// <summary>
/// Determinism hash for reproducibility verification.
/// </summary>
[BsonElement("determinismHash")]
[BsonIgnoreIfNull]
public string? DeterminismHash { get; set; }
/// <summary>
/// TTL expiration timestamp for automatic cleanup.
/// </summary>
[BsonElement("expiresAt")]
[BsonIgnoreIfNull]
public DateTimeOffset? ExpiresAt { get; set; }
}
/// <summary>
/// Embedded metrics document for policy runs.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRunMetricsDocument
{
/// <summary>
/// Total components evaluated.
/// </summary>
[BsonElement("totalComponents")]
public int TotalComponents { get; set; }
/// <summary>
/// Total advisories evaluated.
/// </summary>
[BsonElement("totalAdvisories")]
public int TotalAdvisories { get; set; }
/// <summary>
/// Total findings generated.
/// </summary>
[BsonElement("totalFindings")]
public int TotalFindings { get; set; }
/// <summary>
/// Rules evaluated count.
/// </summary>
[BsonElement("rulesEvaluated")]
public int RulesEvaluated { get; set; }
/// <summary>
/// Rules that matched/fired.
/// </summary>
[BsonElement("rulesFired")]
public int RulesFired { get; set; }
/// <summary>
/// VEX overrides applied.
/// </summary>
[BsonElement("vexOverridesApplied")]
public int VexOverridesApplied { get; set; }
/// <summary>
/// Findings created (new).
/// </summary>
[BsonElement("findingsCreated")]
public int FindingsCreated { get; set; }
/// <summary>
/// Findings updated (changed).
/// </summary>
[BsonElement("findingsUpdated")]
public int FindingsUpdated { get; set; }
/// <summary>
/// Findings unchanged.
/// </summary>
[BsonElement("findingsUnchanged")]
public int FindingsUnchanged { get; set; }
/// <summary>
/// Duration in milliseconds.
/// </summary>
[BsonElement("durationMs")]
public long DurationMs { get; set; }
/// <summary>
/// Memory used in bytes.
/// </summary>
[BsonElement("memoryUsedBytes")]
public long MemoryUsedBytes { get; set; }
}
/// <summary>
/// Embedded input parameters document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRunInputDocument
{
/// <summary>
/// SBOM IDs included in evaluation.
/// </summary>
[BsonElement("sbomIds")]
public List<string> SbomIds { get; set; } = [];
/// <summary>
/// Product keys included in evaluation.
/// </summary>
[BsonElement("productKeys")]
public List<string> ProductKeys { get; set; } = [];
/// <summary>
/// Advisory IDs to evaluate (empty = all).
/// </summary>
[BsonElement("advisoryIds")]
public List<string> AdvisoryIds { get; set; } = [];
/// <summary>
/// Filter criteria applied.
/// </summary>
[BsonElement("filters")]
[BsonIgnoreIfNull]
public Dictionary<string, string>? Filters { get; set; }
}
/// <summary>
/// Embedded outcome summary document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRunOutcomeDocument
{
/// <summary>
/// Overall outcome (pass, fail, warn).
/// </summary>
[BsonElement("result")]
public string Result { get; set; } = "pass";
/// <summary>
/// Findings by severity.
/// </summary>
[BsonElement("bySeverity")]
public Dictionary<string, int> BySeverity { get; set; } = new();
/// <summary>
/// Findings by status.
/// </summary>
[BsonElement("byStatus")]
public Dictionary<string, int> ByStatus { get; set; } = new();
/// <summary>
/// Blocking findings count.
/// </summary>
[BsonElement("blockingCount")]
public int BlockingCount { get; set; }
/// <summary>
/// Summary message.
/// </summary>
[BsonElement("message")]
[BsonIgnoreIfNull]
public string? Message { get; set; }
}
/// <summary>
/// Embedded error document.
/// </summary>
[BsonIgnoreExtraElements]
public sealed class PolicyRunErrorDocument
{
/// <summary>
/// Error code.
/// </summary>
[BsonElement("code")]
public string Code { get; set; } = string.Empty;
/// <summary>
/// Error message.
/// </summary>
[BsonElement("message")]
public string Message { get; set; } = string.Empty;
/// <summary>
/// Stack trace (if available).
/// </summary>
[BsonElement("stackTrace")]
[BsonIgnoreIfNull]
public string? StackTrace { get; set; }
/// <summary>
/// Inner error details.
/// </summary>
[BsonElement("innerError")]
[BsonIgnoreIfNull]
public string? InnerError { get; set; }
}

View File

@@ -1,59 +0,0 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Options;
namespace StellaOps.Policy.Engine.Storage.Mongo.Internal;
/// <summary>
/// MongoDB context for Policy Engine storage operations.
/// Provides configured access to the database with appropriate read/write concerns.
/// </summary>
internal sealed class PolicyEngineMongoContext
{
public PolicyEngineMongoContext(IOptions<PolicyEngineMongoOptions> options, ILogger<PolicyEngineMongoContext> logger)
{
ArgumentNullException.ThrowIfNull(logger);
var value = options?.Value ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(value.ConnectionString))
{
throw new InvalidOperationException("Policy Engine Mongo connection string is not configured.");
}
if (string.IsNullOrWhiteSpace(value.Database))
{
throw new InvalidOperationException("Policy Engine Mongo database name is not configured.");
}
Client = new MongoClient(value.ConnectionString);
var settings = new MongoDatabaseSettings();
if (value.UseMajorityReadConcern)
{
settings.ReadConcern = ReadConcern.Majority;
}
if (value.UseMajorityWriteConcern)
{
settings.WriteConcern = WriteConcern.WMajority;
}
Database = Client.GetDatabase(value.Database, settings);
Options = value;
}
/// <summary>
/// MongoDB client instance.
/// </summary>
public MongoClient Client { get; }
/// <summary>
/// MongoDB database instance with configured read/write concerns.
/// </summary>
public IMongoDatabase Database { get; }
/// <summary>
/// Policy Engine MongoDB options.
/// </summary>
public PolicyEngineMongoOptions Options { get; }
}

View File

@@ -1,44 +0,0 @@
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Engine.Storage.Mongo.Migrations;
namespace StellaOps.Policy.Engine.Storage.Mongo.Internal;
/// <summary>
/// Interface for Policy Engine MongoDB initialization.
/// </summary>
internal interface IPolicyEngineMongoInitializer
{
/// <summary>
/// Ensures all migrations are applied to the database.
/// </summary>
Task EnsureMigrationsAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Initializes Policy Engine MongoDB storage by applying migrations.
/// </summary>
internal sealed class PolicyEngineMongoInitializer : IPolicyEngineMongoInitializer
{
private readonly PolicyEngineMongoContext _context;
private readonly PolicyEngineMigrationRunner _migrationRunner;
private readonly ILogger<PolicyEngineMongoInitializer> _logger;
public PolicyEngineMongoInitializer(
PolicyEngineMongoContext context,
PolicyEngineMigrationRunner migrationRunner,
ILogger<PolicyEngineMongoInitializer> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_migrationRunner = migrationRunner ?? throw new ArgumentNullException(nameof(migrationRunner));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task EnsureMigrationsAsync(CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Ensuring Policy Engine Mongo migrations are applied for database {Database}.",
_context.Options.Database);
await _migrationRunner.RunAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,69 +0,0 @@
using MongoDB.Driver;
namespace StellaOps.Policy.Engine.Storage.Mongo.Internal;
/// <summary>
/// Builds tenant-scoped filters for Policy Engine MongoDB queries.
/// Ensures all queries are properly scoped to the current tenant.
/// </summary>
internal static class TenantFilterBuilder
{
/// <summary>
/// Creates a filter that matches documents for the specified tenant.
/// </summary>
/// <typeparam name="TDocument">Document type with tenantId field.</typeparam>
/// <param name="tenantId">Tenant identifier (will be normalized to lowercase).</param>
/// <returns>A filter definition scoped to the tenant.</returns>
public static FilterDefinition<TDocument> ForTenant<TDocument>(string tenantId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
var normalizedTenantId = tenantId.ToLowerInvariant();
return Builders<TDocument>.Filter.Eq("tenantId", normalizedTenantId);
}
/// <summary>
/// Combines a tenant filter with an additional filter using AND.
/// </summary>
/// <typeparam name="TDocument">Document type with tenantId field.</typeparam>
/// <param name="tenantId">Tenant identifier (will be normalized to lowercase).</param>
/// <param name="additionalFilter">Additional filter to combine.</param>
/// <returns>A combined filter definition.</returns>
public static FilterDefinition<TDocument> ForTenantAnd<TDocument>(
string tenantId,
FilterDefinition<TDocument> additionalFilter)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(additionalFilter);
var tenantFilter = ForTenant<TDocument>(tenantId);
return Builders<TDocument>.Filter.And(tenantFilter, additionalFilter);
}
/// <summary>
/// Creates a filter that matches documents by ID within a tenant scope.
/// </summary>
/// <typeparam name="TDocument">Document type with tenantId and _id fields.</typeparam>
/// <param name="tenantId">Tenant identifier (will be normalized to lowercase).</param>
/// <param name="documentId">Document identifier.</param>
/// <returns>A filter definition matching both tenant and ID.</returns>
public static FilterDefinition<TDocument> ForTenantById<TDocument>(string tenantId, string documentId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(documentId);
var tenantFilter = ForTenant<TDocument>(tenantId);
var idFilter = Builders<TDocument>.Filter.Eq("_id", documentId);
return Builders<TDocument>.Filter.And(tenantFilter, idFilter);
}
/// <summary>
/// Normalizes a tenant ID to lowercase for consistent storage and queries.
/// </summary>
/// <param name="tenantId">Tenant identifier.</param>
/// <returns>Normalized (lowercase) tenant identifier.</returns>
public static string NormalizeTenantId(string tenantId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
return tenantId.ToLowerInvariant();
}
}

View File

@@ -1,283 +0,0 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Initializes effective_finding_* and effective_finding_history_* collections for a policy.
/// Creates collections and indexes on-demand when a policy is first evaluated.
/// </summary>
internal interface IEffectiveFindingCollectionInitializer
{
/// <summary>
/// Ensures the effective finding collection and indexes exist for a policy.
/// </summary>
/// <param name="policyId">The policy identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask EnsureCollectionAsync(string policyId, CancellationToken cancellationToken);
}
/// <inheritdoc />
internal sealed class EffectiveFindingCollectionInitializer : IEffectiveFindingCollectionInitializer
{
private readonly PolicyEngineMongoContext _context;
private readonly ILogger<EffectiveFindingCollectionInitializer> _logger;
private readonly HashSet<string> _initializedCollections = new(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _lock = new(1, 1);
public EffectiveFindingCollectionInitializer(
PolicyEngineMongoContext context,
ILogger<EffectiveFindingCollectionInitializer> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async ValueTask EnsureCollectionAsync(string policyId, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(policyId);
var findingsCollectionName = _context.Options.GetEffectiveFindingsCollectionName(policyId);
var historyCollectionName = _context.Options.GetEffectiveFindingsHistoryCollectionName(policyId);
// Fast path: already initialized in memory
if (_initializedCollections.Contains(findingsCollectionName))
{
return;
}
await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Double-check after acquiring lock
if (_initializedCollections.Contains(findingsCollectionName))
{
return;
}
await EnsureEffectiveFindingCollectionAsync(findingsCollectionName, cancellationToken).ConfigureAwait(false);
await EnsureEffectiveFindingHistoryCollectionAsync(historyCollectionName, cancellationToken).ConfigureAwait(false);
_initializedCollections.Add(findingsCollectionName);
}
finally
{
_lock.Release();
}
}
private async Task EnsureEffectiveFindingCollectionAsync(string collectionName, CancellationToken cancellationToken)
{
var cursor = await _context.Database
.ListCollectionNamesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
var existing = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
if (!existing.Contains(collectionName, StringComparer.Ordinal))
{
_logger.LogInformation("Creating effective finding collection '{CollectionName}'.", collectionName);
await _context.Database.CreateCollectionAsync(collectionName, cancellationToken: cancellationToken).ConfigureAwait(false);
}
var collection = _context.Database.GetCollection<BsonDocument>(collectionName);
// Unique constraint on (tenantId, componentPurl, advisoryId)
var tenantComponentAdvisory = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("componentPurl")
.Ascending("advisoryId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_component_advisory_unique",
Unique = true
});
// Tenant + severity for filtering by risk level
var tenantSeverity = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("severity")
.Descending("updatedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_severity_updatedAt_desc"
});
// Tenant + status for filtering by policy status
var tenantStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Descending("updatedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_updatedAt_desc"
});
// Product key lookup for SBOM-based queries
var tenantProduct = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("productKey"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_product",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("productKey", true)
});
// SBOM ID lookup
var tenantSbom = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("sbomId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_sbom",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("sbomId", true)
});
// Component name lookup for search
var tenantComponentName = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("componentName"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_componentName"
});
// Advisory ID lookup for cross-policy queries
var tenantAdvisory = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("advisoryId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_advisory"
});
// Policy run reference for traceability
var policyRun = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("policyRunId"),
new CreateIndexOptions<BsonDocument>
{
Name = "policyRun_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("policyRunId", true)
});
// Content hash for deduplication checks
var contentHash = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("contentHash"),
new CreateIndexOptions<BsonDocument>
{
Name = "contentHash_lookup"
});
await collection.Indexes.CreateManyAsync(
new[]
{
tenantComponentAdvisory,
tenantSeverity,
tenantStatus,
tenantProduct,
tenantSbom,
tenantComponentName,
tenantAdvisory,
policyRun,
contentHash
},
cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Created indexes for effective finding collection '{CollectionName}'.", collectionName);
}
private async Task EnsureEffectiveFindingHistoryCollectionAsync(string collectionName, CancellationToken cancellationToken)
{
var cursor = await _context.Database
.ListCollectionNamesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
var existing = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
if (!existing.Contains(collectionName, StringComparer.Ordinal))
{
_logger.LogInformation("Creating effective finding history collection '{CollectionName}'.", collectionName);
await _context.Database.CreateCollectionAsync(collectionName, cancellationToken: cancellationToken).ConfigureAwait(false);
}
var collection = _context.Database.GetCollection<BsonDocument>(collectionName);
// Finding + version for retrieving history
var findingVersion = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("findingId")
.Descending("version"),
new CreateIndexOptions<BsonDocument>
{
Name = "finding_version_desc"
});
// Tenant + occurred for chronological history
var tenantOccurred = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("occurredAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_occurredAt_desc"
});
// Change type lookup for filtering history events
var tenantChangeType = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("changeType"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_changeType"
});
// Policy run reference
var policyRun = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("policyRunId"),
new CreateIndexOptions<BsonDocument>
{
Name = "policyRun_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("policyRunId", true)
});
var models = new List<CreateIndexModel<BsonDocument>>
{
findingVersion,
tenantOccurred,
tenantChangeType,
policyRun
};
// TTL index for automatic cleanup of old history entries
if (_context.Options.EffectiveFindingsHistoryRetention > TimeSpan.Zero)
{
var ttlModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "expiresAt_ttl",
ExpireAfter = TimeSpan.Zero
});
models.Add(ttlModel);
}
await collection.Indexes.CreateManyAsync(models, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Created indexes for effective finding history collection '{CollectionName}'.", collectionName);
}
}

View File

@@ -1,345 +0,0 @@
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Migration to ensure all required indexes exist for exception collections.
/// Creates indexes for efficient tenant-scoped queries and status lookups.
/// </summary>
internal sealed class EnsureExceptionIndexesMigration : IPolicyEngineMongoMigration
{
/// <inheritdoc />
public string Id => "20251128_exception_indexes_v1";
/// <inheritdoc />
public async ValueTask ExecuteAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
await EnsureExceptionsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureExceptionReviewsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureExceptionBindingsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the exceptions collection.
/// </summary>
private static async Task EnsureExceptionsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.ExceptionsCollection);
// Tenant + status for finding active/pending exceptions
var tenantStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status"
});
// Tenant + type + status for filtering
var tenantTypeStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("exceptionType")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_type_status"
});
// Tenant + created descending for recent exceptions
var tenantCreated = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("createdAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_createdAt_desc"
});
// Tenant + tags for filtering by tag
var tenantTags = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("tags"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_tags"
});
// Tenant + expiresAt for finding expiring exceptions
var tenantExpires = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_expiresAt",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("expiresAt", true)
});
// Tenant + effectiveFrom for finding pending activations
var tenantEffectiveFrom = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("effectiveFrom"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_effectiveFrom",
PartialFilterExpression = Builders<BsonDocument>.Filter.Eq("status", "approved")
});
// Scope advisory IDs for finding applicable exceptions
var scopeAdvisoryIds = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("scope.advisoryIds"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_scope_advisoryIds"
});
// Scope asset IDs for finding applicable exceptions
var scopeAssetIds = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("scope.assetIds"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_scope_assetIds"
});
// Scope CVE IDs for finding applicable exceptions
var scopeCveIds = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("scope.cveIds"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_scope_cveIds"
});
// CreatedBy for audit queries
var tenantCreatedBy = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("createdBy"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_createdBy"
});
// Priority for ordering applicable exceptions
var tenantPriority = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Descending("priority"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_priority_desc"
});
// Correlation ID for tracing
var correlationId = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("correlationId"),
new CreateIndexOptions<BsonDocument>
{
Name = "correlationId_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("correlationId", true)
});
await collection.Indexes.CreateManyAsync(
new[]
{
tenantStatus,
tenantTypeStatus,
tenantCreated,
tenantTags,
tenantExpires,
tenantEffectiveFrom,
scopeAdvisoryIds,
scopeAssetIds,
scopeCveIds,
tenantCreatedBy,
tenantPriority,
correlationId
},
cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the exception_reviews collection.
/// </summary>
private static async Task EnsureExceptionReviewsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.ExceptionReviewsCollection);
// Tenant + exception for finding reviews of an exception
var tenantException = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("exceptionId")
.Descending("requestedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_exceptionId_requestedAt_desc"
});
// Tenant + status for finding pending reviews
var tenantStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status"
});
// Tenant + designated reviewers for reviewer's queue
var tenantReviewers = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("designatedReviewers"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_designatedReviewers"
});
// Deadline for finding overdue reviews
var tenantDeadline = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("deadline"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_deadline",
PartialFilterExpression = Builders<BsonDocument>.Filter.And(
Builders<BsonDocument>.Filter.Eq("status", "pending"),
Builders<BsonDocument>.Filter.Exists("deadline", true))
});
// RequestedBy for audit queries
var tenantRequestedBy = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("requestedBy"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_requestedBy"
});
await collection.Indexes.CreateManyAsync(
new[]
{
tenantException,
tenantStatus,
tenantReviewers,
tenantDeadline,
tenantRequestedBy
},
cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the exception_bindings collection.
/// </summary>
private static async Task EnsureExceptionBindingsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.ExceptionBindingsCollection);
// Tenant + exception for finding bindings of an exception
var tenantException = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("exceptionId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_exceptionId"
});
// Tenant + asset for finding bindings for an asset
var tenantAsset = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("assetId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_assetId_status"
});
// Tenant + advisory for finding bindings by advisory
var tenantAdvisory = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("advisoryId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_advisoryId_status",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("advisoryId", true)
});
// Tenant + CVE for finding bindings by CVE
var tenantCve = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("cveId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_cveId_status",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("cveId", true)
});
// Tenant + status + expiresAt for finding expired bindings
var tenantExpires = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status")
.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status_expiresAt",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("expiresAt", true)
});
// Effective time range for finding active bindings at a point in time
var tenantEffectiveRange = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("assetId")
.Ascending("status")
.Ascending("effectiveFrom")
.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_asset_status_effectiveRange"
});
await collection.Indexes.CreateManyAsync(
new[]
{
tenantException,
tenantAsset,
tenantAdvisory,
tenantCve,
tenantExpires,
tenantEffectiveRange
},
cancellationToken: cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,54 +0,0 @@
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Migration to ensure all required Policy Engine collections exist.
/// Creates: policies, policy_revisions, policy_bundles, policy_runs, policy_audit, _policy_migrations
/// Note: effective_finding_* and effective_finding_history_* collections are created dynamically per-policy.
/// </summary>
internal sealed class EnsurePolicyCollectionsMigration : IPolicyEngineMongoMigration
{
private readonly ILogger<EnsurePolicyCollectionsMigration> _logger;
public EnsurePolicyCollectionsMigration(ILogger<EnsurePolicyCollectionsMigration> logger)
=> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
/// <inheritdoc />
public string Id => "20251128_policy_collections_v1";
/// <inheritdoc />
public async ValueTask ExecuteAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var requiredCollections = new[]
{
context.Options.PoliciesCollection,
context.Options.PolicyRevisionsCollection,
context.Options.PolicyBundlesCollection,
context.Options.PolicyRunsCollection,
context.Options.AuditCollection,
context.Options.MigrationsCollection
};
var cursor = await context.Database
.ListCollectionNamesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
var existing = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
foreach (var collection in requiredCollections)
{
if (existing.Contains(collection, StringComparer.Ordinal))
{
continue;
}
_logger.LogInformation("Creating Policy Engine Mongo collection '{CollectionName}'.", collection);
await context.Database.CreateCollectionAsync(collection, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -1,312 +0,0 @@
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Migration to ensure all required indexes exist for Policy Engine collections.
/// Creates indexes for efficient tenant-scoped queries and TTL cleanup.
/// </summary>
internal sealed class EnsurePolicyIndexesMigration : IPolicyEngineMongoMigration
{
/// <inheritdoc />
public string Id => "20251128_policy_indexes_v1";
/// <inheritdoc />
public async ValueTask ExecuteAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
await EnsurePoliciesIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsurePolicyRevisionsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsurePolicyBundlesIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsurePolicyRunsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureAuditIndexesAsync(context, cancellationToken).ConfigureAwait(false);
await EnsureExplainsIndexesAsync(context, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policies collection.
/// </summary>
private static async Task EnsurePoliciesIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.PoliciesCollection);
// Tenant lookup with optional tag filtering
var tenantTags = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("tags"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_tags"
});
// Tenant + updated for recent changes
var tenantUpdated = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("updatedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_updatedAt_desc"
});
await collection.Indexes.CreateManyAsync(new[] { tenantTags, tenantUpdated }, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policy_revisions collection.
/// </summary>
private static async Task EnsurePolicyRevisionsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.PolicyRevisionsCollection);
// Tenant + pack for finding revisions of a policy
var tenantPack = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("packId")
.Descending("version"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_pack_version_desc"
});
// Status lookup for finding active/draft revisions
var tenantStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status"
});
// Bundle digest lookup for integrity verification
var bundleDigest = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("bundleDigest"),
new CreateIndexOptions<BsonDocument>
{
Name = "bundleDigest_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("bundleDigest", true)
});
await collection.Indexes.CreateManyAsync(new[] { tenantPack, tenantStatus, bundleDigest }, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policy_bundles collection.
/// </summary>
private static async Task EnsurePolicyBundlesIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.PolicyBundlesCollection);
// Tenant + pack + version for finding specific bundles
var tenantPackVersion = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("packId")
.Ascending("version"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_pack_version",
Unique = true
});
await collection.Indexes.CreateManyAsync(new[] { tenantPackVersion }, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policy_runs collection.
/// </summary>
private static async Task EnsurePolicyRunsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.PolicyRunsCollection);
// Tenant + policy + started for recent runs
var tenantPolicyStarted = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("policyId")
.Descending("startedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_policy_startedAt_desc"
});
// Status lookup for finding pending/running evaluations
var tenantStatus = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("status"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_status"
});
// Correlation ID lookup for tracing
var correlationId = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("correlationId"),
new CreateIndexOptions<BsonDocument>
{
Name = "correlationId_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("correlationId", true)
});
// Trace ID lookup for OpenTelemetry
var traceId = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("traceId"),
new CreateIndexOptions<BsonDocument>
{
Name = "traceId_lookup",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("traceId", true)
});
var models = new List<CreateIndexModel<BsonDocument>>
{
tenantPolicyStarted,
tenantStatus,
correlationId,
traceId
};
// TTL index for automatic cleanup of completed runs
if (context.Options.PolicyRunRetention > TimeSpan.Zero)
{
var ttlModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "expiresAt_ttl",
ExpireAfter = TimeSpan.Zero
});
models.Add(ttlModel);
}
await collection.Indexes.CreateManyAsync(models, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policy_audit collection.
/// </summary>
private static async Task EnsureAuditIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.AuditCollection);
// Tenant + occurred for chronological audit trail
var tenantOccurred = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Descending("occurredAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_occurredAt_desc"
});
// Actor lookup for finding actions by user
var tenantActor = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("actorId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_actor"
});
// Resource lookup for finding actions on specific policy
var tenantResource = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("resourceType")
.Ascending("resourceId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_resource"
});
await collection.Indexes.CreateManyAsync(new[] { tenantOccurred, tenantActor, tenantResource }, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Creates indexes for the policy_explains collection.
/// </summary>
private static async Task EnsureExplainsIndexesAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken)
{
var collection = context.Database.GetCollection<BsonDocument>(context.Options.PolicyExplainsCollection);
// Tenant + run for finding all explains in a run
var tenantRun = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("runId"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_runId"
});
// Tenant + policy + evaluated time for recent explains
var tenantPolicyEvaluated = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("policyId")
.Descending("evaluatedAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_policy_evaluatedAt_desc"
});
// Subject hash lookup for decision linkage
var subjectHash = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("tenantId")
.Ascending("subjectHash"),
new CreateIndexOptions<BsonDocument>
{
Name = "tenant_subjectHash"
});
// AOC chain lookup for attestation queries
var aocCompilation = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("aocChain.compilationId"),
new CreateIndexOptions<BsonDocument>
{
Name = "aocChain_compilationId",
PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("aocChain.compilationId", true)
});
var models = new List<CreateIndexModel<BsonDocument>>
{
tenantRun,
tenantPolicyEvaluated,
subjectHash,
aocCompilation
};
// TTL index for automatic cleanup
if (context.Options.ExplainTraceRetention > TimeSpan.Zero)
{
var ttlModel = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("expiresAt"),
new CreateIndexOptions<BsonDocument>
{
Name = "expiresAt_ttl",
ExpireAfter = TimeSpan.Zero
});
models.Add(ttlModel);
}
await collection.Indexes.CreateManyAsync(models, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,23 +0,0 @@
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Interface for Policy Engine MongoDB migrations.
/// Migrations are applied in lexical order by Id and tracked to ensure idempotency.
/// </summary>
internal interface IPolicyEngineMongoMigration
{
/// <summary>
/// Unique migration identifier.
/// Format: YYYYMMDD_description_vN (e.g., "20251128_policy_collections_v1")
/// </summary>
string Id { get; }
/// <summary>
/// Executes the migration against the Policy Engine database.
/// </summary>
/// <param name="context">MongoDB context with database access.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask ExecuteAsync(PolicyEngineMongoContext context, CancellationToken cancellationToken);
}

View File

@@ -1,30 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// MongoDB document for tracking applied migrations.
/// Collection: _policy_migrations
/// </summary>
[BsonIgnoreExtraElements]
internal sealed class PolicyEngineMigrationRecord
{
/// <summary>
/// MongoDB ObjectId.
/// </summary>
[BsonId]
public ObjectId Id { get; set; }
/// <summary>
/// Unique migration identifier (matches IPolicyEngineMongoMigration.Id).
/// </summary>
[BsonElement("migrationId")]
public string MigrationId { get; set; } = string.Empty;
/// <summary>
/// When the migration was applied.
/// </summary>
[BsonElement("appliedAt")]
public DateTimeOffset AppliedAt { get; set; }
}

View File

@@ -1,85 +0,0 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
namespace StellaOps.Policy.Engine.Storage.Mongo.Migrations;
/// <summary>
/// Executes Policy Engine MongoDB migrations in order.
/// Tracks applied migrations to ensure idempotency.
/// </summary>
internal sealed class PolicyEngineMigrationRunner
{
private readonly PolicyEngineMongoContext _context;
private readonly IReadOnlyList<IPolicyEngineMongoMigration> _migrations;
private readonly ILogger<PolicyEngineMigrationRunner> _logger;
public PolicyEngineMigrationRunner(
PolicyEngineMongoContext context,
IEnumerable<IPolicyEngineMongoMigration> migrations,
ILogger<PolicyEngineMigrationRunner> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
ArgumentNullException.ThrowIfNull(migrations);
_migrations = migrations.OrderBy(m => m.Id, StringComparer.Ordinal).ToArray();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Runs all pending migrations.
/// </summary>
public async ValueTask RunAsync(CancellationToken cancellationToken)
{
if (_migrations.Count == 0)
{
return;
}
var collection = _context.Database.GetCollection<PolicyEngineMigrationRecord>(_context.Options.MigrationsCollection);
await EnsureMigrationIndexAsync(collection, cancellationToken).ConfigureAwait(false);
var applied = await collection
.Find(FilterDefinition<PolicyEngineMigrationRecord>.Empty)
.Project(record => record.MigrationId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var appliedSet = applied.ToHashSet(StringComparer.Ordinal);
foreach (var migration in _migrations)
{
if (appliedSet.Contains(migration.Id))
{
continue;
}
_logger.LogInformation("Applying Policy Engine Mongo migration {MigrationId}.", migration.Id);
await migration.ExecuteAsync(_context, cancellationToken).ConfigureAwait(false);
var record = new PolicyEngineMigrationRecord
{
Id = ObjectId.GenerateNewId(),
MigrationId = migration.Id,
AppliedAt = DateTimeOffset.UtcNow
};
await collection.InsertOneAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Completed Policy Engine Mongo migration {MigrationId}.", migration.Id);
}
}
private static async Task EnsureMigrationIndexAsync(
IMongoCollection<PolicyEngineMigrationRecord> collection,
CancellationToken cancellationToken)
{
var keys = Builders<PolicyEngineMigrationRecord>.IndexKeys.Ascending(record => record.MigrationId);
var model = new CreateIndexModel<PolicyEngineMigrationRecord>(keys, new CreateIndexOptions
{
Name = "migrationId_unique",
Unique = true
});
await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,140 +0,0 @@
namespace StellaOps.Policy.Engine.Storage.Mongo.Options;
/// <summary>
/// Configures MongoDB connectivity and collection names for Policy Engine storage.
/// </summary>
public sealed class PolicyEngineMongoOptions
{
/// <summary>
/// MongoDB connection string.
/// </summary>
public string ConnectionString { get; set; } = "mongodb://localhost:27017";
/// <summary>
/// Database name for policy storage.
/// </summary>
public string Database { get; set; } = "stellaops_policy";
/// <summary>
/// Collection name for policy packs.
/// </summary>
public string PoliciesCollection { get; set; } = "policies";
/// <summary>
/// Collection name for policy revisions.
/// </summary>
public string PolicyRevisionsCollection { get; set; } = "policy_revisions";
/// <summary>
/// Collection name for policy bundles (compiled artifacts).
/// </summary>
public string PolicyBundlesCollection { get; set; } = "policy_bundles";
/// <summary>
/// Collection name for policy evaluation runs.
/// </summary>
public string PolicyRunsCollection { get; set; } = "policy_runs";
/// <summary>
/// Collection prefix for effective findings (per-policy tenant-scoped).
/// Final collection name: {prefix}_{policyId}
/// </summary>
public string EffectiveFindingsCollectionPrefix { get; set; } = "effective_finding";
/// <summary>
/// Collection prefix for effective findings history (append-only).
/// Final collection name: {prefix}_{policyId}
/// </summary>
public string EffectiveFindingsHistoryCollectionPrefix { get; set; } = "effective_finding_history";
/// <summary>
/// Collection name for policy audit log.
/// </summary>
public string AuditCollection { get; set; } = "policy_audit";
/// <summary>
/// Collection name for policy explain traces.
/// </summary>
public string PolicyExplainsCollection { get; set; } = "policy_explains";
/// <summary>
/// Collection name for policy exceptions.
/// </summary>
public string ExceptionsCollection { get; set; } = "exceptions";
/// <summary>
/// Collection name for exception reviews.
/// </summary>
public string ExceptionReviewsCollection { get; set; } = "exception_reviews";
/// <summary>
/// Collection name for exception bindings.
/// </summary>
public string ExceptionBindingsCollection { get; set; } = "exception_bindings";
/// <summary>
/// Collection name for tracking applied migrations.
/// </summary>
public string MigrationsCollection { get; set; } = "_policy_migrations";
/// <summary>
/// TTL for completed policy runs. Zero or negative disables TTL.
/// </summary>
public TimeSpan PolicyRunRetention { get; set; } = TimeSpan.FromDays(90);
/// <summary>
/// TTL for effective findings history entries. Zero or negative disables TTL.
/// </summary>
public TimeSpan EffectiveFindingsHistoryRetention { get; set; } = TimeSpan.FromDays(365);
/// <summary>
/// TTL for explain traces. Zero or negative disables TTL.
/// </summary>
public TimeSpan ExplainTraceRetention { get; set; } = TimeSpan.FromDays(30);
/// <summary>
/// Use majority read concern for consistency.
/// </summary>
public bool UseMajorityReadConcern { get; set; } = true;
/// <summary>
/// Use majority write concern for durability.
/// </summary>
public bool UseMajorityWriteConcern { get; set; } = true;
/// <summary>
/// Command timeout in seconds.
/// </summary>
public int CommandTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Gets the effective findings collection name for a policy.
/// </summary>
public string GetEffectiveFindingsCollectionName(string policyId)
{
var safePolicyId = SanitizeCollectionName(policyId);
return $"{EffectiveFindingsCollectionPrefix}_{safePolicyId}";
}
/// <summary>
/// Gets the effective findings history collection name for a policy.
/// </summary>
public string GetEffectiveFindingsHistoryCollectionName(string policyId)
{
var safePolicyId = SanitizeCollectionName(policyId);
return $"{EffectiveFindingsHistoryCollectionPrefix}_{safePolicyId}";
}
private static string SanitizeCollectionName(string name)
{
// Replace invalid characters with underscores
return string.Create(name.Length, name, (span, source) =>
{
for (int i = 0; i < source.Length; i++)
{
var c = source[i];
span[i] = char.IsLetterOrDigit(c) || c == '_' || c == '-' ? c : '_';
}
}).ToLowerInvariant();
}
}

View File

@@ -1,261 +0,0 @@
using System.Collections.Immutable;
using StellaOps.Policy.Engine.Storage.Mongo.Documents;
namespace StellaOps.Policy.Engine.Storage.Mongo.Repositories;
/// <summary>
/// Repository interface for policy exception operations.
/// </summary>
internal interface IExceptionRepository
{
// Exception operations
/// <summary>
/// Creates a new exception.
/// </summary>
Task<PolicyExceptionDocument> CreateExceptionAsync(
PolicyExceptionDocument exception,
CancellationToken cancellationToken);
/// <summary>
/// Gets an exception by ID.
/// </summary>
Task<PolicyExceptionDocument?> GetExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken);
/// <summary>
/// Updates an existing exception.
/// </summary>
Task<PolicyExceptionDocument?> UpdateExceptionAsync(
PolicyExceptionDocument exception,
CancellationToken cancellationToken);
/// <summary>
/// Lists exceptions for a tenant with filtering and pagination.
/// </summary>
Task<ImmutableArray<PolicyExceptionDocument>> ListExceptionsAsync(
string tenantId,
ExceptionQueryOptions options,
CancellationToken cancellationToken);
/// <summary>
/// Lists exceptions across all tenants with filtering and pagination.
/// </summary>
Task<ImmutableArray<PolicyExceptionDocument>> ListExceptionsAsync(
ExceptionQueryOptions options,
CancellationToken cancellationToken);
/// <summary>
/// Finds active exceptions that apply to a specific asset/advisory.
/// </summary>
Task<ImmutableArray<PolicyExceptionDocument>> FindApplicableExceptionsAsync(
string tenantId,
string assetId,
string? advisoryId,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken);
/// <summary>
/// Updates exception status.
/// </summary>
Task<bool> UpdateExceptionStatusAsync(
string tenantId,
string exceptionId,
string newStatus,
DateTimeOffset timestamp,
CancellationToken cancellationToken);
/// <summary>
/// Revokes an exception.
/// </summary>
Task<bool> RevokeExceptionAsync(
string tenantId,
string exceptionId,
string revokedBy,
string? reason,
DateTimeOffset timestamp,
CancellationToken cancellationToken);
/// <summary>
/// Gets exceptions expiring within a time window.
/// </summary>
Task<ImmutableArray<PolicyExceptionDocument>> GetExpiringExceptionsAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken);
/// <summary>
/// Gets exceptions that should be auto-activated.
/// </summary>
Task<ImmutableArray<PolicyExceptionDocument>> GetPendingActivationsAsync(
string tenantId,
DateTimeOffset asOf,
CancellationToken cancellationToken);
// Review operations
/// <summary>
/// Creates a new review for an exception.
/// </summary>
Task<ExceptionReviewDocument> CreateReviewAsync(
ExceptionReviewDocument review,
CancellationToken cancellationToken);
/// <summary>
/// Gets a review by ID.
/// </summary>
Task<ExceptionReviewDocument?> GetReviewAsync(
string tenantId,
string reviewId,
CancellationToken cancellationToken);
/// <summary>
/// Adds a decision to a review.
/// </summary>
Task<ExceptionReviewDocument?> AddReviewDecisionAsync(
string tenantId,
string reviewId,
ReviewDecisionDocument decision,
CancellationToken cancellationToken);
/// <summary>
/// Completes a review with final status.
/// </summary>
Task<ExceptionReviewDocument?> CompleteReviewAsync(
string tenantId,
string reviewId,
string finalStatus,
DateTimeOffset completedAt,
CancellationToken cancellationToken);
/// <summary>
/// Gets reviews for an exception.
/// </summary>
Task<ImmutableArray<ExceptionReviewDocument>> GetReviewsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken);
/// <summary>
/// Gets pending reviews for a reviewer.
/// </summary>
Task<ImmutableArray<ExceptionReviewDocument>> GetPendingReviewsAsync(
string tenantId,
string? reviewerId,
CancellationToken cancellationToken);
// Binding operations
/// <summary>
/// Creates or updates a binding.
/// </summary>
Task<ExceptionBindingDocument> UpsertBindingAsync(
ExceptionBindingDocument binding,
CancellationToken cancellationToken);
/// <summary>
/// Gets bindings for an exception.
/// </summary>
Task<ImmutableArray<ExceptionBindingDocument>> GetBindingsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken);
/// <summary>
/// Gets active bindings for an asset.
/// </summary>
Task<ImmutableArray<ExceptionBindingDocument>> GetActiveBindingsForAssetAsync(
string tenantId,
string assetId,
DateTimeOffset asOf,
CancellationToken cancellationToken);
/// <summary>
/// Deletes bindings for an exception.
/// </summary>
Task<long> DeleteBindingsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken);
/// <summary>
/// Updates binding status.
/// </summary>
Task<bool> UpdateBindingStatusAsync(
string tenantId,
string bindingId,
string newStatus,
CancellationToken cancellationToken);
/// <summary>
/// Gets expired bindings for cleanup.
/// </summary>
Task<ImmutableArray<ExceptionBindingDocument>> GetExpiredBindingsAsync(
string tenantId,
DateTimeOffset asOf,
int limit,
CancellationToken cancellationToken);
// Statistics
/// <summary>
/// Gets exception counts by status.
/// </summary>
Task<IReadOnlyDictionary<string, int>> GetExceptionCountsByStatusAsync(
string tenantId,
CancellationToken cancellationToken);
}
/// <summary>
/// Query options for listing exceptions.
/// </summary>
public sealed record ExceptionQueryOptions
{
/// <summary>
/// Filter by status.
/// </summary>
public ImmutableArray<string> Statuses { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Filter by exception type.
/// </summary>
public ImmutableArray<string> Types { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Filter by tag.
/// </summary>
public ImmutableArray<string> Tags { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Filter by creator.
/// </summary>
public string? CreatedBy { get; init; }
/// <summary>
/// Include expired exceptions.
/// </summary>
public bool IncludeExpired { get; init; }
/// <summary>
/// Skip count for pagination.
/// </summary>
public int Skip { get; init; }
/// <summary>
/// Limit for pagination (default 100).
/// </summary>
public int Limit { get; init; } = 100;
/// <summary>
/// Sort field.
/// </summary>
public string SortBy { get; init; } = "createdAt";
/// <summary>
/// Sort direction (asc or desc).
/// </summary>
public string SortDirection { get; init; } = "desc";
}

View File

@@ -1,647 +0,0 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Storage.Mongo.Documents;
using StellaOps.Policy.Engine.Storage.Mongo.Options;
using StellaOps.Policy.Engine.Telemetry;
namespace StellaOps.Policy.Engine.Storage.Mongo.Repositories;
/// <summary>
/// MongoDB implementation of the exception repository.
/// </summary>
internal sealed class MongoExceptionRepository : IExceptionRepository
{
private readonly IMongoDatabase _database;
private readonly PolicyEngineMongoOptions _options;
private readonly ILogger<MongoExceptionRepository> _logger;
public MongoExceptionRepository(
IMongoClient mongoClient,
IOptions<PolicyEngineMongoOptions> options,
ILogger<MongoExceptionRepository> logger)
{
ArgumentNullException.ThrowIfNull(mongoClient);
ArgumentNullException.ThrowIfNull(options);
_options = options.Value;
_database = mongoClient.GetDatabase(_options.Database);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private IMongoCollection<PolicyExceptionDocument> Exceptions
=> _database.GetCollection<PolicyExceptionDocument>(_options.ExceptionsCollection);
private IMongoCollection<ExceptionReviewDocument> Reviews
=> _database.GetCollection<ExceptionReviewDocument>(_options.ExceptionReviewsCollection);
private IMongoCollection<ExceptionBindingDocument> Bindings
=> _database.GetCollection<ExceptionBindingDocument>(_options.ExceptionBindingsCollection);
#region Exception Operations
public async Task<PolicyExceptionDocument> CreateExceptionAsync(
PolicyExceptionDocument exception,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(exception);
exception.TenantId = exception.TenantId.ToLowerInvariant();
await Exceptions.InsertOneAsync(exception, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Created exception {ExceptionId} for tenant {TenantId}",
exception.Id, exception.TenantId);
PolicyEngineTelemetry.RecordExceptionOperation(exception.TenantId, "create");
return exception;
}
public async Task<PolicyExceptionDocument?> GetExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken)
{
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Id, exceptionId));
return await Exceptions.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<PolicyExceptionDocument?> UpdateExceptionAsync(
PolicyExceptionDocument exception,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(exception);
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, exception.TenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Id, exception.Id));
var result = await Exceptions.ReplaceOneAsync(filter, exception, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (result.ModifiedCount > 0)
{
_logger.LogInformation(
"Updated exception {ExceptionId} for tenant {TenantId}",
exception.Id, exception.TenantId);
PolicyEngineTelemetry.RecordExceptionOperation(exception.TenantId, "update");
return exception;
}
return null;
}
public async Task<ImmutableArray<PolicyExceptionDocument>> ListExceptionsAsync(
string tenantId,
ExceptionQueryOptions options,
CancellationToken cancellationToken)
{
var filter = BuildFilter(options, tenantId.ToLowerInvariant());
var sort = BuildSort(options);
var results = await Exceptions
.Find(filter)
.Sort(sort)
.Skip(options.Skip)
.Limit(options.Limit)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<ImmutableArray<PolicyExceptionDocument>> ListExceptionsAsync(
ExceptionQueryOptions options,
CancellationToken cancellationToken)
{
var filter = BuildFilter(options, tenantId: null);
var sort = BuildSort(options);
var results = await Exceptions
.Find(filter)
.Sort(sort)
.Skip(options.Skip)
.Limit(options.Limit)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
private static FilterDefinition<PolicyExceptionDocument> BuildFilter(
ExceptionQueryOptions options,
string? tenantId)
{
var filterBuilder = Builders<PolicyExceptionDocument>.Filter;
var filters = new List<FilterDefinition<PolicyExceptionDocument>>();
if (!string.IsNullOrWhiteSpace(tenantId))
{
filters.Add(filterBuilder.Eq(e => e.TenantId, tenantId));
}
if (options.Statuses.Length > 0)
{
filters.Add(filterBuilder.In(e => e.Status, options.Statuses));
}
if (options.Types.Length > 0)
{
filters.Add(filterBuilder.In(e => e.ExceptionType, options.Types));
}
if (options.Tags.Length > 0)
{
filters.Add(filterBuilder.AnyIn(e => e.Tags, options.Tags));
}
if (!string.IsNullOrEmpty(options.CreatedBy))
{
filters.Add(filterBuilder.Eq(e => e.CreatedBy, options.CreatedBy));
}
if (!options.IncludeExpired)
{
var now = DateTimeOffset.UtcNow;
filters.Add(filterBuilder.Or(
filterBuilder.Eq(e => e.ExpiresAt, null),
filterBuilder.Gt(e => e.ExpiresAt, now)));
}
if (filters.Count == 0)
{
return FilterDefinition<PolicyExceptionDocument>.Empty;
}
return filterBuilder.And(filters);
}
private static SortDefinition<PolicyExceptionDocument> BuildSort(ExceptionQueryOptions options)
{
return options.SortDirection.Equals("asc", StringComparison.OrdinalIgnoreCase)
? Builders<PolicyExceptionDocument>.Sort.Ascending(options.SortBy)
: Builders<PolicyExceptionDocument>.Sort.Descending(options.SortBy);
}
public async Task<ImmutableArray<PolicyExceptionDocument>> FindApplicableExceptionsAsync(
string tenantId,
string assetId,
string? advisoryId,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken)
{
var filterBuilder = Builders<PolicyExceptionDocument>.Filter;
var filters = new List<FilterDefinition<PolicyExceptionDocument>>
{
filterBuilder.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
filterBuilder.Eq(e => e.Status, "active"),
filterBuilder.Or(
filterBuilder.Eq(e => e.EffectiveFrom, null),
filterBuilder.Lte(e => e.EffectiveFrom, evaluationTime)),
filterBuilder.Or(
filterBuilder.Eq(e => e.ExpiresAt, null),
filterBuilder.Gt(e => e.ExpiresAt, evaluationTime))
};
// Scope matching - must match at least one criterion
var scopeFilters = new List<FilterDefinition<PolicyExceptionDocument>>
{
filterBuilder.Eq("scope.applyToAll", true),
filterBuilder.AnyEq("scope.assetIds", assetId)
};
// Add PURL pattern matching (simplified - would need regex in production)
scopeFilters.Add(filterBuilder.Not(filterBuilder.Size("scope.purlPatterns", 0)));
if (!string.IsNullOrEmpty(advisoryId))
{
scopeFilters.Add(filterBuilder.AnyEq("scope.advisoryIds", advisoryId));
}
filters.Add(filterBuilder.Or(scopeFilters));
var filter = filterBuilder.And(filters);
var results = await Exceptions
.Find(filter)
.Sort(Builders<PolicyExceptionDocument>.Sort.Descending(e => e.Priority))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<bool> UpdateExceptionStatusAsync(
string tenantId,
string exceptionId,
string newStatus,
DateTimeOffset timestamp,
CancellationToken cancellationToken)
{
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Id, exceptionId));
var updateBuilder = Builders<PolicyExceptionDocument>.Update;
var updates = new List<UpdateDefinition<PolicyExceptionDocument>>
{
updateBuilder.Set(e => e.Status, newStatus),
updateBuilder.Set(e => e.UpdatedAt, timestamp)
};
if (newStatus == "active")
{
updates.Add(updateBuilder.Set(e => e.ActivatedAt, timestamp));
}
var update = updateBuilder.Combine(updates);
var result = await Exceptions.UpdateOneAsync(filter, update, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (result.ModifiedCount > 0)
{
_logger.LogInformation(
"Updated exception {ExceptionId} status to {Status} for tenant {TenantId}",
exceptionId, newStatus, tenantId);
PolicyEngineTelemetry.RecordExceptionOperation(tenantId, $"status_{newStatus}");
}
return result.ModifiedCount > 0;
}
public async Task<bool> RevokeExceptionAsync(
string tenantId,
string exceptionId,
string revokedBy,
string? reason,
DateTimeOffset timestamp,
CancellationToken cancellationToken)
{
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Id, exceptionId));
var update = Builders<PolicyExceptionDocument>.Update
.Set(e => e.Status, "revoked")
.Set(e => e.RevokedAt, timestamp)
.Set(e => e.RevokedBy, revokedBy)
.Set(e => e.RevocationReason, reason)
.Set(e => e.UpdatedAt, timestamp);
var result = await Exceptions.UpdateOneAsync(filter, update, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (result.ModifiedCount > 0)
{
_logger.LogInformation(
"Revoked exception {ExceptionId} by {RevokedBy} for tenant {TenantId}",
exceptionId, revokedBy, tenantId);
PolicyEngineTelemetry.RecordExceptionOperation(tenantId, "revoke");
}
return result.ModifiedCount > 0;
}
public async Task<ImmutableArray<PolicyExceptionDocument>> GetExpiringExceptionsAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken)
{
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Status, "active"),
Builders<PolicyExceptionDocument>.Filter.Gte(e => e.ExpiresAt, from),
Builders<PolicyExceptionDocument>.Filter.Lte(e => e.ExpiresAt, to));
var results = await Exceptions
.Find(filter)
.Sort(Builders<PolicyExceptionDocument>.Sort.Ascending(e => e.ExpiresAt))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<ImmutableArray<PolicyExceptionDocument>> GetPendingActivationsAsync(
string tenantId,
DateTimeOffset asOf,
CancellationToken cancellationToken)
{
var filter = Builders<PolicyExceptionDocument>.Filter.And(
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.TenantId, tenantId.ToLowerInvariant()),
Builders<PolicyExceptionDocument>.Filter.Eq(e => e.Status, "approved"),
Builders<PolicyExceptionDocument>.Filter.Lte(e => e.EffectiveFrom, asOf));
var results = await Exceptions
.Find(filter)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
#endregion
#region Review Operations
public async Task<ExceptionReviewDocument> CreateReviewAsync(
ExceptionReviewDocument review,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(review);
review.TenantId = review.TenantId.ToLowerInvariant();
await Reviews.InsertOneAsync(review, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Created review {ReviewId} for exception {ExceptionId}, tenant {TenantId}",
review.Id, review.ExceptionId, review.TenantId);
PolicyEngineTelemetry.RecordExceptionOperation(review.TenantId, "review_create");
return review;
}
public async Task<ExceptionReviewDocument?> GetReviewAsync(
string tenantId,
string reviewId,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionReviewDocument>.Filter.And(
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.Id, reviewId));
return await Reviews.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<ExceptionReviewDocument?> AddReviewDecisionAsync(
string tenantId,
string reviewId,
ReviewDecisionDocument decision,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionReviewDocument>.Filter.And(
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.Id, reviewId),
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.Status, "pending"));
var update = Builders<ExceptionReviewDocument>.Update
.Push(r => r.Decisions, decision);
var options = new FindOneAndUpdateOptions<ExceptionReviewDocument>
{
ReturnDocument = ReturnDocument.After
};
var result = await Reviews.FindOneAndUpdateAsync(filter, update, options, cancellationToken)
.ConfigureAwait(false);
if (result is not null)
{
_logger.LogInformation(
"Added decision from {ReviewerId} to review {ReviewId} for tenant {TenantId}",
decision.ReviewerId, reviewId, tenantId);
PolicyEngineTelemetry.RecordExceptionOperation(tenantId, $"review_decision_{decision.Decision}");
}
return result;
}
public async Task<ExceptionReviewDocument?> CompleteReviewAsync(
string tenantId,
string reviewId,
string finalStatus,
DateTimeOffset completedAt,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionReviewDocument>.Filter.And(
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.Id, reviewId));
var update = Builders<ExceptionReviewDocument>.Update
.Set(r => r.Status, finalStatus)
.Set(r => r.CompletedAt, completedAt);
var options = new FindOneAndUpdateOptions<ExceptionReviewDocument>
{
ReturnDocument = ReturnDocument.After
};
var result = await Reviews.FindOneAndUpdateAsync(filter, update, options, cancellationToken)
.ConfigureAwait(false);
if (result is not null)
{
_logger.LogInformation(
"Completed review {ReviewId} with status {Status} for tenant {TenantId}",
reviewId, finalStatus, tenantId);
PolicyEngineTelemetry.RecordExceptionOperation(tenantId, $"review_complete_{finalStatus}");
}
return result;
}
public async Task<ImmutableArray<ExceptionReviewDocument>> GetReviewsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionReviewDocument>.Filter.And(
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionReviewDocument>.Filter.Eq(r => r.ExceptionId, exceptionId));
var results = await Reviews
.Find(filter)
.Sort(Builders<ExceptionReviewDocument>.Sort.Descending(r => r.RequestedAt))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<ImmutableArray<ExceptionReviewDocument>> GetPendingReviewsAsync(
string tenantId,
string? reviewerId,
CancellationToken cancellationToken)
{
var filterBuilder = Builders<ExceptionReviewDocument>.Filter;
var filters = new List<FilterDefinition<ExceptionReviewDocument>>
{
filterBuilder.Eq(r => r.TenantId, tenantId.ToLowerInvariant()),
filterBuilder.Eq(r => r.Status, "pending")
};
if (!string.IsNullOrEmpty(reviewerId))
{
filters.Add(filterBuilder.AnyEq(r => r.DesignatedReviewers, reviewerId));
}
var filter = filterBuilder.And(filters);
var results = await Reviews
.Find(filter)
.Sort(Builders<ExceptionReviewDocument>.Sort.Ascending(r => r.Deadline))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
#endregion
#region Binding Operations
public async Task<ExceptionBindingDocument> UpsertBindingAsync(
ExceptionBindingDocument binding,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(binding);
binding.TenantId = binding.TenantId.ToLowerInvariant();
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, binding.TenantId),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.Id, binding.Id));
var options = new ReplaceOptions { IsUpsert = true };
await Bindings.ReplaceOneAsync(filter, binding, options, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Upserted binding {BindingId} for tenant {TenantId}",
binding.Id, binding.TenantId);
return binding;
}
public async Task<ImmutableArray<ExceptionBindingDocument>> GetBindingsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.ExceptionId, exceptionId));
var results = await Bindings
.Find(filter)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<ImmutableArray<ExceptionBindingDocument>> GetActiveBindingsForAssetAsync(
string tenantId,
string assetId,
DateTimeOffset asOf,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.AssetId, assetId),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.Status, "active"),
Builders<ExceptionBindingDocument>.Filter.Lte(b => b.EffectiveFrom, asOf),
Builders<ExceptionBindingDocument>.Filter.Or(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.ExpiresAt, null),
Builders<ExceptionBindingDocument>.Filter.Gt(b => b.ExpiresAt, asOf)));
var results = await Bindings
.Find(filter)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
public async Task<long> DeleteBindingsForExceptionAsync(
string tenantId,
string exceptionId,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.ExceptionId, exceptionId));
var result = await Bindings.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Deleted {Count} bindings for exception {ExceptionId} tenant {TenantId}",
result.DeletedCount, exceptionId, tenantId);
return result.DeletedCount;
}
public async Task<bool> UpdateBindingStatusAsync(
string tenantId,
string bindingId,
string newStatus,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.Id, bindingId));
var update = Builders<ExceptionBindingDocument>.Update.Set(b => b.Status, newStatus);
var result = await Bindings.UpdateOneAsync(filter, update, cancellationToken: cancellationToken)
.ConfigureAwait(false);
return result.ModifiedCount > 0;
}
public async Task<ImmutableArray<ExceptionBindingDocument>> GetExpiredBindingsAsync(
string tenantId,
DateTimeOffset asOf,
int limit,
CancellationToken cancellationToken)
{
var filter = Builders<ExceptionBindingDocument>.Filter.And(
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.TenantId, tenantId.ToLowerInvariant()),
Builders<ExceptionBindingDocument>.Filter.Eq(b => b.Status, "active"),
Builders<ExceptionBindingDocument>.Filter.Lt(b => b.ExpiresAt, asOf));
var results = await Bindings
.Find(filter)
.Limit(limit)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToImmutableArray();
}
#endregion
#region Statistics
public async Task<IReadOnlyDictionary<string, int>> GetExceptionCountsByStatusAsync(
string tenantId,
CancellationToken cancellationToken)
{
var pipeline = new BsonDocument[]
{
new("$match", new BsonDocument("tenantId", tenantId.ToLowerInvariant())),
new("$group", new BsonDocument
{
{ "_id", "$status" },
{ "count", new BsonDocument("$sum", 1) }
})
};
var results = await Exceptions
.Aggregate<BsonDocument>(pipeline, cancellationToken: cancellationToken)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results.ToDictionary(
r => r["_id"].AsString,
r => r["count"].AsInt32);
}
#endregion
}

View File

@@ -1,496 +0,0 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
// Alias to disambiguate from StellaOps.Policy.PolicyDocument (compiled policy IR)
using PolicyPackDocument = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyDocument;
using PolicyRevisionDoc = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyRevisionDocument;
using PolicyBundleDoc = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyBundleDocument;
using PolicyApprovalRec = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyApprovalRecord;
using PolicyAocMetadataDoc = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyAocMetadataDocument;
using PolicyProvenanceDoc = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyProvenanceDocument;
using PolicyAttestationRefDoc = StellaOps.Policy.Engine.Storage.Mongo.Documents.PolicyAttestationRefDocument;
namespace StellaOps.Policy.Engine.Storage.Mongo.Repositories;
/// <summary>
/// MongoDB implementation of policy pack repository with tenant scoping.
/// </summary>
internal sealed class MongoPolicyPackRepository : IPolicyPackRepository
{
private readonly PolicyEngineMongoContext _context;
private readonly ILogger<MongoPolicyPackRepository> _logger;
private readonly TimeProvider _timeProvider;
private readonly string _tenantId;
public MongoPolicyPackRepository(
PolicyEngineMongoContext context,
ILogger<MongoPolicyPackRepository> logger,
TimeProvider timeProvider,
string tenantId)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_tenantId = tenantId?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(tenantId));
}
private IMongoCollection<PolicyPackDocument> Policies =>
_context.Database.GetCollection<PolicyPackDocument>(_context.Options.PoliciesCollection);
private IMongoCollection<PolicyRevisionDoc> Revisions =>
_context.Database.GetCollection<PolicyRevisionDoc>(_context.Options.PolicyRevisionsCollection);
private IMongoCollection<PolicyBundleDoc> Bundles =>
_context.Database.GetCollection<PolicyBundleDoc>(_context.Options.PolicyBundlesCollection);
/// <inheritdoc />
public async Task<PolicyPackRecord> CreateAsync(string packId, string? displayName, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(packId);
var now = _timeProvider.GetUtcNow();
var document = new PolicyPackDocument
{
Id = packId,
TenantId = _tenantId,
DisplayName = displayName,
LatestVersion = 0,
CreatedAt = now,
UpdatedAt = now
};
try
{
await Policies.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Created policy pack {PackId} for tenant {TenantId}", packId, _tenantId);
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
_logger.LogDebug("Policy pack {PackId} already exists for tenant {TenantId}", packId, _tenantId);
var existing = await Policies.Find(p => p.Id == packId && p.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (existing is null)
{
throw new InvalidOperationException($"Policy pack {packId} exists but not for tenant {_tenantId}");
}
return ToDomain(existing);
}
return ToDomain(document);
}
/// <inheritdoc />
public async Task<IReadOnlyList<PolicyPackRecord>> ListAsync(CancellationToken cancellationToken)
{
var documents = await Policies
.Find(p => p.TenantId == _tenantId)
.SortBy(p => p.Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return documents.Select(ToDomain).ToList().AsReadOnly();
}
/// <inheritdoc />
public async Task<PolicyRevisionRecord> UpsertRevisionAsync(
string packId,
int version,
bool requiresTwoPersonApproval,
PolicyRevisionStatus initialStatus,
CancellationToken cancellationToken)
{
var now = _timeProvider.GetUtcNow();
// Ensure pack exists
var pack = await Policies.Find(p => p.Id == packId && p.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (pack is null)
{
pack = new PolicyPackDocument
{
Id = packId,
TenantId = _tenantId,
LatestVersion = 0,
CreatedAt = now,
UpdatedAt = now
};
try
{
await Policies.InsertOneAsync(pack, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
pack = await Policies.Find(p => p.Id == packId && p.TenantId == _tenantId)
.FirstAsync(cancellationToken)
.ConfigureAwait(false);
}
}
// Determine version
var targetVersion = version > 0 ? version : pack.LatestVersion + 1;
var revisionId = PolicyRevisionDoc.CreateId(packId, targetVersion);
// Upsert revision
var filter = Builders<PolicyRevisionDoc>.Filter.Eq(r => r.Id, revisionId);
var update = Builders<PolicyRevisionDoc>.Update
.SetOnInsert(r => r.Id, revisionId)
.SetOnInsert(r => r.TenantId, _tenantId)
.SetOnInsert(r => r.PackId, packId)
.SetOnInsert(r => r.Version, targetVersion)
.SetOnInsert(r => r.RequiresTwoPersonApproval, requiresTwoPersonApproval)
.SetOnInsert(r => r.CreatedAt, now)
.Set(r => r.Status, initialStatus.ToString());
var options = new FindOneAndUpdateOptions<PolicyRevisionDoc>
{
IsUpsert = true,
ReturnDocument = ReturnDocument.After
};
var revision = await Revisions.FindOneAndUpdateAsync(filter, update, options, cancellationToken)
.ConfigureAwait(false);
// Update pack latest version
if (targetVersion > pack.LatestVersion)
{
await Policies.UpdateOneAsync(
p => p.Id == packId && p.TenantId == _tenantId,
Builders<PolicyPackDocument>.Update
.Set(p => p.LatestVersion, targetVersion)
.Set(p => p.UpdatedAt, now),
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
_logger.LogDebug(
"Upserted revision {PackId}:{Version} for tenant {TenantId}",
packId, targetVersion, _tenantId);
return ToDomain(revision);
}
/// <inheritdoc />
public async Task<PolicyRevisionRecord?> GetRevisionAsync(string packId, int version, CancellationToken cancellationToken)
{
var revisionId = PolicyRevisionDoc.CreateId(packId, version);
var revision = await Revisions
.Find(r => r.Id == revisionId && r.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revision is null)
{
return null;
}
// Load bundle if referenced
PolicyBundleDoc? bundle = null;
if (!string.IsNullOrEmpty(revision.BundleId))
{
bundle = await Bundles
.Find(b => b.Id == revision.BundleId && b.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
}
return ToDomain(revision, bundle);
}
/// <inheritdoc />
public async Task<PolicyActivationResult> RecordActivationAsync(
string packId,
int version,
string actorId,
DateTimeOffset timestamp,
string? comment,
CancellationToken cancellationToken)
{
var revisionId = PolicyRevisionDoc.CreateId(packId, version);
// Get current revision
var revision = await Revisions
.Find(r => r.Id == revisionId && r.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revision is null)
{
var pack = await Policies.Find(p => p.Id == packId && p.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
return pack is null
? new PolicyActivationResult(PolicyActivationResultStatus.PackNotFound, null)
: new PolicyActivationResult(PolicyActivationResultStatus.RevisionNotFound, null);
}
if (revision.Status == PolicyRevisionStatus.Active.ToString())
{
return new PolicyActivationResult(PolicyActivationResultStatus.AlreadyActive, ToDomain(revision));
}
if (revision.Status != PolicyRevisionStatus.Approved.ToString())
{
return new PolicyActivationResult(PolicyActivationResultStatus.NotApproved, ToDomain(revision));
}
// Check for duplicate approval
if (revision.Approvals.Any(a => a.ActorId.Equals(actorId, StringComparison.OrdinalIgnoreCase)))
{
return new PolicyActivationResult(PolicyActivationResultStatus.DuplicateApproval, ToDomain(revision));
}
// Add approval
var approval = new PolicyApprovalRec
{
ActorId = actorId,
ApprovedAt = timestamp,
Comment = comment
};
var approvalUpdate = Builders<PolicyRevisionDoc>.Update.Push(r => r.Approvals, approval);
await Revisions.UpdateOneAsync(r => r.Id == revisionId, approvalUpdate, cancellationToken: cancellationToken)
.ConfigureAwait(false);
revision.Approvals.Add(approval);
// Check if we have enough approvals
var approvalCount = revision.Approvals.Count;
if (revision.RequiresTwoPersonApproval && approvalCount < 2)
{
return new PolicyActivationResult(PolicyActivationResultStatus.PendingSecondApproval, ToDomain(revision));
}
// Activate
var activateUpdate = Builders<PolicyRevisionDoc>.Update
.Set(r => r.Status, PolicyRevisionStatus.Active.ToString())
.Set(r => r.ActivatedAt, timestamp);
await Revisions.UpdateOneAsync(r => r.Id == revisionId, activateUpdate, cancellationToken: cancellationToken)
.ConfigureAwait(false);
// Update pack active version
await Policies.UpdateOneAsync(
p => p.Id == packId && p.TenantId == _tenantId,
Builders<PolicyPackDocument>.Update
.Set(p => p.ActiveVersion, version)
.Set(p => p.UpdatedAt, timestamp),
cancellationToken: cancellationToken)
.ConfigureAwait(false);
revision.Status = PolicyRevisionStatus.Active.ToString();
revision.ActivatedAt = timestamp;
_logger.LogInformation(
"Activated revision {PackId}:{Version} for tenant {TenantId} by {ActorId}",
packId, version, _tenantId, actorId);
return new PolicyActivationResult(PolicyActivationResultStatus.Activated, ToDomain(revision));
}
/// <inheritdoc />
public async Task<PolicyBundleRecord> StoreBundleAsync(
string packId,
int version,
PolicyBundleRecord bundle,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(bundle);
var now = _timeProvider.GetUtcNow();
// Ensure revision exists
await UpsertRevisionAsync(packId, version, requiresTwoPersonApproval: false, PolicyRevisionStatus.Draft, cancellationToken)
.ConfigureAwait(false);
// Create bundle document
var bundleDoc = new PolicyBundleDoc
{
Id = bundle.Digest,
TenantId = _tenantId,
PackId = packId,
Version = version,
Signature = bundle.Signature,
SizeBytes = bundle.Size,
Payload = bundle.Payload.ToArray(),
CreatedAt = bundle.CreatedAt,
AocMetadata = bundle.AocMetadata is not null ? ToDocument(bundle.AocMetadata) : null
};
// Upsert bundle
await Bundles.ReplaceOneAsync(
b => b.Id == bundle.Digest && b.TenantId == _tenantId,
bundleDoc,
new ReplaceOptions { IsUpsert = true },
cancellationToken)
.ConfigureAwait(false);
// Link revision to bundle
var revisionId = PolicyRevisionDoc.CreateId(packId, version);
await Revisions.UpdateOneAsync(
r => r.Id == revisionId && r.TenantId == _tenantId,
Builders<PolicyRevisionDoc>.Update
.Set(r => r.BundleId, bundle.Digest)
.Set(r => r.BundleDigest, bundle.Digest),
cancellationToken: cancellationToken)
.ConfigureAwait(false);
_logger.LogDebug(
"Stored bundle {Digest} for {PackId}:{Version} tenant {TenantId}",
bundle.Digest, packId, version, _tenantId);
return bundle;
}
/// <inheritdoc />
public async Task<PolicyBundleRecord?> GetBundleAsync(string packId, int version, CancellationToken cancellationToken)
{
var bundle = await Bundles
.Find(b => b.PackId == packId && b.Version == version && b.TenantId == _tenantId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
return bundle is null ? null : ToDomain(bundle);
}
#region Mapping
private static PolicyPackRecord ToDomain(PolicyPackDocument doc)
{
return new PolicyPackRecord(doc.Id, doc.DisplayName, doc.CreatedAt);
}
private static PolicyRevisionRecord ToDomain(PolicyRevisionDoc doc, PolicyBundleDoc? bundleDoc = null)
{
var status = Enum.TryParse<PolicyRevisionStatus>(doc.Status, ignoreCase: true, out var s)
? s
: PolicyRevisionStatus.Draft;
var revision = new PolicyRevisionRecord(doc.Version, doc.RequiresTwoPersonApproval, status, doc.CreatedAt);
if (doc.ActivatedAt.HasValue)
{
revision.SetStatus(PolicyRevisionStatus.Active, doc.ActivatedAt.Value);
}
foreach (var approval in doc.Approvals)
{
revision.AddApproval(new PolicyActivationApproval(approval.ActorId, approval.ApprovedAt, approval.Comment));
}
if (bundleDoc is not null)
{
revision.SetBundle(ToDomain(bundleDoc));
}
return revision;
}
private static PolicyBundleRecord ToDomain(PolicyBundleDoc doc)
{
PolicyAocMetadata? aocMetadata = null;
if (doc.AocMetadata is not null)
{
var aoc = doc.AocMetadata;
PolicyProvenance? provenance = null;
if (aoc.Provenance is not null)
{
var p = aoc.Provenance;
provenance = new PolicyProvenance(
p.SourceType,
p.SourceUrl,
p.Submitter,
p.CommitSha,
p.Branch,
p.IngestedAt);
}
PolicyAttestationRef? attestationRef = null;
if (aoc.AttestationRef is not null)
{
var a = aoc.AttestationRef;
attestationRef = new PolicyAttestationRef(
a.AttestationId,
a.EnvelopeDigest,
a.Uri,
a.SigningKeyId,
a.CreatedAt);
}
aocMetadata = new PolicyAocMetadata(
aoc.CompilationId,
aoc.CompilerVersion,
aoc.CompiledAt,
aoc.SourceDigest,
aoc.ArtifactDigest,
aoc.ComplexityScore,
aoc.RuleCount,
aoc.DurationMilliseconds,
provenance,
attestationRef);
}
return new PolicyBundleRecord(
doc.Id,
doc.Signature,
doc.SizeBytes,
doc.CreatedAt,
doc.Payload.ToImmutableArray(),
CompiledDocument: null, // Cannot serialize IR document to/from Mongo
aocMetadata);
}
private static PolicyAocMetadataDoc ToDocument(PolicyAocMetadata aoc)
{
return new PolicyAocMetadataDoc
{
CompilationId = aoc.CompilationId,
CompilerVersion = aoc.CompilerVersion,
CompiledAt = aoc.CompiledAt,
SourceDigest = aoc.SourceDigest,
ArtifactDigest = aoc.ArtifactDigest,
ComplexityScore = aoc.ComplexityScore,
RuleCount = aoc.RuleCount,
DurationMilliseconds = aoc.DurationMilliseconds,
Provenance = aoc.Provenance is not null ? ToDocument(aoc.Provenance) : null,
AttestationRef = aoc.AttestationRef is not null ? ToDocument(aoc.AttestationRef) : null
};
}
private static PolicyProvenanceDoc ToDocument(PolicyProvenance p)
{
return new PolicyProvenanceDoc
{
SourceType = p.SourceType,
SourceUrl = p.SourceUrl,
Submitter = p.Submitter,
CommitSha = p.CommitSha,
Branch = p.Branch,
IngestedAt = p.IngestedAt
};
}
private static PolicyAttestationRefDoc ToDocument(PolicyAttestationRef a)
{
return new PolicyAttestationRefDoc
{
AttestationId = a.AttestationId,
EnvelopeDigest = a.EnvelopeDigest,
Uri = a.Uri,
SigningKeyId = a.SigningKeyId,
CreatedAt = a.CreatedAt
};
}
#endregion
}

View File

@@ -1,72 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Policy.Engine.Storage.Mongo.Internal;
using StellaOps.Policy.Engine.Storage.Mongo.Migrations;
using StellaOps.Policy.Engine.Storage.Mongo.Options;
using StellaOps.Policy.Engine.Storage.Mongo.Repositories;
namespace StellaOps.Policy.Engine.Storage.Mongo;
/// <summary>
/// Extension methods for registering Policy Engine MongoDB storage services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds Policy Engine MongoDB storage services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">Optional configuration action for PolicyEngineMongoOptions.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddPolicyEngineMongoStorage(
this IServiceCollection services,
Action<PolicyEngineMongoOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
// Register options
if (configure is not null)
{
services.Configure(configure);
}
// Register context (singleton for connection pooling)
services.AddSingleton<PolicyEngineMongoContext>();
// Register migrations
services.AddSingleton<IPolicyEngineMongoMigration, EnsurePolicyCollectionsMigration>();
services.AddSingleton<IPolicyEngineMongoMigration, EnsurePolicyIndexesMigration>();
services.AddSingleton<IPolicyEngineMongoMigration, EnsureExceptionIndexesMigration>();
// Register migration runner
services.AddSingleton<PolicyEngineMigrationRunner>();
// Register initializer
services.AddSingleton<IPolicyEngineMongoInitializer, PolicyEngineMongoInitializer>();
// Register dynamic collection initializer for effective findings
services.AddSingleton<IEffectiveFindingCollectionInitializer, EffectiveFindingCollectionInitializer>();
// Register repositories
services.AddSingleton<IExceptionRepository, MongoExceptionRepository>();
return services;
}
/// <summary>
/// Adds Policy Engine MongoDB storage services with configuration binding from a configuration section.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">Configuration section containing PolicyEngineMongoOptions.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddPolicyEngineMongoStorage(
this IServiceCollection services,
Microsoft.Extensions.Configuration.IConfigurationSection configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
services.Configure<PolicyEngineMongoOptions>(configuration);
return services.AddPolicyEngineMongoStorage(configure: null);
}
}