audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Tools.GoldenPairs.Models;
|
||||
using StellaOps.Tools.GoldenPairs.Serialization;
|
||||
using StellaOps.Tools.GoldenPairs.Services;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs.Tests;
|
||||
|
||||
public sealed class DiffPipelineServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DiffAsync_ModifiedText_ReturnsPatched()
|
||||
{
|
||||
var metadata = TestData.CreateMetadata();
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var layout = new GoldenPairLayout(temp.Path);
|
||||
Directory.CreateDirectory(layout.GetOriginalDirectory(metadata.Cve));
|
||||
Directory.CreateDirectory(layout.GetPatchedDirectory(metadata.Cve));
|
||||
|
||||
var originalSections = TestData.CreateSectionHashSet(
|
||||
filePath: "original",
|
||||
fileHash: metadata.Original.Sha256,
|
||||
new SectionHashEntry { Name = ".text", Sha256 = "1111", Size = 100 },
|
||||
new SectionHashEntry { Name = ".rodata", Sha256 = "2222", Size = 50 });
|
||||
|
||||
var patchedSections = TestData.CreateSectionHashSet(
|
||||
filePath: "patched",
|
||||
fileHash: metadata.Patched.Sha256,
|
||||
new SectionHashEntry { Name = ".text", Sha256 = "3333", Size = 110 },
|
||||
new SectionHashEntry { Name = ".rodata", Sha256 = "2222", Size = 50 });
|
||||
|
||||
await File.WriteAllTextAsync(layout.GetOriginalSectionHashPath(metadata), GoldenPairsJsonSerializer.Serialize(originalSections));
|
||||
await File.WriteAllTextAsync(layout.GetPatchedSectionHashPath(metadata), GoldenPairsJsonSerializer.Serialize(patchedSections));
|
||||
|
||||
var diff = new DiffPipelineService(layout, new FileSectionHashProvider(), new FixedTimeProvider());
|
||||
var report = await diff.DiffAsync(metadata);
|
||||
|
||||
report.Verdict.Should().Be(GoldenDiffVerdict.Patched);
|
||||
report.MatchesExpected.Should().BeTrue();
|
||||
report.Sections.Should().HaveCount(2);
|
||||
report.Sections.First(section => section.Name == ".text").Status.Should().Be(SectionComparisonStatus.Modified);
|
||||
report.Sections.First(section => section.Name == ".rodata").Status.Should().Be(SectionComparisonStatus.Identical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WrongVerdict_Fails()
|
||||
{
|
||||
var report = new GoldenDiffReport
|
||||
{
|
||||
Cve = "CVE-2022-0847",
|
||||
Original = new ArtifactHashInfo { Sha256 = "aa" },
|
||||
Patched = new ArtifactHashInfo { Sha256 = "bb" },
|
||||
Sections = ImmutableArray<SectionComparison>.Empty,
|
||||
Verdict = GoldenDiffVerdict.Patched,
|
||||
Confidence = 0.9,
|
||||
MatchesExpected = true,
|
||||
Discrepancies = ImmutableArray<string>.Empty,
|
||||
AnalyzedAt = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero),
|
||||
ToolVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var expected = new ExpectedDiff
|
||||
{
|
||||
Verdict = GoldenDiffVerdict.Vanilla,
|
||||
ConfidenceMin = 0.1
|
||||
};
|
||||
|
||||
var validation = new DiffPipelineService(
|
||||
new GoldenPairLayout(Path.GetTempPath()),
|
||||
new FileSectionHashProvider(),
|
||||
new FixedTimeProvider())
|
||||
.Validate(report, expected);
|
||||
|
||||
validation.IsValid.Should().BeFalse();
|
||||
validation.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixed = new(2026, 1, 13, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixed;
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"golden-diff-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Tools.GoldenPairs.Schema;
|
||||
using StellaOps.Tools.GoldenPairs.Serialization;
|
||||
using StellaOps.Tools.GoldenPairs.Services;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs.Tests;
|
||||
|
||||
public sealed class GoldenPairLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_ReturnsMetadataAndNormalizes()
|
||||
{
|
||||
var metadata = TestData.CreateMetadata();
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var datasetRoot = Path.Combine(temp.Path, "datasets");
|
||||
var pairDir = Path.Combine(datasetRoot, metadata.Cve);
|
||||
Directory.CreateDirectory(pairDir);
|
||||
|
||||
var metadataPath = Path.Combine(pairDir, "metadata.json");
|
||||
await File.WriteAllTextAsync(metadataPath, GoldenPairsJsonSerializer.Serialize(metadata));
|
||||
|
||||
var loader = CreateLoader(datasetRoot);
|
||||
var result = await loader.LoadAsync(metadata.Cve);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Metadata.Should().NotBeNull();
|
||||
result.Metadata!.Cve.Should().Be("CVE-2022-0847");
|
||||
result.Metadata!.Advisories[0].Source.Should().Be("nvd");
|
||||
result.Metadata!.Advisories[1].Source.Should().Be("ubuntu");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_MissingMetadata_ReturnsError()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var loader = CreateLoader(temp.Path);
|
||||
|
||||
var result = await loader.LoadAsync("CVE-2099-9999");
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static GoldenPairLoader CreateLoader(string datasetRoot)
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var schemaProvider = new GoldenPairsSchemaProvider(
|
||||
Path.Combine(repoRoot, "docs", "schemas", "golden-pair-v1.schema.json"),
|
||||
Path.Combine(repoRoot, "docs", "schemas", "golden-pairs-index.schema.json"));
|
||||
|
||||
var layout = new GoldenPairLayout(datasetRoot);
|
||||
return new GoldenPairLoader(schemaProvider, layout);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
var solutionPath = Path.Combine(current.FullName, "src", "StellaOps.sln");
|
||||
if (File.Exists(solutionPath))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Repository root not found.");
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"golden-pairs-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Json.Schema;
|
||||
using StellaOps.Tools.GoldenPairs.Schema;
|
||||
using StellaOps.Tools.GoldenPairs.Serialization;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs.Tests;
|
||||
|
||||
public sealed class GoldenPairSchemaTests
|
||||
{
|
||||
[Fact]
|
||||
public void MetadataSchema_ValidatesSample()
|
||||
{
|
||||
var schemaProvider = CreateSchemaProvider();
|
||||
var metadata = TestData.CreateMetadata();
|
||||
var json = GoldenPairsJsonSerializer.Serialize(metadata);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
|
||||
var result = schemaProvider.MetadataSchema.Evaluate(document.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetadataSchema_RejectsMissingCve()
|
||||
{
|
||||
using var document = JsonDocument.Parse("{\"name\":\"Dirty Pipe\"}");
|
||||
var schemaProvider = CreateSchemaProvider();
|
||||
|
||||
var result = schemaProvider.MetadataSchema.Evaluate(document.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexSchema_ValidatesSample()
|
||||
{
|
||||
var schemaProvider = CreateSchemaProvider();
|
||||
var index = TestData.CreateIndex();
|
||||
var json = GoldenPairsJsonSerializer.Serialize(index);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
|
||||
var result = schemaProvider.IndexSchema.Evaluate(document.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
private static GoldenPairsSchemaProvider CreateSchemaProvider()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var metadataSchemaPath = Path.Combine(repoRoot, "docs", "schemas", "golden-pair-v1.schema.json");
|
||||
var indexSchemaPath = Path.Combine(repoRoot, "docs", "schemas", "golden-pairs-index.schema.json");
|
||||
return new GoldenPairsSchemaProvider(metadataSchemaPath, indexSchemaPath);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
var solutionPath = Path.Combine(current.FullName, "src", "StellaOps.sln");
|
||||
if (File.Exists(solutionPath))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Repository root not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Tools.GoldenPairs.Models;
|
||||
using StellaOps.Tools.GoldenPairs.Services;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs.Tests;
|
||||
|
||||
public sealed class PackageMirrorServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FetchAsync_FileSource_CopiesAndVerifies()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var sourcePath = Path.Combine(temp.Path, "source.bin");
|
||||
await File.WriteAllTextAsync(sourcePath, "payload");
|
||||
|
||||
var sha256 = ComputeSha256(sourcePath);
|
||||
var artifact = new BinaryArtifact
|
||||
{
|
||||
Package = "test",
|
||||
Version = "1.0",
|
||||
Distro = "local",
|
||||
Source = new Uri(sourcePath).AbsoluteUri,
|
||||
Sha256 = sha256,
|
||||
HasDebugSymbols = false
|
||||
};
|
||||
|
||||
var mirror = new AptPackageMirrorService(new NoHttpClientFactory(), NullLogger<AptPackageMirrorService>.Instance);
|
||||
var result = await mirror.FetchAsync(artifact, temp.Path);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.HashMatches.Should().BeTrue();
|
||||
File.Exists(result.LocalPath).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_HashMismatch_Fails()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var sourcePath = Path.Combine(temp.Path, "source.bin");
|
||||
await File.WriteAllTextAsync(sourcePath, "payload");
|
||||
|
||||
var artifact = new BinaryArtifact
|
||||
{
|
||||
Package = "test",
|
||||
Version = "1.0",
|
||||
Distro = "local",
|
||||
Source = new Uri(sourcePath).AbsoluteUri,
|
||||
Sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
HasDebugSymbols = false
|
||||
};
|
||||
|
||||
var mirror = new AptPackageMirrorService(new NoHttpClientFactory(), NullLogger<AptPackageMirrorService>.Instance);
|
||||
var result = await mirror.FetchAsync(artifact, temp.Path);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.HashMatches.Should().BeFalse();
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string path)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var bytes = sha.ComputeHash(File.ReadAllBytes(path));
|
||||
var builder = new StringBuilder(bytes.Length * 2);
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
_ = builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private sealed class NoHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name)
|
||||
=> throw new InvalidOperationException("HttpClient should not be used for file sources.");
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"golden-mirror-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\GoldenPairs\StellaOps.Tools.GoldenPairs.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
111
src/Tools/__Tests/StellaOps.Tools.GoldenPairs.Tests/TestData.cs
Normal file
111
src/Tools/__Tests/StellaOps.Tools.GoldenPairs.Tests/TestData.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Tools.GoldenPairs.Models;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs.Tests;
|
||||
|
||||
internal static class TestData
|
||||
{
|
||||
public static GoldenPairMetadata CreateMetadata()
|
||||
=> new()
|
||||
{
|
||||
Cve = "CVE-2022-0847",
|
||||
Name = "Dirty Pipe",
|
||||
Description = "Test description.",
|
||||
Severity = SeverityLevel.High,
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Name = "vmlinux",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64",
|
||||
Os = "linux"
|
||||
},
|
||||
Original = new BinaryArtifact
|
||||
{
|
||||
Package = "linux-image-5.16.11-generic",
|
||||
Version = "5.16.11",
|
||||
Distro = "Ubuntu 22.04",
|
||||
Source = "file:///tmp/original.bin",
|
||||
Sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
BuildId = "deadbeef",
|
||||
HasDebugSymbols = false,
|
||||
PathInPackage = "/boot/vmlinux"
|
||||
},
|
||||
Patched = new BinaryArtifact
|
||||
{
|
||||
Package = "linux-image-5.16.12-generic",
|
||||
Version = "5.16.12",
|
||||
Distro = "Ubuntu 22.04",
|
||||
Source = "file:///tmp/patched.bin",
|
||||
Sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
BuildId = "beefdead",
|
||||
HasDebugSymbols = false,
|
||||
PathInPackage = "/boot/vmlinux"
|
||||
},
|
||||
Patch = new PatchInfo
|
||||
{
|
||||
Commit = "9d2231c5d74e13b2a0546fee6737ee4446017903",
|
||||
Upstream = "https://example.invalid/commit",
|
||||
FunctionsChanged = ImmutableArray.Create("push_pipe", "copy_page_to_iter_pipe"),
|
||||
FilesChanged = ImmutableArray.Create("lib/iov_iter.c", "fs/pipe.c"),
|
||||
Summary = "Fix PIPE_BUF_FLAG_CAN_MERGE handling"
|
||||
},
|
||||
Advisories = ImmutableArray.Create(
|
||||
new AdvisoryRef
|
||||
{
|
||||
Source = "ubuntu",
|
||||
Id = "USN-5317-1",
|
||||
Url = "https://ubuntu.com/security/notices/USN-5317-1"
|
||||
},
|
||||
new AdvisoryRef
|
||||
{
|
||||
Source = "nvd",
|
||||
Id = "CVE-2022-0847",
|
||||
Url = "https://nvd.nist.gov/vuln/detail/CVE-2022-0847"
|
||||
}),
|
||||
ExpectedDiff = new ExpectedDiff
|
||||
{
|
||||
SectionsChanged = ImmutableArray.Create(".text"),
|
||||
SectionsIdentical = ImmutableArray.Create(".rodata", ".data"),
|
||||
Verdict = GoldenDiffVerdict.Patched,
|
||||
ConfidenceMin = 0.9
|
||||
},
|
||||
CreatedAt = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero),
|
||||
CreatedBy = "StellaOps Golden Pairs Tool v1.0.0"
|
||||
};
|
||||
|
||||
public static GoldenPairsIndex CreateIndex()
|
||||
=> new()
|
||||
{
|
||||
Version = "1.0.0",
|
||||
GeneratedAt = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero),
|
||||
Pairs = ImmutableArray.Create(
|
||||
new GoldenPairSummary
|
||||
{
|
||||
Cve = "CVE-2022-0847",
|
||||
Name = "Dirty Pipe",
|
||||
Severity = SeverityLevel.High,
|
||||
Format = BinaryFormat.Elf,
|
||||
Status = GoldenPairStatus.Validated,
|
||||
LastValidated = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero),
|
||||
Path = "CVE-2022-0847"
|
||||
}),
|
||||
Summary = new GoldenPairsIndexSummary
|
||||
{
|
||||
Total = 1,
|
||||
Validated = 1,
|
||||
Failed = 0,
|
||||
Pending = 0
|
||||
}
|
||||
};
|
||||
|
||||
public static SectionHashSet CreateSectionHashSet(string filePath, string fileHash, params SectionHashEntry[] sections)
|
||||
=> new()
|
||||
{
|
||||
FilePath = filePath,
|
||||
FileHash = fileHash,
|
||||
BuildId = "deadbeef",
|
||||
Sections = sections.ToImmutableArray(),
|
||||
ExtractedAt = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero),
|
||||
ExtractorVersion = "1.0.0"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user