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,384 @@
|
||||
// =============================================================================
|
||||
// StellaOps.Integration.AirGap - Air-Gap Integration Tests
|
||||
// Sprint 3500.0004.0003 - T8: Air-Gap Integration Tests
|
||||
// =============================================================================
|
||||
|
||||
using FluentAssertions;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Integration.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for air-gapped (offline) operation.
|
||||
/// Validates that StellaOps functions correctly without network access.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// T8-AC1: Offline kit installation test
|
||||
/// T8-AC2: Offline scan test
|
||||
/// T8-AC3: Offline score replay test
|
||||
/// T8-AC4: Offline proof verification test
|
||||
/// T8-AC5: No network calls during offline operation
|
||||
/// </remarks>
|
||||
[Trait("Category", "AirGap")]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "Offline")]
|
||||
public class AirGapIntegrationTests : IClassFixture<AirGapTestFixture>
|
||||
{
|
||||
private readonly AirGapTestFixture _fixture;
|
||||
|
||||
public AirGapIntegrationTests(AirGapTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
#region T8-AC1: Offline Kit Installation
|
||||
|
||||
[Fact(DisplayName = "T8-AC1.1: Offline kit manifest is valid")]
|
||||
public void OfflineKitManifest_IsValid()
|
||||
{
|
||||
// Arrange & Act
|
||||
var manifest = _fixture.GetOfflineKitManifest();
|
||||
|
||||
// Assert
|
||||
manifest.Should().NotBeNull();
|
||||
manifest.Version.Should().NotBeNullOrEmpty();
|
||||
manifest.Components.Should().NotBeEmpty();
|
||||
manifest.CreatedAt.Should().BeBefore(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC1.2: All required components present")]
|
||||
public void OfflineKit_HasRequiredComponents()
|
||||
{
|
||||
// Arrange
|
||||
var requiredComponents = new[]
|
||||
{
|
||||
"vulnerability-database",
|
||||
"advisory-feeds",
|
||||
"trust-bundles",
|
||||
"signing-keys"
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = _fixture.GetOfflineKitManifest();
|
||||
|
||||
// Assert
|
||||
foreach (var component in requiredComponents)
|
||||
{
|
||||
manifest.Components.Should().ContainKey(component,
|
||||
$"Offline kit missing required component: {component}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC1.3: Component hashes are valid")]
|
||||
public async Task OfflineKitComponents_HaveValidHashes()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = _fixture.GetOfflineKitManifest();
|
||||
var invalidComponents = new List<string>();
|
||||
|
||||
// Act
|
||||
foreach (var (name, component) in manifest.Components)
|
||||
{
|
||||
var actualHash = await _fixture.ComputeComponentHashAsync(name);
|
||||
if (actualHash != component.Hash)
|
||||
{
|
||||
invalidComponents.Add($"{name}: expected {component.Hash}, got {actualHash}");
|
||||
}
|
||||
}
|
||||
|
||||
// Assert
|
||||
invalidComponents.Should().BeEmpty(
|
||||
$"Components with invalid hashes:\n{string.Join("\n", invalidComponents)}");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC1.4: Offline kit installation succeeds")]
|
||||
public async Task OfflineKitInstallation_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var targetPath = _fixture.GetTempDirectory();
|
||||
|
||||
// Act
|
||||
var result = await _fixture.InstallOfflineKitAsync(targetPath);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.InstalledComponents.Should().NotBeEmpty();
|
||||
Directory.Exists(targetPath).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region T8-AC2: Offline Scan
|
||||
|
||||
[Fact(DisplayName = "T8-AC2.1: Scan completes without network")]
|
||||
public async Task OfflineScan_CompletesWithoutNetwork()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.DisableNetworkAsync();
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Findings.Should().NotBeNull();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _fixture.EnableNetworkAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC2.2: Scan uses local vulnerability database")]
|
||||
public async Task OfflineScan_UsesLocalVulnDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.DataSource.Should().Be("offline-kit");
|
||||
result.DataSourcePath.Should().Contain("offline");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC2.3: Scan produces deterministic results offline")]
|
||||
public async Task OfflineScan_ProducesDeterministicResults()
|
||||
{
|
||||
// Arrange
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act - run twice
|
||||
var result1 = await _fixture.RunOfflineScanAsync(targetImage);
|
||||
var result2 = await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
result1.ManifestHash.Should().Be(result2.ManifestHash,
|
||||
"Offline scan should produce identical results");
|
||||
result1.Findings.Count.Should().Be(result2.Findings.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region T8-AC3: Offline Score Replay
|
||||
|
||||
[Fact(DisplayName = "T8-AC3.1: Score replay works offline")]
|
||||
public async Task ScoreReplay_WorksOffline()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ReplayScoreOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(0);
|
||||
result.ReplayedAt.Should().BeBefore(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC3.2: Score replay produces identical score")]
|
||||
public async Task ScoreReplay_ProducesIdenticalScore()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
var originalScore = proofBundle.OriginalScore;
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ReplayScoreOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(originalScore,
|
||||
"Replay score should match original");
|
||||
result.ScoreHash.Should().Be(proofBundle.OriginalScoreHash,
|
||||
"Replay score hash should match original");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC3.3: Score replay includes audit trail")]
|
||||
public async Task ScoreReplay_IncludesAuditTrail()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ReplayScoreOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.AuditTrail.Should().NotBeEmpty();
|
||||
result.AuditTrail.Should().Contain(a => a.Type == "replay_started");
|
||||
result.AuditTrail.Should().Contain(a => a.Type == "replay_completed");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region T8-AC4: Offline Proof Verification
|
||||
|
||||
[Fact(DisplayName = "T8-AC4.1: Proof verification works offline")]
|
||||
public async Task ProofVerification_WorksOffline()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.VerifyProofOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeTrue();
|
||||
result.VerifiedAt.Should().BeBefore(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC4.2: Verification uses offline trust store")]
|
||||
public async Task ProofVerification_UsesOfflineTrustStore()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.VerifyProofOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.TrustSource.Should().Be("offline-trust-store");
|
||||
result.CertificateChain.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC4.3: Tampered proof fails verification")]
|
||||
public async Task TamperedProof_FailsVerification()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
var tamperedBundle = _fixture.TamperWithProof(proofBundle);
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.VerifyProofOfflineAsync(tamperedBundle);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("signature");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC4.4: Expired certificate handling offline")]
|
||||
public async Task ExpiredCertificate_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var proofBundle = _fixture.GetProofBundleWithExpiredCert();
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.VerifyProofOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("expired");
|
||||
result.Warnings.Should().ContainSingle(w => w.Contains("certificate"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region T8-AC5: No Network Calls
|
||||
|
||||
[Fact(DisplayName = "T8-AC5.1: No outbound connections during scan")]
|
||||
public async Task OfflineScan_NoOutboundConnections()
|
||||
{
|
||||
// Arrange
|
||||
var connectionAttempts = new List<string>();
|
||||
_fixture.SetConnectionMonitor(endpoint => connectionAttempts.Add(endpoint));
|
||||
_fixture.SetOfflineMode(true);
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
|
||||
// Act
|
||||
await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
connectionAttempts.Should().BeEmpty(
|
||||
$"Unexpected network connections:\n{string.Join("\n", connectionAttempts)}");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC5.2: No outbound connections during verification")]
|
||||
public async Task OfflineVerification_NoOutboundConnections()
|
||||
{
|
||||
// Arrange
|
||||
var connectionAttempts = new List<string>();
|
||||
_fixture.SetConnectionMonitor(endpoint => connectionAttempts.Add(endpoint));
|
||||
_fixture.SetOfflineMode(true);
|
||||
var proofBundle = _fixture.GetSampleProofBundle();
|
||||
|
||||
// Act
|
||||
await _fixture.VerifyProofOfflineAsync(proofBundle);
|
||||
|
||||
// Assert
|
||||
connectionAttempts.Should().BeEmpty(
|
||||
$"Unexpected network connections:\n{string.Join("\n", connectionAttempts)}");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC5.3: No DNS lookups in offline mode")]
|
||||
public async Task OfflineMode_NoDnsLookups()
|
||||
{
|
||||
// Arrange
|
||||
var dnsLookups = new List<string>();
|
||||
_fixture.SetDnsMonitor(hostname => dnsLookups.Add(hostname));
|
||||
_fixture.SetOfflineMode(true);
|
||||
|
||||
// Act
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
dnsLookups.Should().BeEmpty(
|
||||
$"Unexpected DNS lookups:\n{string.Join("\n", dnsLookups)}");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC5.4: Telemetry disabled in offline mode")]
|
||||
public async Task OfflineMode_TelemetryDisabled()
|
||||
{
|
||||
// Arrange
|
||||
_fixture.SetOfflineMode(true);
|
||||
var targetImage = _fixture.GetLocalTestImage();
|
||||
|
||||
// Act
|
||||
var result = await _fixture.RunOfflineScanAsync(targetImage);
|
||||
|
||||
// Assert
|
||||
result.TelemetrySent.Should().BeFalse();
|
||||
result.Configuration.TelemetryEnabled.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "T8-AC5.5: Network operations gracefully fail")]
|
||||
public async Task NetworkOperations_GracefullyFail()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.DisableNetworkAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - attempt online operation
|
||||
var result = await _fixture.AttemptOnlineUpdateAsync();
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("offline");
|
||||
result.SuggestedAction.Should().Contain("offline-kit");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _fixture.EnableNetworkAsync();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Scanner\StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj" />
|
||||
<ProjectReference Include="..\..\src\Attestor\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Cli\StellaOps.Cli\StellaOps.Cli.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\offline-kit\**\*" LinkBase="offline-kit" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user