audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using StellaOps.Cli.Commands.Scan;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Scanner.Analyzers.Native;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class BinaryDiffServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_InvalidReference_ThrowsBinaryDiffException()
|
||||
{
|
||||
var service = CreateService(new TestOciRegistryClient(), new TestElfSectionHashExtractor(), TimeProvider.System);
|
||||
|
||||
var request = new BinaryDiffRequest
|
||||
{
|
||||
BaseImageRef = "",
|
||||
TargetImageRef = "registry.example.com/app:2",
|
||||
Mode = BinaryDiffMode.Elf
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BinaryDiffException>(() =>
|
||||
service.ComputeDiffAsync(request, null, CancellationToken.None));
|
||||
|
||||
Assert.Equal(BinaryDiffErrorCode.InvalidReference, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_ExcludesUnchanged_WhenIncludeUnchangedFalse()
|
||||
{
|
||||
var baseRef = "registry.example.com/app:1";
|
||||
var targetRef = "registry.example.com/app:2";
|
||||
var baseLayer = CreateLayer(
|
||||
("usr/bin/alpha", CreateElfBytes('a')),
|
||||
("usr/bin/beta", CreateElfBytes('a')));
|
||||
var targetLayer = CreateLayer(
|
||||
("usr/bin/alpha", CreateElfBytes('a')),
|
||||
("usr/bin/beta", CreateElfBytes('b')),
|
||||
("usr/bin/gamma", CreateElfBytes('b')));
|
||||
|
||||
var registry = new TestOciRegistryClient();
|
||||
registry.AddImage(baseRef, "sha256:base", CreateManifest("sha256:layer-base", baseLayer.Length));
|
||||
registry.AddImage(targetRef, "sha256:target", CreateManifest("sha256:layer-target", targetLayer.Length));
|
||||
registry.AddBlob("sha256:layer-base", baseLayer);
|
||||
registry.AddBlob("sha256:layer-target", targetLayer);
|
||||
|
||||
var service = CreateService(registry, new TestElfSectionHashExtractor(), TimeProvider.System);
|
||||
|
||||
var result = await service.ComputeDiffAsync(
|
||||
new BinaryDiffRequest
|
||||
{
|
||||
BaseImageRef = baseRef,
|
||||
TargetImageRef = targetRef,
|
||||
Mode = BinaryDiffMode.Elf,
|
||||
IncludeUnchanged = false,
|
||||
Platform = new BinaryDiffPlatform { Os = "linux", Architecture = "amd64" }
|
||||
},
|
||||
null,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.Findings.Length);
|
||||
Assert.All(result.Findings, finding => Assert.NotEqual(ChangeType.Unchanged, finding.ChangeType));
|
||||
|
||||
var paths = result.Findings.Select(finding => finding.Path).ToArray();
|
||||
var sortedPaths = paths.OrderBy(path => path, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(sortedPaths, paths);
|
||||
|
||||
Assert.Equal(3, result.Summary.TotalBinaries);
|
||||
Assert.Equal(2, result.Summary.Modified);
|
||||
Assert.Equal(1, result.Summary.Unchanged);
|
||||
Assert.Equal(1, result.Summary.Added);
|
||||
Assert.Equal(0, result.Summary.Removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_UsesTimeProviderForMetadata()
|
||||
{
|
||||
var baseRef = "registry.example.com/app:1";
|
||||
var targetRef = "registry.example.com/app:2";
|
||||
var baseLayer = CreateLayer(("usr/bin/app", CreateElfBytes('a')));
|
||||
var targetLayer = CreateLayer(("usr/bin/app", CreateElfBytes('b')));
|
||||
|
||||
var registry = new TestOciRegistryClient();
|
||||
registry.AddImage(baseRef, "sha256:base", CreateManifest("sha256:layer-base", baseLayer.Length));
|
||||
registry.AddImage(targetRef, "sha256:target", CreateManifest("sha256:layer-target", targetLayer.Length));
|
||||
registry.AddBlob("sha256:layer-base", baseLayer);
|
||||
registry.AddBlob("sha256:layer-target", targetLayer);
|
||||
|
||||
var fixedTime = new DateTimeOffset(2026, 1, 13, 1, 0, 0, TimeSpan.Zero);
|
||||
var service = CreateService(registry, new TestElfSectionHashExtractor(), new FixedTimeProvider(fixedTime));
|
||||
|
||||
var result = await service.ComputeDiffAsync(
|
||||
new BinaryDiffRequest
|
||||
{
|
||||
BaseImageRef = baseRef,
|
||||
TargetImageRef = targetRef,
|
||||
Mode = BinaryDiffMode.Elf
|
||||
},
|
||||
null,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(fixedTime, result.Metadata.AnalysisTimestamp);
|
||||
}
|
||||
|
||||
private static BinaryDiffService CreateService(
|
||||
IOciRegistryClient registryClient,
|
||||
IElfSectionHashExtractor extractor,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "test" });
|
||||
return new BinaryDiffService(
|
||||
registryClient,
|
||||
extractor,
|
||||
options,
|
||||
timeProvider,
|
||||
NullLogger<BinaryDiffService>.Instance);
|
||||
}
|
||||
|
||||
private static OciManifest CreateManifest(string layerDigest, long size)
|
||||
{
|
||||
return new OciManifest
|
||||
{
|
||||
Layers = new List<OciDescriptor>
|
||||
{
|
||||
new OciDescriptor
|
||||
{
|
||||
Digest = layerDigest,
|
||||
Size = size,
|
||||
MediaType = "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] CreateLayer(params (string Path, byte[] Content)[] entries)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true))
|
||||
using (var tarWriter = new TarWriter(gzip, TarEntryFormat.Pax))
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var tarEntry = new PaxTarEntry(TarEntryType.RegularFile, entry.Path)
|
||||
{
|
||||
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
|
||||
ModificationTime = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
|
||||
DataStream = new MemoryStream(entry.Content, writable: false)
|
||||
};
|
||||
tarWriter.WriteEntry(tarEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] CreateElfBytes(char marker)
|
||||
{
|
||||
return
|
||||
[
|
||||
0x7F,
|
||||
(byte)'E',
|
||||
(byte)'L',
|
||||
(byte)'F',
|
||||
(byte)marker
|
||||
];
|
||||
}
|
||||
|
||||
private static ElfSectionHashSet CreateHashSet(string path, char marker)
|
||||
{
|
||||
var hash = new string(marker, 64);
|
||||
var section = new ElfSectionHash
|
||||
{
|
||||
Name = ".text",
|
||||
Offset = 0,
|
||||
Size = 16,
|
||||
Sha256 = hash,
|
||||
Blake3 = null,
|
||||
SectionType = ElfSectionType.ProgBits,
|
||||
Flags = ElfSectionFlags.Alloc
|
||||
};
|
||||
|
||||
return new ElfSectionHashSet
|
||||
{
|
||||
FilePath = path,
|
||||
FileHash = hash,
|
||||
BuildId = "build-" + marker,
|
||||
Sections = ImmutableArray.Create(section),
|
||||
ExtractedAt = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
|
||||
ExtractorVersion = "test"
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestOciRegistryClient : IOciRegistryClient
|
||||
{
|
||||
private readonly Dictionary<string, string> _digestsByReference = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, OciManifest> _manifestsByDigest = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, byte[]> _blobsByDigest = new(StringComparer.Ordinal);
|
||||
|
||||
public void AddImage(string reference, string digest, OciManifest manifest)
|
||||
{
|
||||
_digestsByReference[reference] = digest;
|
||||
_manifestsByDigest[digest] = manifest;
|
||||
}
|
||||
|
||||
public void AddBlob(string digest, byte[] blob)
|
||||
{
|
||||
_blobsByDigest[digest] = blob;
|
||||
}
|
||||
|
||||
public Task<string> ResolveDigestAsync(OciImageReference reference, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_digestsByReference.TryGetValue(reference.Original, out var digest))
|
||||
{
|
||||
return Task.FromResult(digest);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Digest not configured for {reference.Original}");
|
||||
}
|
||||
|
||||
public Task<string> ResolveTagAsync(string registry, string repository, string tag, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException("ResolveTagAsync is not used by these tests.");
|
||||
}
|
||||
|
||||
public Task<OciReferrersResponse> ListReferrersAsync(
|
||||
OciImageReference reference,
|
||||
string digest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException("ListReferrersAsync is not used by these tests.");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<OciReferrerDescriptor>> GetReferrersAsync(
|
||||
string registry,
|
||||
string repository,
|
||||
string digest,
|
||||
string? artifactType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException("GetReferrersAsync is not used by these tests.");
|
||||
}
|
||||
|
||||
public Task<OciManifest> GetManifestAsync(
|
||||
OciImageReference reference,
|
||||
string digest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_manifestsByDigest.TryGetValue(digest, out var manifest))
|
||||
{
|
||||
return Task.FromResult(manifest);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Manifest not configured for {digest}");
|
||||
}
|
||||
|
||||
public Task<byte[]> GetBlobAsync(
|
||||
OciImageReference reference,
|
||||
string digest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_blobsByDigest.TryGetValue(digest, out var blob))
|
||||
{
|
||||
return Task.FromResult(blob);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Blob not configured for {digest}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestElfSectionHashExtractor : IElfSectionHashExtractor
|
||||
{
|
||||
public Task<ElfSectionHashSet?> ExtractAsync(string elfPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(elfPath);
|
||||
return ExtractFromBytesAsync(bytes, elfPath, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<ElfSectionHashSet?> ExtractFromBytesAsync(
|
||||
ReadOnlyMemory<byte> elfBytes,
|
||||
string virtualPath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (elfBytes.Length < 5)
|
||||
{
|
||||
return Task.FromResult<ElfSectionHashSet?>(null);
|
||||
}
|
||||
|
||||
var marker = (char)elfBytes.Span[4];
|
||||
var normalized = marker switch
|
||||
{
|
||||
'a' => 'a',
|
||||
'b' => 'b',
|
||||
_ => 'c'
|
||||
};
|
||||
|
||||
return Task.FromResult<ElfSectionHashSet?>(CreateHashSet(virtualPath, normalized));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user