Refactor code structure for improved readability and maintainability
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-06 21:48:12 +02:00
parent f6c22854a4
commit dd0067ea0b
105 changed files with 12662 additions and 427 deletions

View File

@@ -0,0 +1,237 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.TaskRunner.Core.AirGap;
namespace StellaOps.TaskRunner.Infrastructure.AirGap;
/// <summary>
/// HTTP client implementation for retrieving air-gap status from the AirGap controller.
/// </summary>
public sealed class HttpAirGapStatusProvider : IAirGapStatusProvider
{
private readonly HttpClient _httpClient;
private readonly IOptions<AirGapStatusProviderOptions> _options;
private readonly ILogger<HttpAirGapStatusProvider> _logger;
public HttpAirGapStatusProvider(
HttpClient httpClient,
IOptions<AirGapStatusProviderOptions> options,
ILogger<HttpAirGapStatusProvider> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<SealedModeStatus> GetStatusAsync(
string? tenantId = null,
CancellationToken cancellationToken = default)
{
var options = _options.Value;
var url = string.IsNullOrWhiteSpace(tenantId)
? options.StatusEndpoint
: $"{options.StatusEndpoint}?tenantId={Uri.EscapeDataString(tenantId)}";
try
{
var response = await _httpClient.GetFromJsonAsync<AirGapStatusDto>(
url,
cancellationToken).ConfigureAwait(false);
if (response is null)
{
_logger.LogWarning("AirGap controller returned null response.");
return SealedModeStatus.Unavailable();
}
return MapToSealedModeStatus(response);
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Failed to connect to AirGap controller at {Url}.", url);
if (options.UseHeuristicFallback)
{
return await GetStatusFromHeuristicsAsync(cancellationToken).ConfigureAwait(false);
}
return SealedModeStatus.Unavailable();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error getting air-gap status.");
return SealedModeStatus.Unavailable();
}
}
private static SealedModeStatus MapToSealedModeStatus(AirGapStatusDto dto)
{
TimeAnchorInfo? timeAnchor = null;
if (dto.TimeAnchor is not null)
{
timeAnchor = new TimeAnchorInfo(
dto.TimeAnchor.Timestamp,
dto.TimeAnchor.Signature,
dto.TimeAnchor.Valid,
dto.TimeAnchor.ExpiresAt);
}
return new SealedModeStatus(
Sealed: dto.Sealed,
Mode: dto.Sealed ? "sealed" : "unsealed",
SealedAt: dto.SealedAt,
SealedBy: dto.SealedBy,
BundleVersion: dto.BundleVersion,
BundleDigest: dto.BundleDigest,
LastAdvisoryUpdate: dto.LastAdvisoryUpdate,
AdvisoryStalenessHours: dto.AdvisoryStalenessHours,
TimeAnchor: timeAnchor,
EgressBlocked: dto.EgressBlocked,
NetworkPolicy: dto.NetworkPolicy);
}
private async Task<SealedModeStatus> GetStatusFromHeuristicsAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Using heuristic detection for sealed mode status.");
var score = 0.0;
var weights = 0.0;
// Check AIRGAP_MODE environment variable (high weight)
var airgapMode = Environment.GetEnvironmentVariable("AIRGAP_MODE");
if (string.Equals(airgapMode, "sealed", StringComparison.OrdinalIgnoreCase))
{
score += 0.3;
}
weights += 0.3;
// Check for sealed file marker (medium weight)
var sealedMarkerPath = _options.Value.SealedMarkerPath;
if (!string.IsNullOrWhiteSpace(sealedMarkerPath) && File.Exists(sealedMarkerPath))
{
score += 0.2;
}
weights += 0.2;
// Check network connectivity (high weight)
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(2));
var testResponse = await _httpClient.GetAsync(
_options.Value.ConnectivityTestUrl,
cts.Token).ConfigureAwait(false);
// If we can reach external network, likely not sealed
}
catch (Exception)
{
// Network blocked, likely sealed
score += 0.3;
}
weights += 0.3;
// Check for local registry configuration (low weight)
var registryEnv = Environment.GetEnvironmentVariable("CONTAINER_REGISTRY");
if (!string.IsNullOrWhiteSpace(registryEnv) &&
(registryEnv.Contains("localhost", StringComparison.OrdinalIgnoreCase) ||
registryEnv.Contains("127.0.0.1", StringComparison.Ordinal)))
{
score += 0.1;
}
weights += 0.1;
// Check proxy settings (low weight)
var httpProxy = Environment.GetEnvironmentVariable("HTTP_PROXY") ??
Environment.GetEnvironmentVariable("http_proxy");
var noProxy = Environment.GetEnvironmentVariable("NO_PROXY") ??
Environment.GetEnvironmentVariable("no_proxy");
if (string.IsNullOrWhiteSpace(httpProxy) && !string.IsNullOrWhiteSpace(noProxy))
{
score += 0.1;
}
weights += 0.1;
var normalizedScore = weights > 0 ? score / weights : 0;
var threshold = _options.Value.HeuristicThreshold;
var isSealed = normalizedScore >= threshold;
_logger.LogInformation(
"Heuristic detection result: score={Score:F2}, threshold={Threshold:F2}, sealed={IsSealed}",
normalizedScore,
threshold,
isSealed);
return new SealedModeStatus(
Sealed: isSealed,
Mode: isSealed ? "sealed-heuristic" : "unsealed-heuristic",
SealedAt: null,
SealedBy: null,
BundleVersion: null,
BundleDigest: null,
LastAdvisoryUpdate: null,
AdvisoryStalenessHours: 0,
TimeAnchor: null,
EgressBlocked: isSealed,
NetworkPolicy: isSealed ? "heuristic-detected" : null);
}
private sealed record AirGapStatusDto(
[property: JsonPropertyName("sealed")] bool Sealed,
[property: JsonPropertyName("sealed_at")] DateTimeOffset? SealedAt,
[property: JsonPropertyName("sealed_by")] string? SealedBy,
[property: JsonPropertyName("bundle_version")] string? BundleVersion,
[property: JsonPropertyName("bundle_digest")] string? BundleDigest,
[property: JsonPropertyName("last_advisory_update")] DateTimeOffset? LastAdvisoryUpdate,
[property: JsonPropertyName("advisory_staleness_hours")] int AdvisoryStalenessHours,
[property: JsonPropertyName("time_anchor")] TimeAnchorDto? TimeAnchor,
[property: JsonPropertyName("egress_blocked")] bool EgressBlocked,
[property: JsonPropertyName("network_policy")] string? NetworkPolicy);
private sealed record TimeAnchorDto(
[property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp,
[property: JsonPropertyName("signature")] string? Signature,
[property: JsonPropertyName("valid")] bool Valid,
[property: JsonPropertyName("expires_at")] DateTimeOffset? ExpiresAt);
}
/// <summary>
/// Configuration options for the HTTP air-gap status provider.
/// </summary>
public sealed class AirGapStatusProviderOptions
{
/// <summary>
/// Base URL of the AirGap controller.
/// </summary>
public string BaseUrl { get; set; } = "http://localhost:8080";
/// <summary>
/// Status endpoint path.
/// </summary>
public string StatusEndpoint { get; set; } = "/api/v1/airgap/status";
/// <summary>
/// Whether to use heuristic fallback when controller is unavailable.
/// </summary>
public bool UseHeuristicFallback { get; set; } = true;
/// <summary>
/// Heuristic score threshold (0.0-1.0) to consider environment sealed.
/// </summary>
public double HeuristicThreshold { get; set; } = 0.7;
/// <summary>
/// Path to the sealed mode marker file.
/// </summary>
public string? SealedMarkerPath { get; set; } = "/etc/stellaops/sealed";
/// <summary>
/// URL to test external connectivity.
/// </summary>
public string ConnectivityTestUrl { get; set; } = "https://api.stellaops.org/health";
}