sprints and audit work
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
using StellaOps.Unknowns.Core.Hints;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.Unknowns.Core.Tests.Hints;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for hint combination logic and confidence aggregation.
|
||||
/// </summary>
|
||||
public sealed class HintCombinationTests
|
||||
{
|
||||
private readonly ProvenanceHintBuilder _builder = new(TimeProvider.System);
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_EmptyList_ReturnsZeroConfidence()
|
||||
{
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints([]);
|
||||
|
||||
// Assert
|
||||
hypothesis.Should().Be("No provenance hints available");
|
||||
confidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_SingleHighConfidenceHint_ReturnsHypothesisAndConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("openssl", 0.95)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
hypothesis.Should().Contain("openssl");
|
||||
confidence.Should().Be(0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_MultipleAgreeingHints_BoostsConfidence()
|
||||
{
|
||||
// Arrange - all hints point to same package
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("openssl", 0.85),
|
||||
CreateImportHint("openssl", 0.80),
|
||||
CreateVersionHint("openssl", 0.70)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().BeGreaterThan(0.85); // Boosted from multiple agreeing hints
|
||||
hypothesis.Should().Contain("confirmed by");
|
||||
hypothesis.Should().Contain("3 evidence sources");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_MultipleDisagreeingHints_UsesBestSingleHint()
|
||||
{
|
||||
// Arrange - hints point to different packages
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("openssl", 0.95),
|
||||
CreateImportHint("curl", 0.80),
|
||||
CreateVersionHint("wget", 0.70)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().Be(0.95); // Highest single hint
|
||||
hypothesis.Should().Contain("openssl"); // Best match
|
||||
hypothesis.Should().NotContain("confirmed by"); // No agreement
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_TwoAgreeingHighConfidence_CombinesConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("curl", 0.90),
|
||||
CreateVersionHint("curl", 0.75)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().BeGreaterThan(0.90);
|
||||
confidence.Should().BeLessThan(1.0); // Capped at 0.99
|
||||
hypothesis.Should().Contain("confirmed by");
|
||||
hypothesis.Should().Contain("2 evidence sources");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_OneLowConfidenceOneHigh_UsesHighConfidenceOnly()
|
||||
{
|
||||
// Arrange
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("openssl", 0.95),
|
||||
CreateVersionHint("openssl", 0.25) // Below 0.5 threshold
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().Be(0.95); // Only high-confidence hint used
|
||||
hypothesis.Should().NotContain("confirmed by"); // Low confidence ignored
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_ThreeAgreeingHints_DoesNotExceed099()
|
||||
{
|
||||
// Arrange - many agreeing high-confidence hints
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("nginx", 0.95),
|
||||
CreateImportHint("nginx", 0.92),
|
||||
CreateVersionHint("nginx", 0.88),
|
||||
CreateCorpusHint("nginx", 0.85)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().BeLessThanOrEqualTo(0.99);
|
||||
hypothesis.Should().Contain("confirmed by");
|
||||
hypothesis.Should().Contain("4 evidence sources");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_MixedConfidencesSamePackage_CountsOnlyHighConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var hints = new[]
|
||||
{
|
||||
CreateBuildIdHint("bash", 0.90), // High
|
||||
CreateImportHint("bash", 0.60), // Medium
|
||||
CreateVersionHint("bash", 0.30) // Low (excluded)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
hypothesis.Should().Contain("confirmed by");
|
||||
hypothesis.Should().Contain("2 evidence sources"); // Only high+medium
|
||||
}
|
||||
|
||||
// Helper methods to create test hints
|
||||
|
||||
private ProvenanceHint CreateBuildIdHint(string package, double confidence)
|
||||
{
|
||||
var match = new BuildIdMatchResult
|
||||
{
|
||||
Package = package,
|
||||
Version = "1.0.0",
|
||||
Distro = "debian"
|
||||
};
|
||||
|
||||
return _builder.BuildFromBuildId("test-build-id", "sha1", match);
|
||||
}
|
||||
|
||||
private ProvenanceHint CreateImportHint(string package, double similarity)
|
||||
{
|
||||
var matches = new[]
|
||||
{
|
||||
new FingerprintMatch
|
||||
{
|
||||
Package = package,
|
||||
Version = "1.0.0",
|
||||
Similarity = similarity,
|
||||
Source = "test-corpus"
|
||||
}
|
||||
};
|
||||
|
||||
return _builder.BuildFromImportFingerprint("fp-test", new[] { "lib1.so" }, matches);
|
||||
}
|
||||
|
||||
private ProvenanceHint CreateVersionHint(string package, double confidence)
|
||||
{
|
||||
var versionStrings = new[]
|
||||
{
|
||||
new ExtractedVersionString
|
||||
{
|
||||
Value = $"{package} 1.0.0",
|
||||
Location = ".rodata",
|
||||
Confidence = confidence
|
||||
}
|
||||
};
|
||||
|
||||
return _builder.BuildFromVersionStrings(versionStrings);
|
||||
}
|
||||
|
||||
private ProvenanceHint CreateCorpusHint(string package, double similarity)
|
||||
{
|
||||
return _builder.BuildFromCorpusMatch(
|
||||
"test-corpus",
|
||||
$"{package}/1.0.0",
|
||||
"hash",
|
||||
similarity,
|
||||
null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using StellaOps.Unknowns.Core.Hints;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.Unknowns.Core.Tests.Hints;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProvenanceHintBuilder - all hint building scenarios.
|
||||
/// </summary>
|
||||
public sealed class ProvenanceHintBuilderTests
|
||||
{
|
||||
private readonly ProvenanceHintBuilder _builder = new(TimeProvider.System);
|
||||
|
||||
[Fact]
|
||||
public void BuildFromBuildId_WithMatch_CreatesVeryHighConfidenceHint()
|
||||
{
|
||||
// Arrange
|
||||
var match = new BuildIdMatchResult
|
||||
{
|
||||
Package = "openssl",
|
||||
Version = "1.1.1k",
|
||||
Distro = "debian",
|
||||
CatalogSource = "debian-security"
|
||||
};
|
||||
|
||||
// Act
|
||||
var hint = _builder.BuildFromBuildId("abc123", "sha1", match);
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.BuildIdMatch);
|
||||
hint.Confidence.Should().Be(0.95);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.VeryHigh);
|
||||
hint.Hypothesis.Should().Contain("openssl");
|
||||
hint.Hypothesis.Should().Contain("1.1.1k");
|
||||
hint.Hypothesis.Should().Contain("debian");
|
||||
hint.Evidence.BuildId.Should().NotBeNull();
|
||||
hint.Evidence.BuildId!.BuildId.Should().Be("abc123");
|
||||
hint.Evidence.BuildId.MatchedPackage.Should().Be("openssl");
|
||||
hint.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
hint.SuggestedActions[0].Action.Should().Be("verify_build_id");
|
||||
hint.HintId.Should().StartWith("hint:sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromBuildId_WithoutMatch_CreatesLowConfidenceHint()
|
||||
{
|
||||
// Act
|
||||
var hint = _builder.BuildFromBuildId("unknown123", "sha1", null);
|
||||
|
||||
// Assert
|
||||
hint.Confidence.Should().Be(0.2);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.VeryLow);
|
||||
hint.Hypothesis.Should().Contain("no catalog match");
|
||||
hint.Evidence.BuildId!.MatchedPackage.Should().BeNull();
|
||||
hint.SuggestedActions.Should().Contain(a => a.Action == "expand_catalog");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromImportFingerprint_WithMatch_IncludesMatchedPackage()
|
||||
{
|
||||
// Arrange
|
||||
var matches = new[]
|
||||
{
|
||||
new FingerprintMatch
|
||||
{
|
||||
Package = "libc6",
|
||||
Version = "2.31",
|
||||
Similarity = 0.92,
|
||||
Source = "debian-corpus"
|
||||
}
|
||||
};
|
||||
|
||||
var imports = new[] { "libc.so.6", "libpthread.so.0" };
|
||||
|
||||
// Act
|
||||
var hint = _builder.BuildFromImportFingerprint("fp-abc", imports, matches);
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.ImportTableFingerprint);
|
||||
hint.Confidence.Should().Be(0.92);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.VeryHigh);
|
||||
hint.Hypothesis.Should().Contain("libc6");
|
||||
hint.Hypothesis.Should().Contain("2.31");
|
||||
hint.Evidence.ImportFingerprint.Should().NotBeNull();
|
||||
hint.Evidence.ImportFingerprint!.ImportedLibraries.Should().HaveCount(2);
|
||||
hint.Evidence.ImportFingerprint.MatchedFingerprints.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromImportFingerprint_WithoutMatch_CreatesMediumConfidenceHint()
|
||||
{
|
||||
// Arrange
|
||||
var imports = new[] { "unknown.so.1" };
|
||||
|
||||
// Act
|
||||
var hint = _builder.BuildFromImportFingerprint("fp-xyz", imports, null);
|
||||
|
||||
// Assert
|
||||
hint.Confidence.Should().Be(0.3);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.Low);
|
||||
hint.Hypothesis.Should().Contain("fp-xyz");
|
||||
hint.Evidence.ImportFingerprint!.MatchedFingerprints.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromSectionLayout_WithMatch_IncludesSimilarity()
|
||||
{
|
||||
// Arrange
|
||||
var sections = new[]
|
||||
{
|
||||
new SectionInfo { Name = ".text", Type = "PROGBITS", Size = 0x1000 },
|
||||
new SectionInfo { Name = ".data", Type = "PROGBITS", Size = 0x200 }
|
||||
};
|
||||
|
||||
var matches = new[]
|
||||
{
|
||||
new LayoutMatch { Package = "bash", Similarity = 0.88 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var hint = _builder.BuildFromSectionLayout(sections, matches);
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.SectionLayout);
|
||||
hint.Confidence.Should().Be(0.88);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.VeryHigh);
|
||||
hint.Hypothesis.Should().Contain("bash");
|
||||
hint.Evidence.SectionLayout.Should().NotBeNull();
|
||||
hint.Evidence.SectionLayout!.Sections.Should().HaveCount(2);
|
||||
hint.Evidence.SectionLayout.LayoutHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromDistroPattern_IncludesDistroAndRelease()
|
||||
{
|
||||
// Act
|
||||
var hint = _builder.BuildFromDistroPattern("debian", "bullseye", "rpath", "/usr/lib/x86_64-linux-gnu");
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.DistroPattern);
|
||||
hint.Confidence.Should().Be(0.7);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.High);
|
||||
hint.Hypothesis.Should().Contain("debian");
|
||||
hint.Hypothesis.Should().Contain("bullseye");
|
||||
hint.Evidence.DistroPattern.Should().NotBeNull();
|
||||
hint.Evidence.DistroPattern!.Distro.Should().Be("debian");
|
||||
hint.Evidence.DistroPattern.Release.Should().Be("bullseye");
|
||||
hint.SuggestedActions[0].Link.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromVersionStrings_WithMultipleStrings_SelectsBestGuess()
|
||||
{
|
||||
// Arrange
|
||||
var versionStrings = new[]
|
||||
{
|
||||
new ExtractedVersionString { Value = "1.2.3", Location = ".rodata", Confidence = 0.8 },
|
||||
new ExtractedVersionString { Value = "1.2", Location = ".comment", Confidence = 0.5 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var hint = _builder.BuildFromVersionStrings(versionStrings);
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.VersionString);
|
||||
hint.Confidence.Should().Be(0.8);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.High);
|
||||
hint.Hypothesis.Should().Contain("1.2.3");
|
||||
hint.Evidence.VersionString.Should().NotBeNull();
|
||||
hint.Evidence.VersionString!.BestGuess.Should().Be("1.2.3");
|
||||
hint.Evidence.VersionString.VersionStrings.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromCorpusMatch_HighSimilarity_CreatesVeryHighConfidence()
|
||||
{
|
||||
// Act
|
||||
var hint = _builder.BuildFromCorpusMatch(
|
||||
"debian-packages",
|
||||
"curl/7.68.0",
|
||||
"hash",
|
||||
0.95,
|
||||
new Dictionary<string, string> { ["arch"] = "amd64" });
|
||||
|
||||
// Assert
|
||||
hint.Type.Should().Be(ProvenanceHintType.CorpusMatch);
|
||||
hint.Confidence.Should().Be(0.95);
|
||||
hint.ConfidenceLevel.Should().Be(HintConfidence.VeryHigh);
|
||||
hint.Hypothesis.Should().Contain("High confidence match");
|
||||
hint.Hypothesis.Should().Contain("curl/7.68.0");
|
||||
hint.Evidence.CorpusMatch.Should().NotBeNull();
|
||||
hint.Evidence.CorpusMatch!.CorpusName.Should().Be("debian-packages");
|
||||
hint.Evidence.CorpusMatch.Metadata.Should().ContainKey("arch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_NoHints_ReturnsZeroConfidence()
|
||||
{
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints([]);
|
||||
|
||||
// Assert
|
||||
hypothesis.Should().Contain("No provenance hints");
|
||||
confidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_SingleHint_ReturnsBestHypothesis()
|
||||
{
|
||||
// Arrange
|
||||
var hints = new[]
|
||||
{
|
||||
_builder.BuildFromBuildId("abc123", "sha1", new BuildIdMatchResult
|
||||
{
|
||||
Package = "openssl",
|
||||
Version = "1.1.1k",
|
||||
Distro = "debian"
|
||||
})
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
hypothesis.Should().Contain("openssl");
|
||||
confidence.Should().Be(0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombineHints_MultipleAgreeingHints_BoostsConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var buildIdMatch = new BuildIdMatchResult
|
||||
{
|
||||
Package = "openssl",
|
||||
Version = "1.1.1k",
|
||||
Distro = "debian"
|
||||
};
|
||||
|
||||
var hints = new[]
|
||||
{
|
||||
_builder.BuildFromBuildId("abc123", "sha1", buildIdMatch),
|
||||
_builder.BuildFromDistroPattern("debian", "bullseye", "rpath", "/usr/lib"),
|
||||
_builder.BuildFromVersionStrings(new[]
|
||||
{
|
||||
new ExtractedVersionString { Value = "1.1.1k", Location = ".rodata", Confidence = 0.7 }
|
||||
})
|
||||
};
|
||||
|
||||
// Act
|
||||
var (hypothesis, confidence) = _builder.CombineHints(hints);
|
||||
|
||||
// Assert
|
||||
confidence.Should().BeGreaterThan(0.95); // Boosted from multiple agreeing hints
|
||||
hypothesis.Should().Contain("confirmed by");
|
||||
hypothesis.Should().Contain("evidence sources");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HintId_IsContentAddressed_DeterministicForSameInput()
|
||||
{
|
||||
// Arrange & Act
|
||||
var hint1 = _builder.BuildFromBuildId("abc123", "sha1", null);
|
||||
var hint2 = _builder.BuildFromBuildId("abc123", "sha1", null);
|
||||
|
||||
// Assert
|
||||
hint1.HintId.Should().Be(hint2.HintId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HintId_IsDifferent_ForDifferentInput()
|
||||
{
|
||||
// Arrange & Act
|
||||
var hint1 = _builder.BuildFromBuildId("abc123", "sha1", null);
|
||||
var hint2 = _builder.BuildFromBuildId("xyz789", "sha1", null);
|
||||
|
||||
// Assert
|
||||
hint1.HintId.Should().NotBe(hint2.HintId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Unknowns.Core.Hints;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Unknowns.Core.Tests.Hints;
|
||||
|
||||
/// <summary>
|
||||
/// Golden fixture tests for provenance hint serialization.
|
||||
/// Ensures stable JSON output for cross-service compatibility.
|
||||
/// </summary>
|
||||
public sealed class ProvenanceHintSerializationTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ProvenanceHintBuilder _builder = new(new FrozenTimeProvider());
|
||||
|
||||
[Fact]
|
||||
public void BuildIdHint_Serialization_ProducesExpectedJson()
|
||||
{
|
||||
// Arrange
|
||||
var match = new BuildIdMatchResult
|
||||
{
|
||||
Package = "openssl",
|
||||
Version = "1.1.1k",
|
||||
Distro = "debian",
|
||||
CatalogSource = "debian-security"
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromBuildId("abc123def456", "sha1", match);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert - round-trip
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Type.Should().Be(ProvenanceHintType.BuildIdMatch);
|
||||
deserialized.Confidence.Should().Be(0.95);
|
||||
deserialized.ConfidenceLevel.Should().Be(HintConfidence.VeryHigh);
|
||||
deserialized.Evidence.BuildId.Should().NotBeNull();
|
||||
deserialized.Evidence.BuildId!.BuildId.Should().Be("abc123def456");
|
||||
deserialized.Evidence.BuildId.MatchedPackage.Should().Be("openssl");
|
||||
|
||||
// Assert - stable keys
|
||||
json.Should().Contain("\"hint_id\":");
|
||||
json.Should().Contain("\"type\":");
|
||||
json.Should().Contain("\"confidence\":");
|
||||
json.Should().Contain("\"confidence_level\":");
|
||||
json.Should().Contain("\"hypothesis\":");
|
||||
json.Should().Contain("\"evidence\":");
|
||||
json.Should().Contain("\"suggested_actions\":");
|
||||
json.Should().Contain("\"generated_at\":");
|
||||
json.Should().Contain("\"source\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportFingerprintHint_Serialization_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var matches = new[]
|
||||
{
|
||||
new FingerprintMatch
|
||||
{
|
||||
Package = "libc6",
|
||||
Version = "2.31-13",
|
||||
Similarity = 0.92,
|
||||
Source = "debian-corpus"
|
||||
}
|
||||
};
|
||||
|
||||
var imports = new[] { "libc.so.6", "libpthread.so.0", "libdl.so.2" };
|
||||
var hint = _builder.BuildFromImportFingerprint("fp-abc123", imports, matches);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Evidence.ImportFingerprint.Should().NotBeNull();
|
||||
deserialized.Evidence.ImportFingerprint!.Fingerprint.Should().Be("fp-abc123");
|
||||
deserialized.Evidence.ImportFingerprint.ImportedLibraries.Should().HaveCount(3);
|
||||
deserialized.Evidence.ImportFingerprint.MatchedFingerprints.Should().HaveCount(1);
|
||||
deserialized.Evidence.ImportFingerprint.MatchedFingerprints![0].Package.Should().Be("libc6");
|
||||
deserialized.Evidence.ImportFingerprint.MatchedFingerprints[0].Similarity.Should().Be(0.92);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SectionLayoutHint_Serialization_PreservesAllSections()
|
||||
{
|
||||
// Arrange
|
||||
var sections = new[]
|
||||
{
|
||||
new SectionInfo { Name = ".text", Type = "PROGBITS", Size = 0x1000, Flags = "AX" },
|
||||
new SectionInfo { Name = ".data", Type = "PROGBITS", Size = 0x200, Flags = "WA" },
|
||||
new SectionInfo { Name = ".bss", Type = "NOBITS", Size = 0x100, Flags = "WA" }
|
||||
};
|
||||
|
||||
var matches = new[]
|
||||
{
|
||||
new LayoutMatch { Package = "bash", Similarity = 0.88 }
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromSectionLayout(sections, matches);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Evidence.SectionLayout.Should().NotBeNull();
|
||||
deserialized.Evidence.SectionLayout!.Sections.Should().HaveCount(3);
|
||||
deserialized.Evidence.SectionLayout.Sections[0].Name.Should().Be(".text");
|
||||
deserialized.Evidence.SectionLayout.Sections[0].Size.Should().Be(0x1000);
|
||||
deserialized.Evidence.SectionLayout.LayoutHash.Should().NotBeNullOrEmpty();
|
||||
deserialized.Evidence.SectionLayout.MatchedLayouts.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistroPatternHint_Serialization_IncludesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var hint = _builder.BuildFromDistroPattern(
|
||||
"debian",
|
||||
"bullseye",
|
||||
"rpath",
|
||||
"/usr/lib/x86_64-linux-gnu");
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Evidence.DistroPattern.Should().NotBeNull();
|
||||
deserialized.Evidence.DistroPattern!.Distro.Should().Be("debian");
|
||||
deserialized.Evidence.DistroPattern.Release.Should().Be("bullseye");
|
||||
deserialized.Evidence.DistroPattern.PatternType.Should().Be("rpath");
|
||||
deserialized.Evidence.DistroPattern.MatchedPattern.Should().Be("/usr/lib/x86_64-linux-gnu");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionStringHint_Serialization_PreservesAllVersionStrings()
|
||||
{
|
||||
// Arrange
|
||||
var versionStrings = new[]
|
||||
{
|
||||
new ExtractedVersionString { Value = "1.2.3", Location = ".rodata", Confidence = 0.8 },
|
||||
new ExtractedVersionString { Value = "1.2", Location = ".comment", Confidence = 0.5 },
|
||||
new ExtractedVersionString { Value = "v1.2.3-stable", Location = ".data", Confidence = 0.7 }
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromVersionStrings(versionStrings);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Evidence.VersionString.Should().NotBeNull();
|
||||
deserialized.Evidence.VersionString!.VersionStrings.Should().HaveCount(3);
|
||||
deserialized.Evidence.VersionString.BestGuess.Should().Be("1.2.3"); // Highest confidence
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorpusMatchHint_Serialization_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["arch"] = "amd64",
|
||||
["build_date"] = "2024-01-15",
|
||||
["compiler"] = "gcc-11.2.0"
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromCorpusMatch(
|
||||
"debian-packages",
|
||||
"curl/7.68.0",
|
||||
"hash",
|
||||
0.95,
|
||||
metadata);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Evidence.CorpusMatch.Should().NotBeNull();
|
||||
deserialized.Evidence.CorpusMatch!.CorpusName.Should().Be("debian-packages");
|
||||
deserialized.Evidence.CorpusMatch.MatchedEntry.Should().Be("curl/7.68.0");
|
||||
deserialized.Evidence.CorpusMatch.Similarity.Should().Be(0.95);
|
||||
deserialized.Evidence.CorpusMatch.Metadata.Should().NotBeNull();
|
||||
deserialized.Evidence.CorpusMatch.Metadata!["arch"].Should().Be("amd64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuggestedActions_Serialization_PreservesOrder()
|
||||
{
|
||||
// Arrange
|
||||
var match = new BuildIdMatchResult
|
||||
{
|
||||
Package = "test",
|
||||
Version = "1.0",
|
||||
Distro = "debian"
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromBuildId("test-id", "sha1", match);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ProvenanceHint>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
deserialized.SuggestedActions[0].Action.Should().NotBeNullOrEmpty();
|
||||
deserialized.SuggestedActions[0].Priority.Should().BeGreaterThan(0);
|
||||
deserialized.SuggestedActions[0].Effort.Should().NotBeNullOrEmpty();
|
||||
deserialized.SuggestedActions[0].Description.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HintId_IsDeterministic_ForSameInput()
|
||||
{
|
||||
// Arrange & Act
|
||||
var hint1 = _builder.BuildFromBuildId("same-id", "sha1", null);
|
||||
var hint2 = _builder.BuildFromBuildId("same-id", "sha1", null);
|
||||
|
||||
var json1 = JsonSerializer.Serialize(hint1, JsonOptions);
|
||||
var json2 = JsonSerializer.Serialize(hint2, JsonOptions);
|
||||
|
||||
// Assert
|
||||
hint1.HintId.Should().Be(hint2.HintId);
|
||||
json1.Should().Contain(hint1.HintId);
|
||||
json2.Should().Contain(hint2.HintId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedAt_UsesFixedTimestamp_InTests()
|
||||
{
|
||||
// Arrange
|
||||
var hint = _builder.BuildFromBuildId("test", "sha1", null);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
|
||||
// Assert
|
||||
hint.GeneratedAt.Should().Be(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
json.Should().Contain("\"generated_at\":\"2025-01-01T00:00:00+00:00\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteHint_JsonOutput_IsValid()
|
||||
{
|
||||
// Arrange
|
||||
var match = new BuildIdMatchResult
|
||||
{
|
||||
Package = "nginx",
|
||||
Version = "1.18.0-6",
|
||||
Distro = "debian",
|
||||
CatalogSource = "debian-security",
|
||||
AdvisoryLink = "https://security.debian.org/nginx"
|
||||
};
|
||||
|
||||
var hint = _builder.BuildFromBuildId("deadbeef0123456789abcdef", "sha256", match);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
|
||||
// Assert - JSON is parseable
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
parsed.RootElement.GetProperty("hint_id").GetString().Should().StartWith("hint:sha256:");
|
||||
parsed.RootElement.GetProperty("type").GetString().Should().NotBeNullOrEmpty();
|
||||
parsed.RootElement.GetProperty("confidence").GetDouble().Should().BeInRange(0, 1);
|
||||
parsed.RootElement.GetProperty("evidence").GetProperty("build_id").GetProperty("catalog_source")
|
||||
.GetString().Should().Be("debian-security");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frozen time provider for deterministic test timestamps.
|
||||
/// </summary>
|
||||
private sealed class FrozenTimeProvider : TimeProvider
|
||||
{
|
||||
private static readonly DateTimeOffset FrozenTime = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => FrozenTime;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user