Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Provider for retrieving air-gap sealed mode status.
|
||||
/// </summary>
|
||||
public interface IAirGapStatusProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current sealed mode status of the environment.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Optional tenant ID for multi-tenant environments.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The sealed mode status.</returns>
|
||||
Task<SealedModeStatus> GetStatusAsync(string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using StellaOps.TaskRunner.Core.Events;
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Audit logger for sealed install enforcement decisions.
|
||||
/// </summary>
|
||||
public interface ISealedInstallAuditLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs an enforcement decision.
|
||||
/// </summary>
|
||||
Task LogEnforcementAsync(
|
||||
TaskPackManifest manifest,
|
||||
SealedInstallEnforcementResult result,
|
||||
string? tenantId = null,
|
||||
string? runId = null,
|
||||
string? actor = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of sealed install audit logger using timeline events.
|
||||
/// </summary>
|
||||
public sealed class SealedInstallAuditLogger : ISealedInstallAuditLogger
|
||||
{
|
||||
private readonly IPackRunTimelineEventEmitter _eventEmitter;
|
||||
|
||||
public SealedInstallAuditLogger(IPackRunTimelineEventEmitter eventEmitter)
|
||||
{
|
||||
_eventEmitter = eventEmitter ?? throw new ArgumentNullException(nameof(eventEmitter));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LogEnforcementAsync(
|
||||
TaskPackManifest manifest,
|
||||
SealedInstallEnforcementResult result,
|
||||
string? tenantId = null,
|
||||
string? runId = null,
|
||||
string? actor = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
var effectiveTenantId = tenantId ?? "default";
|
||||
var effectiveRunId = runId ?? Guid.NewGuid().ToString("n");
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var eventType = result.Allowed
|
||||
? PackRunEventTypes.SealedInstallAllowed
|
||||
: PackRunEventTypes.SealedInstallDenied;
|
||||
|
||||
var severity = result.Allowed
|
||||
? PackRunEventSeverity.Info
|
||||
: PackRunEventSeverity.Warning;
|
||||
|
||||
var attributes = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["pack_name"] = manifest.Metadata.Name,
|
||||
["pack_version"] = manifest.Metadata.Version,
|
||||
["decision"] = result.Allowed ? "allowed" : "denied",
|
||||
["sealed_install_required"] = manifest.Spec.SealedInstall.ToString().ToLowerInvariant()
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.ErrorCode))
|
||||
{
|
||||
attributes["error_code"] = result.ErrorCode;
|
||||
}
|
||||
|
||||
object payload;
|
||||
if (result.Allowed)
|
||||
{
|
||||
payload = new
|
||||
{
|
||||
event_type = "sealed_install_enforcement",
|
||||
pack_id = manifest.Metadata.Name,
|
||||
pack_version = manifest.Metadata.Version,
|
||||
decision = "allowed",
|
||||
reason = result.Message
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = new
|
||||
{
|
||||
event_type = "sealed_install_enforcement",
|
||||
pack_id = manifest.Metadata.Name,
|
||||
pack_version = manifest.Metadata.Version,
|
||||
decision = "denied",
|
||||
reason = result.ErrorCode,
|
||||
message = result.Message,
|
||||
violation = result.Violation is not null
|
||||
? new
|
||||
{
|
||||
required_sealed = result.Violation.RequiredSealed,
|
||||
actual_sealed = result.Violation.ActualSealed,
|
||||
recommendation = result.Violation.Recommendation
|
||||
}
|
||||
: null,
|
||||
requirement_violations = result.RequirementViolations?.Select(v => new
|
||||
{
|
||||
requirement = v.Requirement,
|
||||
expected = v.Expected,
|
||||
actual = v.Actual,
|
||||
message = v.Message
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
var timelineEvent = PackRunTimelineEvent.Create(
|
||||
tenantId: effectiveTenantId,
|
||||
eventType: eventType,
|
||||
source: "StellaOps.TaskRunner.SealedInstallEnforcer",
|
||||
occurredAt: now,
|
||||
runId: effectiveRunId,
|
||||
actor: actor,
|
||||
severity: severity,
|
||||
attributes: attributes,
|
||||
payload: payload);
|
||||
|
||||
await _eventEmitter.EmitAsync(timelineEvent, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces sealed install requirements for task packs.
|
||||
/// Per sealed-install-enforcement.md contract.
|
||||
/// </summary>
|
||||
public interface ISealedInstallEnforcer
|
||||
{
|
||||
/// <summary>
|
||||
/// Enforces sealed install requirements for a task pack.
|
||||
/// </summary>
|
||||
/// <param name="manifest">The task pack manifest.</param>
|
||||
/// <param name="tenantId">Optional tenant ID.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Enforcement result indicating whether execution is allowed.</returns>
|
||||
Task<SealedInstallEnforcementResult> EnforceAsync(
|
||||
TaskPackManifest manifest,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Result of sealed install enforcement check.
|
||||
/// Per sealed-install-enforcement.md contract.
|
||||
/// </summary>
|
||||
public sealed record SealedInstallEnforcementResult(
|
||||
/// <summary>Whether execution is allowed.</summary>
|
||||
bool Allowed,
|
||||
|
||||
/// <summary>Error code if denied.</summary>
|
||||
string? ErrorCode,
|
||||
|
||||
/// <summary>Human-readable message.</summary>
|
||||
string Message,
|
||||
|
||||
/// <summary>Detailed violation information.</summary>
|
||||
SealedInstallViolation? Violation,
|
||||
|
||||
/// <summary>Requirement violations if any.</summary>
|
||||
IReadOnlyList<RequirementViolation>? RequirementViolations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an allowed result.
|
||||
/// </summary>
|
||||
public static SealedInstallEnforcementResult CreateAllowed(string message)
|
||||
=> new(true, null, message, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a denied result.
|
||||
/// </summary>
|
||||
public static SealedInstallEnforcementResult CreateDenied(
|
||||
string errorCode,
|
||||
string message,
|
||||
SealedInstallViolation? violation = null,
|
||||
IReadOnlyList<RequirementViolation>? requirementViolations = null)
|
||||
=> new(false, errorCode, message, violation, requirementViolations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details about a sealed install violation.
|
||||
/// </summary>
|
||||
public sealed record SealedInstallViolation(
|
||||
/// <summary>Pack ID that requires sealed install.</summary>
|
||||
string PackId,
|
||||
|
||||
/// <summary>Pack version.</summary>
|
||||
string? PackVersion,
|
||||
|
||||
/// <summary>Whether pack requires sealed install.</summary>
|
||||
bool RequiredSealed,
|
||||
|
||||
/// <summary>Actual sealed status of environment.</summary>
|
||||
bool ActualSealed,
|
||||
|
||||
/// <summary>Recommendation for resolving the violation.</summary>
|
||||
string Recommendation);
|
||||
|
||||
/// <summary>
|
||||
/// Details about a requirement violation.
|
||||
/// </summary>
|
||||
public sealed record RequirementViolation(
|
||||
/// <summary>Name of the requirement that was violated.</summary>
|
||||
string Requirement,
|
||||
|
||||
/// <summary>Expected value.</summary>
|
||||
string Expected,
|
||||
|
||||
/// <summary>Actual value.</summary>
|
||||
string Actual,
|
||||
|
||||
/// <summary>Human-readable message describing the violation.</summary>
|
||||
string Message);
|
||||
|
||||
/// <summary>
|
||||
/// Error codes for sealed install enforcement.
|
||||
/// </summary>
|
||||
public static class SealedInstallErrorCodes
|
||||
{
|
||||
/// <summary>Pack requires sealed but environment is not sealed.</summary>
|
||||
public const string SealedInstallViolation = "SEALED_INSTALL_VIOLATION";
|
||||
|
||||
/// <summary>Sealed requirements not met.</summary>
|
||||
public const string SealedRequirementsViolation = "SEALED_REQUIREMENTS_VIOLATION";
|
||||
|
||||
/// <summary>Bundle version below minimum required.</summary>
|
||||
public const string BundleVersionViolation = "BUNDLE_VERSION_VIOLATION";
|
||||
|
||||
/// <summary>Advisory data too stale.</summary>
|
||||
public const string AdvisoryStalenessViolation = "ADVISORY_STALENESS_VIOLATION";
|
||||
|
||||
/// <summary>Time anchor missing or invalid.</summary>
|
||||
public const string TimeAnchorViolation = "TIME_ANCHOR_VIOLATION";
|
||||
|
||||
/// <summary>Bundle signature verification failed.</summary>
|
||||
public const string SignatureVerificationViolation = "SIGNATURE_VERIFICATION_VIOLATION";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CLI exit codes for sealed install enforcement.
|
||||
/// </summary>
|
||||
public static class SealedInstallExitCodes
|
||||
{
|
||||
/// <summary>Pack requires sealed but environment is not.</summary>
|
||||
public const int SealedInstallViolation = 40;
|
||||
|
||||
/// <summary>Bundle version below minimum.</summary>
|
||||
public const int BundleVersionViolation = 41;
|
||||
|
||||
/// <summary>Advisory data too stale.</summary>
|
||||
public const int AdvisoryStalenessViolation = 42;
|
||||
|
||||
/// <summary>Time anchor missing or invalid.</summary>
|
||||
public const int TimeAnchorViolation = 43;
|
||||
|
||||
/// <summary>Bundle signature verification failed.</summary>
|
||||
public const int SignatureVerificationViolation = 44;
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces sealed install requirements for task packs.
|
||||
/// Per sealed-install-enforcement.md contract.
|
||||
/// </summary>
|
||||
public sealed class SealedInstallEnforcer : ISealedInstallEnforcer
|
||||
{
|
||||
private readonly IAirGapStatusProvider _statusProvider;
|
||||
private readonly IOptions<SealedInstallEnforcementOptions> _options;
|
||||
private readonly ILogger<SealedInstallEnforcer> _logger;
|
||||
|
||||
public SealedInstallEnforcer(
|
||||
IAirGapStatusProvider statusProvider,
|
||||
IOptions<SealedInstallEnforcementOptions> options,
|
||||
ILogger<SealedInstallEnforcer> logger)
|
||||
{
|
||||
_statusProvider = statusProvider ?? throw new ArgumentNullException(nameof(statusProvider));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SealedInstallEnforcementResult> EnforceAsync(
|
||||
TaskPackManifest manifest,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
|
||||
var options = _options.Value;
|
||||
|
||||
// Check if enforcement is enabled
|
||||
if (!options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Sealed install enforcement is disabled.");
|
||||
return SealedInstallEnforcementResult.CreateAllowed("Enforcement disabled");
|
||||
}
|
||||
|
||||
// Check for development bypass
|
||||
if (options.BypassForDevelopment && IsDevelopmentEnvironment())
|
||||
{
|
||||
_logger.LogWarning("Sealed install enforcement bypassed for development environment.");
|
||||
return SealedInstallEnforcementResult.CreateAllowed("Development bypass active");
|
||||
}
|
||||
|
||||
// If pack doesn't require sealed install, allow
|
||||
if (!manifest.Spec.SealedInstall)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Pack {PackName} v{PackVersion} does not require sealed install.",
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version);
|
||||
|
||||
return SealedInstallEnforcementResult.CreateAllowed("Pack does not require sealed install");
|
||||
}
|
||||
|
||||
// Get environment sealed status
|
||||
SealedModeStatus status;
|
||||
try
|
||||
{
|
||||
status = await _statusProvider.GetStatusAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get air-gap status. Denying sealed install pack.");
|
||||
|
||||
return SealedInstallEnforcementResult.CreateDenied(
|
||||
SealedInstallErrorCodes.SealedInstallViolation,
|
||||
"Failed to verify sealed mode status",
|
||||
new SealedInstallViolation(
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version,
|
||||
RequiredSealed: true,
|
||||
ActualSealed: false,
|
||||
Recommendation: "Ensure the AirGap controller is accessible: stella airgap status"));
|
||||
}
|
||||
|
||||
// Core check: environment must be sealed
|
||||
if (!status.Sealed)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sealed install violation: Pack {PackName} v{PackVersion} requires sealed environment but environment is {Mode}.",
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version,
|
||||
status.Mode);
|
||||
|
||||
return SealedInstallEnforcementResult.CreateDenied(
|
||||
SealedInstallErrorCodes.SealedInstallViolation,
|
||||
"Pack requires sealed environment but environment is not sealed",
|
||||
new SealedInstallViolation(
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version,
|
||||
RequiredSealed: true,
|
||||
ActualSealed: false,
|
||||
Recommendation: "Activate sealed mode with: stella airgap seal"));
|
||||
}
|
||||
|
||||
// Check sealed requirements if specified
|
||||
var requirements = manifest.Spec.SealedRequirements ?? SealedRequirements.Default;
|
||||
var violations = ValidateRequirements(requirements, status, options);
|
||||
|
||||
if (violations.Count > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Sealed requirements violation for pack {PackName} v{PackVersion}: {ViolationCount} requirement(s) not met.",
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version,
|
||||
violations.Count);
|
||||
|
||||
return SealedInstallEnforcementResult.CreateDenied(
|
||||
SealedInstallErrorCodes.SealedRequirementsViolation,
|
||||
"Sealed requirements not met",
|
||||
violation: null,
|
||||
requirementViolations: violations);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sealed install requirements satisfied for pack {PackName} v{PackVersion}.",
|
||||
manifest.Metadata.Name,
|
||||
manifest.Metadata.Version);
|
||||
|
||||
return SealedInstallEnforcementResult.CreateAllowed("Sealed install requirements satisfied");
|
||||
}
|
||||
|
||||
private List<RequirementViolation> ValidateRequirements(
|
||||
SealedRequirements requirements,
|
||||
SealedModeStatus status,
|
||||
SealedInstallEnforcementOptions options)
|
||||
{
|
||||
var violations = new List<RequirementViolation>();
|
||||
|
||||
// Bundle version check
|
||||
if (!string.IsNullOrWhiteSpace(requirements.MinBundleVersion) &&
|
||||
!string.IsNullOrWhiteSpace(status.BundleVersion))
|
||||
{
|
||||
if (!IsVersionSatisfied(status.BundleVersion, requirements.MinBundleVersion))
|
||||
{
|
||||
violations.Add(new RequirementViolation(
|
||||
Requirement: "min_bundle_version",
|
||||
Expected: requirements.MinBundleVersion,
|
||||
Actual: status.BundleVersion,
|
||||
Message: $"Bundle version {status.BundleVersion} < required {requirements.MinBundleVersion}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Advisory staleness check
|
||||
var effectiveStaleness = status.AdvisoryStalenessHours;
|
||||
var maxStaleness = requirements.MaxAdvisoryStalenessHours;
|
||||
|
||||
// Apply grace period if configured
|
||||
if (options.StalenessGracePeriodHours > 0)
|
||||
{
|
||||
maxStaleness += options.StalenessGracePeriodHours;
|
||||
}
|
||||
|
||||
if (effectiveStaleness > maxStaleness)
|
||||
{
|
||||
if (options.DenyOnStaleness)
|
||||
{
|
||||
violations.Add(new RequirementViolation(
|
||||
Requirement: "max_advisory_staleness_hours",
|
||||
Expected: requirements.MaxAdvisoryStalenessHours.ToString(),
|
||||
Actual: effectiveStaleness.ToString(),
|
||||
Message: $"Advisory data is {effectiveStaleness}h old, max allowed is {requirements.MaxAdvisoryStalenessHours}h"));
|
||||
}
|
||||
else if (effectiveStaleness > options.StalenessWarningThresholdHours)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Advisory data is {Staleness}h old, approaching max allowed {MaxStaleness}h.",
|
||||
effectiveStaleness,
|
||||
requirements.MaxAdvisoryStalenessHours);
|
||||
}
|
||||
}
|
||||
|
||||
// Time anchor check
|
||||
if (requirements.RequireTimeAnchor)
|
||||
{
|
||||
if (status.TimeAnchor is null)
|
||||
{
|
||||
violations.Add(new RequirementViolation(
|
||||
Requirement: "require_time_anchor",
|
||||
Expected: "valid time anchor",
|
||||
Actual: "missing",
|
||||
Message: "Valid time anchor required but not present"));
|
||||
}
|
||||
else if (!status.TimeAnchor.Valid)
|
||||
{
|
||||
violations.Add(new RequirementViolation(
|
||||
Requirement: "require_time_anchor",
|
||||
Expected: "valid time anchor",
|
||||
Actual: "invalid",
|
||||
Message: "Time anchor present but invalid or expired"));
|
||||
}
|
||||
else if (status.TimeAnchor.ExpiresAt.HasValue &&
|
||||
status.TimeAnchor.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
{
|
||||
violations.Add(new RequirementViolation(
|
||||
Requirement: "require_time_anchor",
|
||||
Expected: "non-expired time anchor",
|
||||
Actual: $"expired at {status.TimeAnchor.ExpiresAt.Value:O}",
|
||||
Message: "Time anchor has expired"));
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static bool IsVersionSatisfied(string actual, string required)
|
||||
{
|
||||
// Try semantic version comparison
|
||||
if (Version.TryParse(NormalizeVersion(actual), out var actualVersion) &&
|
||||
Version.TryParse(NormalizeVersion(required), out var requiredVersion))
|
||||
{
|
||||
return actualVersion >= requiredVersion;
|
||||
}
|
||||
|
||||
// Fall back to string comparison
|
||||
return string.Compare(actual, required, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static string NormalizeVersion(string version)
|
||||
{
|
||||
// Strip common prefixes like 'v' and suffixes like '-beta'
|
||||
var normalized = version.TrimStart('v', 'V');
|
||||
var dashIndex = normalized.IndexOf('-', StringComparison.Ordinal);
|
||||
if (dashIndex > 0)
|
||||
{
|
||||
normalized = normalized[..dashIndex];
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static bool IsDevelopmentEnvironment()
|
||||
{
|
||||
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ??
|
||||
Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
|
||||
|
||||
return string.Equals(env, "Development", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for sealed install enforcement.
|
||||
/// </summary>
|
||||
public sealed class SealedInstallEnforcementOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether enforcement is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Grace period for advisory staleness in hours.
|
||||
/// </summary>
|
||||
public int StalenessGracePeriodHours { get; set; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Warning threshold for staleness in hours.
|
||||
/// </summary>
|
||||
public int StalenessWarningThresholdHours { get; set; } = 120;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to deny on staleness violation (false = warn only).
|
||||
/// </summary>
|
||||
public bool DenyOnStaleness { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use heuristic detection when AirGap controller is unavailable.
|
||||
/// </summary>
|
||||
public bool UseHeuristicDetection { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Heuristic score threshold to consider environment sealed.
|
||||
/// </summary>
|
||||
public double HeuristicThreshold { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Bypass enforcement in development environments (DANGEROUS).
|
||||
/// </summary>
|
||||
public bool BypassForDevelopment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Log all enforcement decisions.
|
||||
/// </summary>
|
||||
public bool LogAllDecisions { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Audit retention in days.
|
||||
/// </summary>
|
||||
public int AuditRetentionDays { get; set; } = 365;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the sealed mode status of the air-gap environment.
|
||||
/// Per sealed-install-enforcement.md contract.
|
||||
/// </summary>
|
||||
public sealed record SealedModeStatus(
|
||||
/// <summary>Whether the environment is currently sealed.</summary>
|
||||
bool Sealed,
|
||||
|
||||
/// <summary>Current mode (sealed, unsealed, transitioning).</summary>
|
||||
string Mode,
|
||||
|
||||
/// <summary>When the environment was sealed.</summary>
|
||||
DateTimeOffset? SealedAt,
|
||||
|
||||
/// <summary>Identity that sealed the environment.</summary>
|
||||
string? SealedBy,
|
||||
|
||||
/// <summary>Air-gap bundle version currently installed.</summary>
|
||||
string? BundleVersion,
|
||||
|
||||
/// <summary>Digest of the bundle.</summary>
|
||||
string? BundleDigest,
|
||||
|
||||
/// <summary>When advisories were last updated.</summary>
|
||||
DateTimeOffset? LastAdvisoryUpdate,
|
||||
|
||||
/// <summary>Hours since last advisory update.</summary>
|
||||
int AdvisoryStalenessHours,
|
||||
|
||||
/// <summary>Time anchor information.</summary>
|
||||
TimeAnchorInfo? TimeAnchor,
|
||||
|
||||
/// <summary>Whether egress is blocked.</summary>
|
||||
bool EgressBlocked,
|
||||
|
||||
/// <summary>Network policy in effect.</summary>
|
||||
string? NetworkPolicy)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an unsealed status (environment not in air-gap mode).
|
||||
/// </summary>
|
||||
public static SealedModeStatus Unsealed() => new(
|
||||
Sealed: false,
|
||||
Mode: "unsealed",
|
||||
SealedAt: null,
|
||||
SealedBy: null,
|
||||
BundleVersion: null,
|
||||
BundleDigest: null,
|
||||
LastAdvisoryUpdate: null,
|
||||
AdvisoryStalenessHours: 0,
|
||||
TimeAnchor: null,
|
||||
EgressBlocked: false,
|
||||
NetworkPolicy: null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a status indicating the provider is unavailable.
|
||||
/// </summary>
|
||||
public static SealedModeStatus Unavailable() => new(
|
||||
Sealed: false,
|
||||
Mode: "unavailable",
|
||||
SealedAt: null,
|
||||
SealedBy: null,
|
||||
BundleVersion: null,
|
||||
BundleDigest: null,
|
||||
LastAdvisoryUpdate: null,
|
||||
AdvisoryStalenessHours: 0,
|
||||
TimeAnchor: null,
|
||||
EgressBlocked: false,
|
||||
NetworkPolicy: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time anchor information for sealed environments.
|
||||
/// </summary>
|
||||
public sealed record TimeAnchorInfo(
|
||||
/// <summary>The anchor timestamp.</summary>
|
||||
DateTimeOffset Timestamp,
|
||||
|
||||
/// <summary>Signature of the time anchor.</summary>
|
||||
string? Signature,
|
||||
|
||||
/// <summary>Whether the time anchor is valid.</summary>
|
||||
bool Valid,
|
||||
|
||||
/// <summary>When the time anchor expires.</summary>
|
||||
DateTimeOffset? ExpiresAt);
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Sealed install requirements specified in a task pack manifest.
|
||||
/// Per sealed-install-enforcement.md contract.
|
||||
/// </summary>
|
||||
public sealed record SealedRequirements(
|
||||
/// <summary>Minimum air-gap bundle version required.</summary>
|
||||
[property: JsonPropertyName("min_bundle_version")]
|
||||
string? MinBundleVersion,
|
||||
|
||||
/// <summary>Maximum age of advisory data in hours (default: 168).</summary>
|
||||
[property: JsonPropertyName("max_advisory_staleness_hours")]
|
||||
int MaxAdvisoryStalenessHours,
|
||||
|
||||
/// <summary>Whether a valid time anchor is required (default: true).</summary>
|
||||
[property: JsonPropertyName("require_time_anchor")]
|
||||
bool RequireTimeAnchor,
|
||||
|
||||
/// <summary>Maximum allowed offline duration in hours (default: 720).</summary>
|
||||
[property: JsonPropertyName("allowed_offline_duration_hours")]
|
||||
int AllowedOfflineDurationHours,
|
||||
|
||||
/// <summary>Whether bundle signature verification is required (default: true).</summary>
|
||||
[property: JsonPropertyName("require_signature_verification")]
|
||||
bool RequireSignatureVerification)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default sealed requirements.
|
||||
/// </summary>
|
||||
public static SealedRequirements Default => new(
|
||||
MinBundleVersion: null,
|
||||
MaxAdvisoryStalenessHours: 168,
|
||||
RequireTimeAnchor: true,
|
||||
AllowedOfflineDurationHours: 720,
|
||||
RequireSignatureVerification: true);
|
||||
}
|
||||
@@ -301,6 +301,18 @@ public static class PackRunEventTypes
|
||||
/// <summary>Policy gate evaluated.</summary>
|
||||
public const string PolicyEvaluated = "pack.policy.evaluated";
|
||||
|
||||
/// <summary>Sealed install enforcement performed.</summary>
|
||||
public const string SealedInstallEnforcement = "pack.sealed_install.enforcement";
|
||||
|
||||
/// <summary>Sealed install enforcement denied execution.</summary>
|
||||
public const string SealedInstallDenied = "pack.sealed_install.denied";
|
||||
|
||||
/// <summary>Sealed install enforcement allowed execution.</summary>
|
||||
public const string SealedInstallAllowed = "pack.sealed_install.allowed";
|
||||
|
||||
/// <summary>Sealed install requirements warning.</summary>
|
||||
public const string SealedInstallWarning = "pack.sealed_install.warning";
|
||||
|
||||
/// <summary>Checks if the event type is a pack run event.</summary>
|
||||
public static bool IsPackRunEvent(string eventType) =>
|
||||
eventType.StartsWith(Prefix, StringComparison.Ordinal);
|
||||
|
||||
@@ -2,9 +2,9 @@ using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
internal static class TaskRunnerTelemetry
|
||||
public static class TaskRunnerTelemetry
|
||||
{
|
||||
internal const string MeterName = "stellaops.taskrunner";
|
||||
public const string MeterName = "stellaops.taskrunner";
|
||||
|
||||
internal static readonly Meter Meter = new(MeterName);
|
||||
internal static readonly Histogram<double> StepDurationMs =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.TaskPacks;
|
||||
|
||||
@@ -82,6 +83,18 @@ public sealed class TaskPackSpec
|
||||
|
||||
[JsonPropertyName("slo")]
|
||||
public TaskPackSlo? Slo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this pack requires a sealed (air-gapped) environment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sealedInstall")]
|
||||
public bool SealedInstall { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific requirements for sealed install mode.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sealedRequirements")]
|
||||
public SealedRequirements? SealedRequirements { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackInput
|
||||
|
||||
Reference in New Issue
Block a user