tests fixes and sprints work
This commit is contained in:
1028
src/Verifier/BundleVerifier.cs
Normal file
1028
src/Verifier/BundleVerifier.cs
Normal file
File diff suppressed because it is too large
Load Diff
168
src/Verifier/Program.cs
Normal file
168
src/Verifier/Program.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Program.cs
|
||||
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
// Task: GCB-003 - Implement standalone offline verifier
|
||||
// Description: Entry point for standalone bundle verifier CLI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Parsing;
|
||||
using StellaOps.Verifier;
|
||||
|
||||
// Exit codes:
|
||||
// 0: All verifications passed
|
||||
// 1: One or more verifications failed
|
||||
// 2: Invalid input or configuration error
|
||||
|
||||
var bundleOption = new Option<FileInfo>("--bundle", ["-b"])
|
||||
{
|
||||
Description = "Path to the evidence bundle to verify",
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
var trustedKeysOption = new Option<FileInfo?>("--trusted-keys", ["-k"])
|
||||
{
|
||||
Description = "Path to trusted public keys file"
|
||||
};
|
||||
|
||||
var trustProfileOption = new Option<FileInfo?>("--trust-profile", ["-p"])
|
||||
{
|
||||
Description = "Path to trust profile JSON file"
|
||||
};
|
||||
|
||||
var outputOption = new Option<FileInfo?>("--output", ["-o"])
|
||||
{
|
||||
Description = "Path to write verification report"
|
||||
};
|
||||
|
||||
var formatOption = new Option<ReportFormat>("--format", ["-f"])
|
||||
{
|
||||
Description = "Report output format"
|
||||
};
|
||||
formatOption.SetDefaultValue(ReportFormat.Markdown);
|
||||
|
||||
var verifySignaturesOption = new Option<bool>("--verify-signatures")
|
||||
{
|
||||
Description = "Verify bundle manifest signatures"
|
||||
};
|
||||
verifySignaturesOption.SetDefaultValue(true);
|
||||
|
||||
var verifyTimestampsOption = new Option<bool>("--verify-timestamps")
|
||||
{
|
||||
Description = "Verify RFC 3161 timestamps"
|
||||
};
|
||||
verifyTimestampsOption.SetDefaultValue(true);
|
||||
|
||||
var verifyDigestsOption = new Option<bool>("--verify-digests")
|
||||
{
|
||||
Description = "Verify blob digests"
|
||||
};
|
||||
verifyDigestsOption.SetDefaultValue(true);
|
||||
|
||||
var verifyPairsOption = new Option<bool>("--verify-pairs")
|
||||
{
|
||||
Description = "Verify pair artifacts (SBOM, delta-sig)"
|
||||
};
|
||||
verifyPairsOption.SetDefaultValue(true);
|
||||
|
||||
var quietOption = new Option<bool>("--quiet", ["-q"])
|
||||
{
|
||||
Description = "Suppress output except for errors"
|
||||
};
|
||||
|
||||
var verboseOption = new Option<bool>("--verbose", ["-v"])
|
||||
{
|
||||
Description = "Show detailed verification output"
|
||||
};
|
||||
|
||||
var verifyCommand = new Command("verify", "Verify an evidence bundle")
|
||||
{
|
||||
bundleOption,
|
||||
trustedKeysOption,
|
||||
trustProfileOption,
|
||||
outputOption,
|
||||
formatOption,
|
||||
verifySignaturesOption,
|
||||
verifyTimestampsOption,
|
||||
verifyDigestsOption,
|
||||
verifyPairsOption,
|
||||
quietOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
verifyCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var bundle = context.ParseResult.GetValueForOption(bundleOption)!;
|
||||
var trustedKeys = context.ParseResult.GetValueForOption(trustedKeysOption);
|
||||
var trustProfile = context.ParseResult.GetValueForOption(trustProfileOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption);
|
||||
var format = context.ParseResult.GetValueForOption(formatOption);
|
||||
var verifySignatures = context.ParseResult.GetValueForOption(verifySignaturesOption);
|
||||
var verifyTimestamps = context.ParseResult.GetValueForOption(verifyTimestampsOption);
|
||||
var verifyDigests = context.ParseResult.GetValueForOption(verifyDigestsOption);
|
||||
var verifyPairs = context.ParseResult.GetValueForOption(verifyPairsOption);
|
||||
var quiet = context.ParseResult.GetValueForOption(quietOption);
|
||||
var verbose = context.ParseResult.GetValueForOption(verboseOption);
|
||||
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundle.FullName,
|
||||
TrustedKeysPath = trustedKeys?.FullName,
|
||||
TrustProfilePath = trustProfile?.FullName,
|
||||
OutputPath = output?.FullName,
|
||||
OutputFormat = format,
|
||||
VerifySignatures = verifySignatures,
|
||||
VerifyTimestamps = verifyTimestamps,
|
||||
VerifyDigests = verifyDigests,
|
||||
VerifyPairs = verifyPairs,
|
||||
Quiet = quiet,
|
||||
Verbose = verbose
|
||||
};
|
||||
|
||||
var verifier = new BundleVerifier();
|
||||
var exitCode = await verifier.VerifyAsync(options, context.GetCancellationToken());
|
||||
context.ExitCode = exitCode;
|
||||
});
|
||||
|
||||
var infoCommand = new Command("info", "Display bundle information without verification")
|
||||
{
|
||||
bundleOption,
|
||||
formatOption,
|
||||
quietOption
|
||||
};
|
||||
|
||||
infoCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var bundle = context.ParseResult.GetValueForOption(bundleOption)!;
|
||||
var format = context.ParseResult.GetValueForOption(formatOption);
|
||||
var quiet = context.ParseResult.GetValueForOption(quietOption);
|
||||
|
||||
var verifier = new BundleVerifier();
|
||||
var exitCode = await verifier.ShowInfoAsync(
|
||||
bundle.FullName,
|
||||
format,
|
||||
quiet,
|
||||
context.GetCancellationToken());
|
||||
context.ExitCode = exitCode;
|
||||
});
|
||||
|
||||
var rootCommand = new RootCommand("Stella Ops Bundle Verifier - Offline evidence bundle verification")
|
||||
{
|
||||
verifyCommand,
|
||||
infoCommand
|
||||
};
|
||||
|
||||
// Add version option
|
||||
rootCommand.AddOption(new Option<bool>("--version", "Show version information"));
|
||||
|
||||
var parser = new CommandLineBuilder(rootCommand)
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler((ex, context) =>
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
context.ExitCode = 2;
|
||||
})
|
||||
.Build();
|
||||
|
||||
return await parser.InvokeAsync(args);
|
||||
54
src/Verifier/StellaOps.Verifier.csproj
Normal file
54
src/Verifier/StellaOps.Verifier.csproj
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
StellaOps.Verifier.csproj
|
||||
Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
Task: GCB-003 - Implement standalone offline verifier
|
||||
Description: Standalone verifier for evidence bundles in air-gapped environments
|
||||
-->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
||||
<!-- Assembly metadata -->
|
||||
<AssemblyName>stella-verifier</AssemblyName>
|
||||
<RootNamespace>StellaOps.Verifier</RootNamespace>
|
||||
<Product>Stella Ops Bundle Verifier</Product>
|
||||
<Description>Standalone verifier for Stella Ops evidence bundles</Description>
|
||||
|
||||
<!-- Single-file and self-contained publishing -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
|
||||
<!-- Optimize for size -->
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Runtime identifiers for cross-platform builds -->
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == ''">
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;linux-musl-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Minimal dependencies for standalone operation -->
|
||||
<ItemGroup>
|
||||
<!-- No database, network, or heavy framework dependencies -->
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
25
src/Verifier/StellaOps.Verifier.sln
Normal file
25
src/Verifier/StellaOps.Verifier.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Verifier", "StellaOps.Verifier.csproj", "{5E7B8A1C-0F3D-4E6B-9C2A-1D8F7E6B5A4C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Verifier.Tests", "__Tests\StellaOps.Verifier.Tests\StellaOps.Verifier.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5E7B8A1C-0F3D-4E6B-9C2A-1D8F7E6B5A4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5E7B8A1C-0F3D-4E6B-9C2A-1D8F7E6B5A4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5E7B8A1C-0F3D-4E6B-9C2A-1D8F7E6B5A4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5E7B8A1C-0F3D-4E6B-9C2A-1D8F7E6B5A4C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
90
src/Verifier/VerifierOptions.cs
Normal file
90
src/Verifier/VerifierOptions.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VerifierOptions.cs
|
||||
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
// Task: GCB-003 - Implement standalone offline verifier
|
||||
// Description: Options for bundle verification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Verifier;
|
||||
|
||||
/// <summary>
|
||||
/// Options for bundle verification.
|
||||
/// </summary>
|
||||
public sealed class VerifierOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to the bundle file to verify.
|
||||
/// </summary>
|
||||
public required string BundlePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to trusted public keys file.
|
||||
/// </summary>
|
||||
public string? TrustedKeysPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to trust profile JSON file.
|
||||
/// </summary>
|
||||
public string? TrustProfilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to write verification report.
|
||||
/// </summary>
|
||||
public string? OutputPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output format for the report.
|
||||
/// </summary>
|
||||
public ReportFormat OutputFormat { get; init; } = ReportFormat.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to verify signatures.
|
||||
/// </summary>
|
||||
public bool VerifySignatures { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to verify timestamps.
|
||||
/// </summary>
|
||||
public bool VerifyTimestamps { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to verify digests.
|
||||
/// </summary>
|
||||
public bool VerifyDigests { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to verify pair artifacts.
|
||||
/// </summary>
|
||||
public bool VerifyPairs { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Suppress output except for errors.
|
||||
/// </summary>
|
||||
public bool Quiet { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Show detailed verification output.
|
||||
/// </summary>
|
||||
public bool Verbose { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Report output format.
|
||||
/// </summary>
|
||||
public enum ReportFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Markdown format.
|
||||
/// </summary>
|
||||
Markdown,
|
||||
|
||||
/// <summary>
|
||||
/// JSON format.
|
||||
/// </summary>
|
||||
Json,
|
||||
|
||||
/// <summary>
|
||||
/// Plain text format (for terminal output).
|
||||
/// </summary>
|
||||
Text
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BundleVerifierTests.cs
|
||||
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
// Task: GCB-003 - Implement standalone offline verifier
|
||||
// Description: Unit tests for BundleVerifier standalone verification logic
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Verifier.Tests;
|
||||
|
||||
public sealed class BundleVerifierTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly BundleVerifier _sut;
|
||||
|
||||
public BundleVerifierTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"verifier-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_sut = new BundleVerifier();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region Verify Tests
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_NonexistentBundle_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz"),
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2); // Error
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidBundle_ReturnsPassed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0); // Passed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_BundleWithBadDigest_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithBadDigest();
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Failed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_UnsignedBundle_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Warning treated as failure
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithOutputReport_WritesReport()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var reportPath = Path.Combine(_tempDir, "report.md");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
OutputPath = reportPath,
|
||||
OutputFormat = ReportFormat.Markdown,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
File.Exists(reportPath).Should().BeTrue();
|
||||
var content = await File.ReadAllTextAsync(reportPath);
|
||||
content.Should().Contain("Bundle Verification Report");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithJsonReport_WritesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var reportPath = Path.Combine(_tempDir, "report.json");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
OutputPath = reportPath,
|
||||
OutputFormat = ReportFormat.Json,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
File.Exists(reportPath).Should().BeTrue();
|
||||
var content = await File.ReadAllTextAsync(reportPath);
|
||||
var json = JsonDocument.Parse(content);
|
||||
json.RootElement.GetProperty("overallStatus").GetString().Should().Be("Passed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithTrustedKeys_ValidatesSignerKey()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithSignature("trusted-key-id");
|
||||
var trustedKeysPath = CreateTrustedKeysFile(["trusted-key-id"]);
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
TrustedKeysPath = trustedKeysPath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0); // Key is trusted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithUntrustedKey_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithSignature("untrusted-key-id");
|
||||
var trustedKeysPath = CreateTrustedKeysFile(["trusted-key-id"]);
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
TrustedKeysPath = trustedKeysPath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Key not trusted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithCancellation_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await cts.CancelAsync();
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options, cts.Token);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2); // Error (cancelled)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShowInfo Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ShowInfoAsync_ValidBundle_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.ShowInfoAsync(bundlePath, ReportFormat.Text, quiet: true);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowInfoAsync_NonexistentBundle_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz");
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.ShowInfoAsync(bundlePath, ReportFormat.Text, quiet: true);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string CreateTestBundle(string package, string advisoryId, string distribution)
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var pairId = $"{package}-{advisoryId}-{distribution}";
|
||||
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
|
||||
Directory.CreateDirectory(pairDir);
|
||||
|
||||
// Create binaries
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "pre.bin"), [1, 2, 3, 4]);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "post.bin"), [5, 6, 7, 8]);
|
||||
|
||||
// Create SBOM
|
||||
var sbom = new { spdxVersion = "SPDX-3.0.1", name = $"{package}-sbom" };
|
||||
var sbomContent = JsonSerializer.SerializeToUtf8Bytes(sbom);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "sbom.spdx.json"), sbomContent);
|
||||
var sbomDigest = ComputeHash(sbomContent);
|
||||
|
||||
// Create delta-sig
|
||||
var predicate = new { payloadType = "application/vnd.stella-ops.delta-sig+json", payload = "test" };
|
||||
var predicateContent = JsonSerializer.SerializeToUtf8Bytes(predicate);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "delta-sig.dsse.json"), predicateContent);
|
||||
var predicateDigest = ComputeHash(predicateContent);
|
||||
|
||||
// Create manifest
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "BundleVerifierTests",
|
||||
pairs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
pairId,
|
||||
package,
|
||||
advisoryId,
|
||||
distribution,
|
||||
sbomDigest,
|
||||
deltaSigDigest = predicateDigest
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTestBundleWithBadDigest()
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var pairId = "openssl-CVE-2024-1234-debian";
|
||||
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
|
||||
Directory.CreateDirectory(pairDir);
|
||||
|
||||
// Create SBOM with wrong digest in manifest
|
||||
var sbom = new { spdxVersion = "SPDX-3.0.1", name = "openssl-sbom" };
|
||||
File.WriteAllText(
|
||||
Path.Combine(pairDir, "sbom.spdx.json"),
|
||||
JsonSerializer.Serialize(sbom));
|
||||
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "Test",
|
||||
pairs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
pairId,
|
||||
package = "openssl",
|
||||
advisoryId = "CVE-2024-1234",
|
||||
distribution = "debian",
|
||||
sbomDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000", // Wrong
|
||||
deltaSigDigest = (string?)null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTestBundleWithSignature(string keyId)
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "Test",
|
||||
pairs = Array.Empty<object>()
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest));
|
||||
|
||||
var signature = new
|
||||
{
|
||||
signatureType = "cosign",
|
||||
keyId,
|
||||
placeholder = false
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json.sig"),
|
||||
JsonSerializer.Serialize(signature));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTrustedKeysFile(string[] keyIds)
|
||||
{
|
||||
var path = Path.Combine(_tempDir, $"trusted-keys-{Guid.NewGuid():N}.json");
|
||||
var keys = new { keyIds };
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(keys));
|
||||
return path;
|
||||
}
|
||||
|
||||
private string CreateTarball(string sourceDir)
|
||||
{
|
||||
var tarPath = Path.Combine(_tempDir, $"{Guid.NewGuid():N}.tar.gz");
|
||||
|
||||
var tempTar = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
using (var tarStream = File.Create(tempTar))
|
||||
{
|
||||
System.Formats.Tar.TarFile.CreateFromDirectory(
|
||||
sourceDir,
|
||||
tarStream,
|
||||
includeBaseDirectory: false);
|
||||
}
|
||||
|
||||
using var inputStream = File.OpenRead(tempTar);
|
||||
using var outputStream = File.Create(tarPath);
|
||||
using var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal);
|
||||
inputStream.CopyTo(gzipStream);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempTar))
|
||||
{
|
||||
File.Delete(tempTar);
|
||||
}
|
||||
|
||||
Directory.Delete(sourceDir, recursive: true);
|
||||
}
|
||||
|
||||
return tarPath;
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
StellaOps.Verifier.Tests.csproj
|
||||
Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
Task: GCB-003 - Implement standalone offline verifier
|
||||
Description: Unit tests for standalone bundle verifier
|
||||
-->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Verifier.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user