Add unit tests for PackRunAttestation and SealedInstallEnforcer
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled

- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission.
- Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic.
- Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
StellaOps Bot
2025-12-06 22:25:30 +02:00
parent dd0067ea0b
commit 4042fc2184
110 changed files with 20084 additions and 639 deletions

View File

@@ -0,0 +1,70 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Severity level.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<Severity>))]
public enum Severity
{
[JsonPropertyName("critical")]
Critical,
[JsonPropertyName("high")]
High,
[JsonPropertyName("medium")]
Medium,
[JsonPropertyName("low")]
Low,
[JsonPropertyName("info")]
Info
}
/// <summary>
/// RFC 7807 Problem Details for HTTP APIs.
/// </summary>
public sealed record ProblemDetails
{
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("title")]
public required string Title { get; init; }
[JsonPropertyName("status")]
public required int Status { get; init; }
[JsonPropertyName("detail")]
public string? Detail { get; init; }
[JsonPropertyName("instance")]
public string? Instance { get; init; }
[JsonPropertyName("errors")]
public IReadOnlyList<ValidationError>? Errors { get; init; }
}
/// <summary>
/// Validation error.
/// </summary>
public sealed record ValidationError
{
[JsonPropertyName("field")]
public string? Field { get; init; }
[JsonPropertyName("message")]
public string? Message { get; init; }
}
/// <summary>
/// Common pagination parameters.
/// </summary>
public sealed record PaginationParams
{
public int PageSize { get; init; } = 20;
public string? PageToken { get; init; }
}

View File

@@ -0,0 +1,109 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Policy override.
/// </summary>
public sealed record Override
{
[JsonPropertyName("override_id")]
public required Guid OverrideId { get; init; }
[JsonPropertyName("profile_id")]
public Guid? ProfileId { get; init; }
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("status")]
public required OverrideStatus Status { get; init; }
[JsonPropertyName("reason")]
public string? Reason { get; init; }
[JsonPropertyName("scope")]
public OverrideScope? Scope { get; init; }
[JsonPropertyName("expires_at")]
public DateTimeOffset? ExpiresAt { get; init; }
[JsonPropertyName("approved_by")]
public string? ApprovedBy { get; init; }
[JsonPropertyName("approved_at")]
public DateTimeOffset? ApprovedAt { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; init; }
}
/// <summary>
/// Override status.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<OverrideStatus>))]
public enum OverrideStatus
{
[JsonPropertyName("pending")]
Pending,
[JsonPropertyName("approved")]
Approved,
[JsonPropertyName("disabled")]
Disabled,
[JsonPropertyName("expired")]
Expired
}
/// <summary>
/// Override scope.
/// </summary>
public sealed record OverrideScope
{
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("cve_id")]
public string? CveId { get; init; }
[JsonPropertyName("component")]
public string? Component { get; init; }
[JsonPropertyName("environment")]
public string? Environment { get; init; }
}
/// <summary>
/// Request to create an override.
/// </summary>
public sealed record CreateOverrideRequest
{
[JsonPropertyName("profile_id")]
public Guid? ProfileId { get; init; }
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("reason")]
public required string Reason { get; init; }
[JsonPropertyName("scope")]
public OverrideScope? Scope { get; init; }
[JsonPropertyName("expires_at")]
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Request to approve an override.
/// </summary>
public sealed record ApproveOverrideRequest
{
[JsonPropertyName("comment")]
public string? Comment { get; init; }
}

View File

