- Implement ProofChainTestFixture for PostgreSQL-backed integration tests. - Create StellaOps.Integration.ProofChain project with necessary dependencies. - Add ReachabilityIntegrationTests to validate call graph extraction and reachability analysis. - Introduce ReachabilityTestFixture for managing corpus and fixture paths. - Establish StellaOps.Integration.Reachability project with required references. - Develop UnknownsWorkflowTests to cover the unknowns lifecycle: detection, ranking, escalation, and resolution. - Create StellaOps.Integration.Unknowns project with dependencies for unknowns workflow.
419 lines
12 KiB
C#
419 lines
12 KiB
C#
// =============================================================================
|
|
// StellaOps.Integration.AirGap - Air-Gap Test Fixture
|
|
// Sprint 3500.0004.0003 - T8: Air-Gap Integration Tests
|
|
// =============================================================================
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Integration.AirGap;
|
|
|
|
/// <summary>
|
|
/// Test fixture for air-gap integration tests.
|
|
/// Manages offline kit, network simulation, and test artifacts.
|
|
/// </summary>
|
|
public sealed class AirGapTestFixture : IDisposable
|
|
{
|
|
private readonly string _offlineKitPath;
|
|
private readonly string _tempDir;
|
|
private bool _offlineMode;
|
|
private Action<string>? _connectionMonitor;
|
|
private Action<string>? _dnsMonitor;
|
|
|
|
public AirGapTestFixture()
|
|
{
|
|
_offlineKitPath = Path.Combine(AppContext.BaseDirectory, "offline-kit");
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"stellaops-airgap-test-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
#region Offline Kit
|
|
|
|
public OfflineKitManifest GetOfflineKitManifest()
|
|
{
|
|
var manifestPath = Path.Combine(_offlineKitPath, "manifest.json");
|
|
|
|
if (File.Exists(manifestPath))
|
|
{
|
|
var json = File.ReadAllText(manifestPath);
|
|
return JsonSerializer.Deserialize<OfflineKitManifest>(json) ?? GetDefaultManifest();
|
|
}
|
|
|
|
return GetDefaultManifest();
|
|
}
|
|
|
|
public async Task<string> ComputeComponentHashAsync(string componentName)
|
|
{
|
|
var componentPath = Path.Combine(_offlineKitPath, componentName);
|
|
|
|
if (!Directory.Exists(componentPath) && !File.Exists(componentPath))
|
|
{
|
|
return "MISSING";
|
|
}
|
|
|
|
using var sha256 = SHA256.Create();
|
|
|
|
if (File.Exists(componentPath))
|
|
{
|
|
await using var stream = File.OpenRead(componentPath);
|
|
var hash = await sha256.ComputeHashAsync(stream);
|
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
|
}
|
|
|
|
// Directory - hash all files
|
|
var files = Directory.GetFiles(componentPath, "*", SearchOption.AllDirectories)
|
|
.OrderBy(f => f)
|
|
.ToList();
|
|
|
|
using var combinedStream = new MemoryStream();
|
|
foreach (var file in files)
|
|
{
|
|
await using var fileStream = File.OpenRead(file);
|
|
await fileStream.CopyToAsync(combinedStream);
|
|
}
|
|
|
|
combinedStream.Position = 0;
|
|
var dirHash = await sha256.ComputeHashAsync(combinedStream);
|
|
return Convert.ToHexString(dirHash).ToLowerInvariant();
|
|
}
|
|
|
|
public async Task<InstallationResult> InstallOfflineKitAsync(string targetPath)
|
|
{
|
|
await Task.Delay(10); // Simulate installation
|
|
|
|
var manifest = GetOfflineKitManifest();
|
|
var installed = new List<string>();
|
|
|
|
foreach (var (name, _) in manifest.Components)
|
|
{
|
|
var sourcePath = Path.Combine(_offlineKitPath, name);
|
|
var destPath = Path.Combine(targetPath, name);
|
|
|
|
if (Directory.Exists(sourcePath))
|
|
{
|
|
Directory.CreateDirectory(destPath);
|
|
// Simulate copy
|
|
}
|
|
else if (File.Exists(sourcePath))
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
|
|
// Simulate copy
|
|
}
|
|
|
|
installed.Add(name);
|
|
}
|
|
|
|
return new InstallationResult
|
|
{
|
|
Success = true,
|
|
InstalledComponents = installed
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Images
|
|
|
|
public string GetLocalTestImage()
|
|
{
|
|
return "localhost/test-image:v1.0.0";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scanning
|
|
|
|
public async Task<ScanResult> RunOfflineScanAsync(string targetImage)
|
|
{
|
|
await Task.Delay(50); // Simulate scan
|
|
|
|
if (!_offlineMode)
|
|
{
|
|
_connectionMonitor?.Invoke("nvd.nist.gov:443");
|
|
}
|
|
|
|
return new ScanResult
|
|
{
|
|
Success = true,
|
|
Findings = GenerateSampleFindings(),
|
|
ManifestHash = "sha256:abc123def456",
|
|
DataSource = _offlineMode ? "offline-kit" : "online",
|
|
DataSourcePath = _offlineMode ? _offlineKitPath : "https://feeds.stellaops.io",
|
|
TelemetrySent = !_offlineMode,
|
|
Configuration = new ScanConfiguration
|
|
{
|
|
TelemetryEnabled = !_offlineMode
|
|
}
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Score Replay
|
|
|
|
public ProofBundle GetSampleProofBundle()
|
|
{
|
|
return new ProofBundle
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
CreatedAt = DateTime.UtcNow.AddDays(-1),
|
|
OriginalScore = 7.5,
|
|
OriginalScoreHash = "sha256:score123",
|
|
Signature = Convert.ToBase64String(new byte[64]),
|
|
CertificateChain = new[] { "cert1", "cert2", "root" }
|
|
};
|
|
}
|
|
|
|
public async Task<ReplayResult> ReplayScoreOfflineAsync(ProofBundle bundle)
|
|
{
|
|
await Task.Delay(20); // Simulate replay
|
|
|
|
return new ReplayResult
|
|
{
|
|
Success = true,
|
|
Score = bundle.OriginalScore,
|
|
ScoreHash = bundle.OriginalScoreHash,
|
|
ReplayedAt = DateTime.UtcNow,
|
|
AuditTrail = new[]
|
|
{
|
|
new AuditEntry { Type = "replay_started", Timestamp = DateTime.UtcNow.AddMilliseconds(-20) },
|
|
new AuditEntry { Type = "data_loaded", Timestamp = DateTime.UtcNow.AddMilliseconds(-15) },
|
|
new AuditEntry { Type = "score_computed", Timestamp = DateTime.UtcNow.AddMilliseconds(-5) },
|
|
new AuditEntry { Type = "replay_completed", Timestamp = DateTime.UtcNow }
|
|
}
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Proof Verification
|
|
|
|
public async Task<VerificationResult> VerifyProofOfflineAsync(ProofBundle bundle)
|
|
{
|
|
await Task.Delay(10); // Simulate verification
|
|
|
|
var isTampered = bundle.Signature.Contains("TAMPERED");
|
|
var isExpired = bundle.CertificateChain.Any(c => c.Contains("EXPIRED"));
|
|
|
|
return new VerificationResult
|
|
{
|
|
Valid = !isTampered && !isExpired,
|
|
VerifiedAt = DateTime.UtcNow,
|
|
TrustSource = "offline-trust-store",
|
|
CertificateChain = bundle.CertificateChain,
|
|
FailureReason = isTampered ? "Invalid signature" : (isExpired ? "Certificate expired" : null),
|
|
Warnings = isExpired ? new[] { "certificate chain contains expired certificate" } : Array.Empty<string>()
|
|
};
|
|
}
|
|
|
|
public ProofBundle TamperWithProof(ProofBundle original)
|
|
{
|
|
return original with
|
|
{
|
|
Signature = "TAMPERED_" + original.Signature
|
|
};
|
|
}
|
|
|
|
public ProofBundle GetProofBundleWithExpiredCert()
|
|
{
|
|
return new ProofBundle
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
CreatedAt = DateTime.UtcNow.AddYears(-2),
|
|
OriginalScore = 5.0,
|
|
OriginalScoreHash = "sha256:expired123",
|
|
Signature = Convert.ToBase64String(new byte[64]),
|
|
CertificateChain = new[] { "cert1", "EXPIRED_cert2", "root" }
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Network Control
|
|
|
|
public void SetOfflineMode(bool offline)
|
|
{
|
|
_offlineMode = offline;
|
|
}
|
|
|
|
public async Task DisableNetworkAsync()
|
|
{
|
|
_offlineMode = true;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public async Task EnableNetworkAsync()
|
|
{
|
|
_offlineMode = false;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public void SetConnectionMonitor(Action<string> monitor)
|
|
{
|
|
_connectionMonitor = monitor;
|
|
}
|
|
|
|
public void SetDnsMonitor(Action<string> monitor)
|
|
{
|
|
_dnsMonitor = monitor;
|
|
}
|
|
|
|
public async Task<OnlineUpdateResult> AttemptOnlineUpdateAsync()
|
|
{
|
|
if (_offlineMode)
|
|
{
|
|
return new OnlineUpdateResult
|
|
{
|
|
Success = false,
|
|
FailureReason = "System is in offline mode",
|
|
SuggestedAction = "Use offline-kit update mechanism"
|
|
};
|
|
}
|
|
|
|
await Task.Delay(100);
|
|
return new OnlineUpdateResult { Success = true };
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
public string GetTempDirectory()
|
|
{
|
|
var path = Path.Combine(_tempDir, Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
|
|
private static List<Finding> GenerateSampleFindings()
|
|
{
|
|
return new List<Finding>
|
|
{
|
|
new() { CveId = "CVE-2024-00001", Severity = "HIGH", Score = 8.0 },
|
|
new() { CveId = "CVE-2024-00002", Severity = "MEDIUM", Score = 5.5 },
|
|
new() { CveId = "CVE-2024-00003", Severity = "LOW", Score = 3.2 }
|
|
};
|
|
}
|
|
|
|
private static OfflineKitManifest GetDefaultManifest()
|
|
{
|
|
return new OfflineKitManifest
|
|
{
|
|
Version = "1.0.0",
|
|
CreatedAt = DateTime.UtcNow.AddDays(-7),
|
|
Components = new Dictionary<string, OfflineComponent>
|
|
{
|
|
["vulnerability-database"] = new() { Hash = "sha256:vulndb123", Size = 1024 * 1024 },
|
|
["advisory-feeds"] = new() { Hash = "sha256:feeds456", Size = 512 * 1024 },
|
|
["trust-bundles"] = new() { Hash = "sha256:trust789", Size = 64 * 1024 },
|
|
["signing-keys"] = new() { Hash = "sha256:keys012", Size = 16 * 1024 }
|
|
}
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch
|
|
{
|
|
// Best effort cleanup
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Record Types
|
|
|
|
public record OfflineKitManifest
|
|
{
|
|
public string Version { get; init; } = "";
|
|
public DateTime CreatedAt { get; init; }
|
|
public Dictionary<string, OfflineComponent> Components { get; init; } = new();
|
|
}
|
|
|
|
public record OfflineComponent
|
|
{
|
|
public string Hash { get; init; } = "";
|
|
public long Size { get; init; }
|
|
}
|
|
|
|
public record InstallationResult
|
|
{
|
|
public bool Success { get; init; }
|
|
public List<string> InstalledComponents { get; init; } = new();
|
|
}
|
|
|
|
public record ScanResult
|
|
{
|
|
public bool Success { get; init; }
|
|
public List<Finding> Findings { get; init; } = new();
|
|
public string ManifestHash { get; init; } = "";
|
|
public string DataSource { get; init; } = "";
|
|
public string DataSourcePath { get; init; } = "";
|
|
public bool TelemetrySent { get; init; }
|
|
public ScanConfiguration Configuration { get; init; } = new();
|
|
}
|
|
|
|
public record ScanConfiguration
|
|
{
|
|
public bool TelemetryEnabled { get; init; }
|
|
}
|
|
|
|
public record Finding
|
|
{
|
|
public string CveId { get; init; } = "";
|
|
public string Severity { get; init; } = "";
|
|
public double Score { get; init; }
|
|
}
|
|
|
|
public record ProofBundle
|
|
{
|
|
public string Id { get; init; } = "";
|
|
public DateTime CreatedAt { get; init; }
|
|
public double OriginalScore { get; init; }
|
|
public string OriginalScoreHash { get; init; } = "";
|
|
public string Signature { get; init; } = "";
|
|
public string[] CertificateChain { get; init; } = Array.Empty<string>();
|
|
}
|
|
|
|
public record ReplayResult
|
|
{
|
|
public bool Success { get; init; }
|
|
public double Score { get; init; }
|
|
public string ScoreHash { get; init; } = "";
|
|
public DateTime ReplayedAt { get; init; }
|
|
public AuditEntry[] AuditTrail { get; init; } = Array.Empty<AuditEntry>();
|
|
}
|
|
|
|
public record AuditEntry
|
|
{
|
|
public string Type { get; init; } = "";
|
|
public DateTime Timestamp { get; init; }
|
|
}
|
|
|
|
public record VerificationResult
|
|
{
|
|
public bool Valid { get; init; }
|
|
public DateTime VerifiedAt { get; init; }
|
|
public string TrustSource { get; init; } = "";
|
|
public string[] CertificateChain { get; init; } = Array.Empty<string>();
|
|
public string? FailureReason { get; init; }
|
|
public string[] Warnings { get; init; } = Array.Empty<string>();
|
|
}
|
|
|
|
public record OnlineUpdateResult
|
|
{
|
|
public bool Success { get; init; }
|
|
public string? FailureReason { get; init; }
|
|
public string? SuggestedAction { get; init; }
|
|
}
|
|
|
|
#endregion
|