Add integration tests for Proof Chain and Reachability workflows
- 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.
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
// =============================================================================
|
||||
// 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
|
||||
Reference in New Issue
Block a user