audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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.");
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View 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"
};
}