@@ -0,0 +1,287 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Policy pack workspace entity.
/// </summary>
public sealed record PolicyPack
{
[JsonPropertyName("pack_id")]
public required Guid PackId { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("status")]
public required PolicyPackStatus Status { get; init; }
[JsonPropertyName("rules")]
public IReadOnlyList<PolicyRule>? Rules { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("updated_at")]
public required DateTimeOffset UpdatedAt { get; init; }
[JsonPropertyName("published_at")]
public DateTimeOffset? PublishedAt { get; init; }
[JsonPropertyName("digest")]
public string? Digest { get; init; }
}
/// <summary>
/// Policy pack status.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PolicyPackStatus>))]
public enum PolicyPackStatus
{
[JsonPropertyName("draft")]
Draft,
[JsonPropertyName("pending_review")]
PendingReview,
[JsonPropertyName("published")]
Published,
[JsonPropertyName("archived")]
Archived
}
/// <summary>
/// Individual policy rule within a pack.
/// </summary>
public sealed record PolicyRule
{
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("severity")]
public required Severity Severity { get; init; }
[JsonPropertyName("rego")]
public string? Rego { get; init; }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; } = true;
}
/// <summary>
/// Request to create a policy pack.
/// </summary>
public sealed record CreatePolicyPackRequest
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("rules")]
public IReadOnlyList<PolicyRule>? Rules { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Request to update a policy pack.
/// </summary>
public sealed record UpdatePolicyPackRequest
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("rules")]
public IReadOnlyList<PolicyRule>? Rules { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Paginated list of policy packs.
/// </summary>
public sealed record PolicyPackList
{
[JsonPropertyName("items")]
public required IReadOnlyList<PolicyPack> Items { get; init; }
[JsonPropertyName("next_page_token")]
public string? NextPageToken { get; init; }
}
/// <summary>
/// Compilation result for a policy pack.
/// </summary>
public sealed record CompilationResult
{
[JsonPropertyName("success")]
public required bool Success { get; init; }
[JsonPropertyName("errors")]
public IReadOnlyList<CompilationError>? Errors { get; init; }
[JsonPropertyName("warnings")]
public IReadOnlyList<CompilationWarning>? Warnings { get; init; }
[JsonPropertyName("digest")]
public string? Digest { get; init; }
}
/// <summary>
/// Compilation error.
/// </summary>
public sealed record CompilationError
{
[JsonPropertyName("rule_id")]
public string? RuleId { get; init; }
[JsonPropertyName("line")]
public int? Line { get; init; }
[JsonPropertyName("column")]
public int? Column { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
}
/// <summary>
/// Compilation warning.
/// </summary>
public sealed record CompilationWarning
{
[JsonPropertyName("rule_id")]
public string? RuleId { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
}
/// <summary>
/// Request to simulate a policy pack.
/// </summary>
public sealed record SimulationRequest
{
[JsonPropertyName("input")]
public required IReadOnlyDictionary<string, object> Input { get; init; }
[JsonPropertyName("options")]
public SimulationOptions? Options { get; init; }
}
/// <summary>
/// Simulation options.
/// </summary>
public sealed record SimulationOptions
{
[JsonPropertyName("trace")]
public bool Trace { get; init; }
[JsonPropertyName("explain")]
public bool Explain { get; init; }
}
/// <summary>
/// Simulation result.
/// </summary>
public sealed record SimulationResult
{
[JsonPropertyName("result")]
public required IReadOnlyDictionary<string, object> Result { get; init; }
[JsonPropertyName("violations")]
public IReadOnlyList<SimulatedViolation>? Violations { get; init; }
[JsonPropertyName("trace")]
public IReadOnlyList<string>? Trace { get; init; }
[JsonPropertyName("explain")]
public PolicyExplainTrace? Explain { get; init; }
}
/// <summary>
/// Simulated violation.
/// </summary>
public sealed record SimulatedViolation
{
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("severity")]
public required string Severity { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
[JsonPropertyName("context")]
public IReadOnlyDictionary<string, object>? Context { get; init; }
}
/// <summary>
/// Policy explain trace.
/// </summary>
public sealed record PolicyExplainTrace
{
[JsonPropertyName("steps")]
public IReadOnlyList<object>? Steps { get; init; }
}
/// <summary>
/// Request to publish a policy pack.
/// </summary>
public sealed record PublishRequest
{
[JsonPropertyName("approval_id")]
public string? ApprovalId { get; init; }
}
/// <summary>
/// Request to promote a policy pack.
/// </summary>
public sealed record PromoteRequest
{
[JsonPropertyName("target_environment")]
public TargetEnvironment? TargetEnvironment { get; init; }
[JsonPropertyName("approval_id")]
public string? ApprovalId { get; init; }
}
/// <summary>
/// Target environment for promotion.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<TargetEnvironment>))]
public enum TargetEnvironment
{
[JsonPropertyName("staging")]
Staging,
[JsonPropertyName("production")]
Production
}

View File

@@ -0,0 +1,121 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Sealed mode status (air-gap operation).
/// </summary>
public sealed record SealedModeStatus
{
[JsonPropertyName("sealed")]
public required bool Sealed { get; init; }
[JsonPropertyName("mode")]
public required SealedMode Mode { get; init; }
[JsonPropertyName("sealed_at")]
public DateTimeOffset? SealedAt { get; init; }
[JsonPropertyName("sealed_by")]
public string? SealedBy { get; init; }
[JsonPropertyName("bundle_version")]
public string? BundleVersion { get; init; }
[JsonPropertyName("last_advisory_update")]
public DateTimeOffset? LastAdvisoryUpdate { get; init; }
[JsonPropertyName("time_anchor")]
public TimeAnchor? TimeAnchor { get; init; }
}
/// <summary>
/// Sealed mode state.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<SealedMode>))]
public enum SealedMode
{
[JsonPropertyName("online")]
Online,
[JsonPropertyName("sealed")]
Sealed,
[JsonPropertyName("transitioning")]
Transitioning
}
/// <summary>
/// Time anchor for sealed mode operations.
/// </summary>
public sealed record TimeAnchor
{
[JsonPropertyName("timestamp")]
public required DateTimeOffset Timestamp { get; init; }
[JsonPropertyName("signature")]
public string? Signature { get; init; }
[JsonPropertyName("valid")]
public required bool Valid { get; init; }
[JsonPropertyName("expires_at")]
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Request to seal the environment.
/// </summary>
public sealed record SealRequest
{
[JsonPropertyName("reason")]
public string? Reason { get; init; }
[JsonPropertyName("time_anchor")]
public DateTimeOffset? TimeAnchor { get; init; }
}
/// <summary>
/// Request to unseal the environment.
/// </summary>
public sealed record UnsealRequest
{
[JsonPropertyName("reason")]
public required string Reason { get; init; }
[JsonPropertyName("audit_note")]
public string? AuditNote { get; init; }
}
/// <summary>
/// Request to verify an air-gap bundle.
/// </summary>
public sealed record VerifyBundleRequest
{
[JsonPropertyName("bundle_digest")]
public required string BundleDigest { get; init; }
[JsonPropertyName("public_key")]
public string? PublicKey { get; init; }
}
/// <summary>
/// Result of bundle verification.
/// </summary>
public sealed record BundleVerificationResult
{
[JsonPropertyName("valid")]
public required bool Valid { get; init; }
[JsonPropertyName("bundle_digest")]
public string? BundleDigest { get; init; }
[JsonPropertyName("signed_at")]
public DateTimeOffset? SignedAt { get; init; }
[JsonPropertyName("signer_fingerprint")]
public string? SignerFingerprint { get; init; }
[JsonPropertyName("errors")]
public IReadOnlyList<string>? Errors { get; init; }
}

View File

@@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Policy snapshot.
/// </summary>
public sealed record Snapshot
{
[JsonPropertyName("snapshot_id")]
public required Guid SnapshotId { get; init; }
[JsonPropertyName("digest")]
public required string Digest { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("pack_ids")]
public IReadOnlyList<Guid>? PackIds { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; init; }
}
/// <summary>
/// Request to create a snapshot.
/// </summary>
public sealed record CreateSnapshotRequest
{
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("pack_ids")]
public required IReadOnlyList<Guid> PackIds { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Paginated list of snapshots.
/// </summary>
public sealed record SnapshotList
{
[JsonPropertyName("items")]
public required IReadOnlyList<Snapshot> Items { get; init; }
[JsonPropertyName("next_page_token")]
public string? NextPageToken { get; init; }
}

View File

@@ -0,0 +1,94 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Overall staleness status.
/// </summary>
public sealed record StalenessStatus
{
[JsonPropertyName("overall_status")]
public required StalenessLevel OverallStatus { get; init; }
[JsonPropertyName("sources")]
public required IReadOnlyList<SourceStaleness> Sources { get; init; }
[JsonPropertyName("last_check")]
public DateTimeOffset? LastCheck { get; init; }
}
/// <summary>
/// Staleness level.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<StalenessLevel>))]
public enum StalenessLevel
{
[JsonPropertyName("fresh")]
Fresh,
[JsonPropertyName("stale")]
Stale,
[JsonPropertyName("critical")]
Critical,
[JsonPropertyName("unknown")]
Unknown
}
/// <summary>
/// Staleness status for an individual source.
/// </summary>
public sealed record SourceStaleness
{
[JsonPropertyName("source_id")]
public required string SourceId { get; init; }
[JsonPropertyName("source_name")]
public string? SourceName { get; init; }
[JsonPropertyName("status")]
public required StalenessLevel Status { get; init; }
[JsonPropertyName("last_update")]
public required DateTimeOffset LastUpdate { get; init; }
[JsonPropertyName("max_age_hours")]
public int? MaxAgeHours { get; init; }
[JsonPropertyName("age_hours")]
public double? AgeHours { get; init; }
}
/// <summary>
/// Request to evaluate staleness.
/// </summary>
public sealed record EvaluateStalenessRequest
{
[JsonPropertyName("source_id")]
public required string SourceId { get; init; }
[JsonPropertyName("threshold_hours")]
public int? ThresholdHours { get; init; }
}
/// <summary>
/// Result of staleness evaluation.
/// </summary>
public sealed record StalenessEvaluation
{
[JsonPropertyName("source_id")]
public required string SourceId { get; init; }
[JsonPropertyName("is_stale")]
public required bool IsStale { get; init; }
[JsonPropertyName("age_hours")]
public double? AgeHours { get; init; }
[JsonPropertyName("threshold_hours")]
public int? ThresholdHours { get; init; }
[JsonPropertyName("recommendation")]
public string? Recommendation { get; init; }
}

View File

@@ -0,0 +1,145 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Verification policy for attestation validation.
/// Based on OpenAPI: docs/schemas/policy-registry-api.openapi.yaml
/// </summary>
public sealed record VerificationPolicy
{
[JsonPropertyName("policy_id")]
public required string PolicyId { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("tenant_scope")]
public required string TenantScope { get; init; }
[JsonPropertyName("predicate_types")]
public required IReadOnlyList<string> PredicateTypes { get; init; }
[JsonPropertyName("signer_requirements")]
public required SignerRequirements SignerRequirements { get; init; }
[JsonPropertyName("validity_window")]
public ValidityWindow? ValidityWindow { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("updated_at")]
public required DateTimeOffset UpdatedAt { get; init; }
}
/// <summary>
/// Requirements for attestation signers.
/// </summary>
public sealed record SignerRequirements
{
[JsonPropertyName("minimum_signatures")]
public int MinimumSignatures { get; init; } = 1;
[JsonPropertyName("trusted_key_fingerprints")]
public required IReadOnlyList<string> TrustedKeyFingerprints { get; init; }
[JsonPropertyName("trusted_issuers")]
public IReadOnlyList<string>? TrustedIssuers { get; init; }
[JsonPropertyName("require_rekor")]
public bool RequireRekor { get; init; }
[JsonPropertyName("algorithms")]
public IReadOnlyList<string>? Algorithms { get; init; }
}
/// <summary>
/// Validity window for attestations.
/// </summary>
public sealed record ValidityWindow
{
[JsonPropertyName("not_before")]
public DateTimeOffset? NotBefore { get; init; }
[JsonPropertyName("not_after")]
public DateTimeOffset? NotAfter { get; init; }
[JsonPropertyName("max_attestation_age")]
public int? MaxAttestationAge { get; init; }
}
/// <summary>
/// Request to create a verification policy.
/// </summary>
public sealed record CreateVerificationPolicyRequest
{
[JsonPropertyName("policy_id")]
public required string PolicyId { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("tenant_scope")]
public string? TenantScope { get; init; }
[JsonPropertyName("predicate_types")]
public required IReadOnlyList<string> PredicateTypes { get; init; }
[JsonPropertyName("signer_requirements")]
public SignerRequirements? SignerRequirements { get; init; }
[JsonPropertyName("validity_window")]
public ValidityWindow? ValidityWindow { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Request to update a verification policy.
/// </summary>
public sealed record UpdateVerificationPolicyRequest
{
[JsonPropertyName("version")]
public string? Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("predicate_types")]
public IReadOnlyList<string>? PredicateTypes { get; init; }
[JsonPropertyName("signer_requirements")]
public SignerRequirements? SignerRequirements { get; init; }
[JsonPropertyName("validity_window")]
public ValidityWindow? ValidityWindow { get; init; }
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Paginated list of verification policies.
/// </summary>
public sealed record VerificationPolicyList
{
[JsonPropertyName("items")]
public required IReadOnlyList<VerificationPolicy> Items { get; init; }
[JsonPropertyName("next_page_token")]
public string? NextPageToken { get; init; }
[JsonPropertyName("total_count")]
public int? TotalCount { get; init; }
}

View File

@@ -0,0 +1,114 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Registry.Contracts;
/// <summary>
/// Policy violation.
/// </summary>
public sealed record Violation
{
[JsonPropertyName("violation_id")]
public required Guid ViolationId { get; init; }
[JsonPropertyName("policy_id")]
public string? PolicyId { get; init; }
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("severity")]
public required Severity Severity { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("cve_id")]
public string? CveId { get; init; }
[JsonPropertyName("context")]
public IReadOnlyDictionary<string, object>? Context { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
}
/// <summary>
/// Request to create a violation.
/// </summary>
public sealed record CreateViolationRequest
{
[JsonPropertyName("policy_id")]
public string? PolicyId { get; init; }
[JsonPropertyName("rule_id")]
public required string RuleId { get; init; }
[JsonPropertyName("severity")]
public required Severity Severity { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("cve_id")]
public string? CveId { get; init; }
[JsonPropertyName("context")]
public IReadOnlyDictionary<string, object>? Context { get; init; }
}
/// <summary>
/// Batch request to create violations.
/// </summary>
public sealed record ViolationBatchRequest
{
[JsonPropertyName("violations")]
public required IReadOnlyList<CreateViolationRequest> Violations { get; init; }
}
/// <summary>
/// Result of batch violation creation.
/// </summary>
public sealed record ViolationBatchResult
{
[JsonPropertyName("created")]
public required int Created { get; init; }
[JsonPropertyName("failed")]
public required int Failed { get; init; }
[JsonPropertyName("errors")]
public IReadOnlyList<BatchError>? Errors { get; init; }
}
/// <summary>
/// Error from batch operation.
/// </summary>
public sealed record BatchError
{
[JsonPropertyName("index")]
public int? Index { get; init; }
[JsonPropertyName("error")]
public string? Error { get; init; }
}
/// <summary>
/// Paginated list of violations.
/// </summary>
public sealed record ViolationList
{
[JsonPropertyName("items")]
public required IReadOnlyList<Violation> Items { get; init; }
[JsonPropertyName("next_page_token")]
public string? NextPageToken { get; init; }
[JsonPropertyName("total_count")]
public int? TotalCount { get; init; }
}

View File

@@ -0,0 +1,214 @@
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry;
/// <summary>
/// Typed HTTP client for Policy Registry API.
/// Based on OpenAPI: docs/schemas/policy-registry-api.openapi.yaml
/// </summary>
public interface IPolicyRegistryClient
{
// ============================================================
// VERIFICATION POLICY OPERATIONS
// ============================================================
Task<VerificationPolicyList> ListVerificationPoliciesAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default);
Task<VerificationPolicy> CreateVerificationPolicyAsync(
Guid tenantId,
CreateVerificationPolicyRequest request,
CancellationToken cancellationToken = default);
Task<VerificationPolicy> GetVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default);
Task<VerificationPolicy> UpdateVerificationPolicyAsync(
Guid tenantId,
string policyId,
UpdateVerificationPolicyRequest request,
CancellationToken cancellationToken = default);
Task DeleteVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default);
// ============================================================
// POLICY PACK OPERATIONS
// ============================================================
Task<PolicyPackList> ListPolicyPacksAsync(
Guid tenantId,
PolicyPackStatus? status = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default);
Task<PolicyPack> CreatePolicyPackAsync(
Guid tenantId,
CreatePolicyPackRequest request,
CancellationToken cancellationToken = default);
Task<PolicyPack> GetPolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default);
Task<PolicyPack> UpdatePolicyPackAsync(
Guid tenantId,
Guid packId,
UpdatePolicyPackRequest request,
CancellationToken cancellationToken = default);
Task DeletePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default);
Task<CompilationResult> CompilePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default);
Task<SimulationResult> SimulatePolicyPackAsync(
Guid tenantId,
Guid packId,
SimulationRequest request,
CancellationToken cancellationToken = default);
Task<PolicyPack> PublishPolicyPackAsync(
Guid tenantId,
Guid packId,
PublishRequest? request = null,
CancellationToken cancellationToken = default);
Task<PolicyPack> PromotePolicyPackAsync(
Guid tenantId,
Guid packId,
PromoteRequest? request = null,
CancellationToken cancellationToken = default);
// ============================================================
// SNAPSHOT OPERATIONS
// ============================================================
Task<SnapshotList> ListSnapshotsAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default);
Task<Snapshot> CreateSnapshotAsync(
Guid tenantId,
CreateSnapshotRequest request,
CancellationToken cancellationToken = default);
Task<Snapshot> GetSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default);
Task DeleteSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default);
Task<Snapshot> GetSnapshotByDigestAsync(
Guid tenantId,
string digest,
CancellationToken cancellationToken = default);
// ============================================================
// VIOLATION OPERATIONS
// ============================================================
Task<ViolationList> ListViolationsAsync(
Guid tenantId,
Severity? severity = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default);
Task<Violation> AppendViolationAsync(
Guid tenantId,
CreateViolationRequest request,
CancellationToken cancellationToken = default);
Task<ViolationBatchResult> AppendViolationBatchAsync(
Guid tenantId,
ViolationBatchRequest request,
CancellationToken cancellationToken = default);
Task<Violation> GetViolationAsync(
Guid tenantId,
Guid violationId,
CancellationToken cancellationToken = default);
// ============================================================
// OVERRIDE OPERATIONS
// ============================================================
Task<Override> CreateOverrideAsync(
Guid tenantId,
CreateOverrideRequest request,
CancellationToken cancellationToken = default);
Task<Override> GetOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
Task DeleteOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
Task<Override> ApproveOverrideAsync(
Guid tenantId,
Guid overrideId,
ApproveOverrideRequest? request = null,
CancellationToken cancellationToken = default);
Task<Override> DisableOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
// ============================================================
// SEALED MODE OPERATIONS
// ============================================================
Task<SealedModeStatus> GetSealedModeStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
Task<SealedModeStatus> SealAsync(
Guid tenantId,
SealRequest? request = null,
CancellationToken cancellationToken = default);
Task<SealedModeStatus> UnsealAsync(
Guid tenantId,
UnsealRequest request,
CancellationToken cancellationToken = default);
Task<BundleVerificationResult> VerifyBundleAsync(
Guid tenantId,
VerifyBundleRequest request,
CancellationToken cancellationToken = default);
// ============================================================
// STALENESS OPERATIONS
// ============================================================
Task<StalenessStatus> GetStalenessStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
Task<StalenessEvaluation> EvaluateStalenessAsync(
Guid tenantId,
EvaluateStalenessRequest request,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,634 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry;
/// <summary>
/// HTTP client implementation for Policy Registry API.
/// </summary>
public sealed class PolicyRegistryClient : IPolicyRegistryClient
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
public PolicyRegistryClient(HttpClient httpClient, IOptions<PolicyRegistryClientOptions>? options = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
if (options?.Value?.BaseUrl is not null && _httpClient.BaseAddress is null)
{
_httpClient.BaseAddress = new Uri(options.Value.BaseUrl);
}
}
private static void AddTenantHeader(HttpRequestMessage request, Guid tenantId)
{
request.Headers.Add("X-Tenant-Id", tenantId.ToString());
}
private static string BuildQueryString(PaginationParams? pagination, params (string name, string? value)[] additional)
{
var parts = new List<string>();
if (pagination is not null)
{
if (pagination.PageSize != 20)
{
parts.Add($"page_size={pagination.PageSize}");
}
if (!string.IsNullOrWhiteSpace(pagination.PageToken))
{
parts.Add($"page_token={Uri.EscapeDataString(pagination.PageToken)}");
}
}
foreach (var (name, value) in additional)
{
if (!string.IsNullOrWhiteSpace(value))
{
parts.Add($"{name}={Uri.EscapeDataString(value)}");
}
}
return parts.Count > 0 ? "?" + string.Join("&", parts) : string.Empty;
}
// ============================================================
// VERIFICATION POLICY OPERATIONS
// ============================================================
public async Task<VerificationPolicyList> ListVerificationPoliciesAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination);
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/verification-policies{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicyList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> CreateVerificationPolicyAsync(
Guid tenantId,
CreateVerificationPolicyRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/verification-policies");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> GetVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> UpdateVerificationPolicyAsync(
Guid tenantId,
string policyId,
UpdateVerificationPolicyRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
// ============================================================
// POLICY PACK OPERATIONS
// ============================================================
public async Task<PolicyPackList> ListPolicyPacksAsync(
Guid tenantId,
PolicyPackStatus? status = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination, ("status", status?.ToString().ToLowerInvariant()));
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/packs{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPackList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> CreatePolicyPackAsync(
Guid tenantId,
CreatePolicyPackRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/packs");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> GetPolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> UpdatePolicyPackAsync(
Guid tenantId,
Guid packId,
UpdatePolicyPackRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeletePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<CompilationResult> CompilePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/compile");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Note: 422 also returns CompilationResult, so we read regardless of status
return await response.Content.ReadFromJsonAsync<CompilationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SimulationResult> SimulatePolicyPackAsync(
Guid tenantId,
Guid packId,
SimulationRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/simulate");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SimulationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> PublishPolicyPackAsync(
Guid tenantId,
Guid packId,
PublishRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/publish");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> PromotePolicyPackAsync(
Guid tenantId,
Guid packId,
PromoteRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/promote");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// SNAPSHOT OPERATIONS
// ============================================================
public async Task<SnapshotList> ListSnapshotsAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination);
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SnapshotList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Snapshot> CreateSnapshotAsync(
Guid tenantId,
CreateSnapshotRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/snapshots");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Snapshot> GetSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots/{snapshotId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/snapshots/{snapshotId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<Snapshot> GetSnapshotByDigestAsync(
Guid tenantId,
string digest,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots/by-digest/{Uri.EscapeDataString(digest)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// VIOLATION OPERATIONS
// ============================================================
public async Task<ViolationList> ListViolationsAsync(
Guid tenantId,
Severity? severity = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination, ("severity", severity?.ToString().ToLowerInvariant()));
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/violations{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ViolationList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Violation> AppendViolationAsync(
Guid tenantId,
CreateViolationRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/violations");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Violation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<ViolationBatchResult> AppendViolationBatchAsync(
Guid tenantId,
ViolationBatchRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/violations/batch");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ViolationBatchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Violation> GetViolationAsync(
Guid tenantId,
Guid violationId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/violations/{violationId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Violation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// OVERRIDE OPERATIONS
// ============================================================
public async Task<Override> CreateOverrideAsync(
Guid tenantId,
CreateOverrideRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/overrides");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Override> GetOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/overrides/{overrideId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/overrides/{overrideId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<Override> ApproveOverrideAsync(
Guid tenantId,
Guid overrideId,
ApproveOverrideRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/overrides/{overrideId}:approve");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Override> DisableOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/overrides/{overrideId}:disable");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// SEALED MODE OPERATIONS
// ============================================================
public async Task<SealedModeStatus> GetSealedModeStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/policy/sealed-mode/status");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SealedModeStatus> SealAsync(
Guid tenantId,
SealRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/seal");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SealedModeStatus> UnsealAsync(
Guid tenantId,
UnsealRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/unseal");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<BundleVerificationResult> VerifyBundleAsync(
Guid tenantId,
VerifyBundleRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/verify");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<BundleVerificationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// STALENESS OPERATIONS
// ============================================================
public async Task<StalenessStatus> GetStalenessStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/policy/staleness/status");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StalenessStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<StalenessEvaluation> EvaluateStalenessAsync(
Guid tenantId,
EvaluateStalenessRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/staleness/evaluate");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StalenessEvaluation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
}
/// <summary>
/// Configuration options for Policy Registry client.
/// </summary>
public sealed class PolicyRegistryClientOptions
{
public string? BaseUrl { get; set; }
}

View File

@@ -0,0 +1,46 @@
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.Policy.Registry;
/// <summary>
/// Extension methods for registering Policy Registry services.
/// </summary>
public static class PolicyRegistryServiceCollectionExtensions
{
/// <summary>
/// Adds the Policy Registry typed HTTP client to the service collection.
/// </summary>
public static IServiceCollection AddPolicyRegistryClient(
this IServiceCollection services,
Action<PolicyRegistryClientOptions>? configureOptions = null)
{
if (configureOptions is not null)
{
services.Configure(configureOptions);
}
services.AddHttpClient<IPolicyRegistryClient, PolicyRegistryClient>();
return services;
}
/// <summary>
/// Adds the Policy Registry typed HTTP client with a custom base address.
/// </summary>
public static IServiceCollection AddPolicyRegistryClient(
this IServiceCollection services,
string baseUrl)
{
services.Configure<PolicyRegistryClientOptions>(options =>
{
options.BaseUrl = baseUrl;
});
services.AddHttpClient<IPolicyRegistryClient, PolicyRegistryClient>(client =>
{
client.BaseAddress = new Uri(baseUrl);
});
return services;
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.Policy.Registry</RootNamespace>
<AssemblyName>StellaOps.Policy.Registry</AssemblyName>
<Description>Policy Registry typed clients and contracts for StellaOps Policy Engine</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup>
</Project>