up
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Attestation;
|
||||
|
||||
public sealed class AttestorClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendPlaceholderAsync_PostsJsonPayload()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
var attestorUri = new Uri("https://attestor.example.com/api/v1/provenance");
|
||||
|
||||
await client.SendPlaceholderAsync(attestorUri, document, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(handler.CapturedRequest);
|
||||
Assert.Equal(HttpMethod.Post, handler.CapturedRequest!.Method);
|
||||
Assert.Equal(attestorUri, handler.CapturedRequest.RequestUri);
|
||||
|
||||
var content = await handler.CapturedRequest.Content!.ReadAsStringAsync();
|
||||
var json = JsonDocument.Parse(content);
|
||||
Assert.Equal(document.Subject.Digest, json.RootElement.GetProperty("imageDigest").GetString());
|
||||
Assert.Equal(document.Artifact.Digest, json.RootElement.GetProperty("sbomDigest").GetString());
|
||||
Assert.Equal(document.Provenance.ExpectedDsseSha256, json.RootElement.GetProperty("expectedDsseSha256").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendPlaceholderAsync_ThrowsOnFailure()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = new StringContent("invalid")
|
||||
});
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
var attestorUri = new Uri("https://attestor.example.com/api/v1/provenance");
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => client.SendPlaceholderAsync(attestorUri, document, CancellationToken.None));
|
||||
}
|
||||
|
||||
private static DescriptorDocument BuildDescriptorDocument()
|
||||
{
|
||||
var subject = new DescriptorSubject("application/vnd.oci.image.manifest.v1+json", "sha256:img");
|
||||
var artifact = new DescriptorArtifact("application/vnd.cyclonedx+json", "sha256:sbom", 42, new System.Collections.Generic.Dictionary<string, string>());
|
||||
var provenance = new DescriptorProvenance("pending", "sha256:dsse", "nonce", "https://attestor.example.com/api/v1/provenance", "https://slsa.dev/provenance/v1");
|
||||
var generatorMetadata = new DescriptorGeneratorMetadata("generator", "1.0.0");
|
||||
var metadata = new System.Collections.Generic.Dictionary<string, string>();
|
||||
return new DescriptorDocument("schema", DateTimeOffset.UtcNow, generatorMetadata, subject, artifact, provenance, metadata);
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpResponseMessage response;
|
||||
|
||||
public RecordingHandler(HttpResponseMessage response)
|
||||
{
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public HttpRequestMessage? CapturedRequest { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
CapturedRequest = request;
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Cas;
|
||||
|
||||
public sealed class LocalCasClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyWriteAsync_WritesProbeObject()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var client = new LocalCasClient(new LocalCasOptions
|
||||
{
|
||||
RootDirectory = temp.Path,
|
||||
Algorithm = "sha256"
|
||||
});
|
||||
|
||||
var result = await client.VerifyWriteAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal("sha256", result.Algorithm);
|
||||
Assert.True(File.Exists(result.Path));
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(result.Path);
|
||||
Assert.Equal("stellaops-buildx-probe"u8.ToArray(), bytes);
|
||||
|
||||
var expectedDigest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
Assert.Equal(expectedDigest, result.Digest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateAsync_BuildsDeterministicDescriptor()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DescriptorGenerator.Schema, document.Schema);
|
||||
Assert.Equal(fakeTime.GetUtcNow(), document.GeneratedAt);
|
||||
Assert.Equal(request.ImageDigest, document.Subject.Digest);
|
||||
Assert.Equal(request.SbomMediaType, document.Artifact.MediaType);
|
||||
Assert.Equal(request.SbomName, document.Artifact.Annotations["org.opencontainers.image.title"]);
|
||||
Assert.Equal("pending", document.Provenance.Status);
|
||||
Assert.Equal(request.AttestorUri, document.Provenance.AttestorUri);
|
||||
Assert.Equal(request.PredicateType, document.Provenance.PredicateType);
|
||||
|
||||
var expectedSbomDigest = ComputeSha256File(sbomPath);
|
||||
Assert.Equal(expectedSbomDigest, document.Artifact.Digest);
|
||||
Assert.Equal(expectedSbomDigest, document.Metadata["sbomDigest"]);
|
||||
|
||||
var expectedDsse = ComputeExpectedDsse(request.ImageDigest, expectedSbomDigest, document.Provenance.Nonce);
|
||||
Assert.Equal(expectedDsse, document.Provenance.ExpectedDsseSha256);
|
||||
Assert.Equal(expectedDsse, document.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"]);
|
||||
}
|
||||
|
||||
private static string ComputeSha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = SHA256.HashData(stream);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
private static string ComputeExpectedDsse(string imageDigest, string sbomDigest, string nonce)
|
||||
{
|
||||
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(payload), hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Manifest;
|
||||
|
||||
public sealed class BuildxPluginManifestLoaderTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ReturnsManifestWithSourceInformation()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var manifestPath = System.IO.Path.Combine(temp.Path, "stellaops.manifest.json");
|
||||
await File.WriteAllTextAsync(manifestPath, BuildSampleManifestJson("stellaops.sbom-indexer"));
|
||||
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
var manifests = await loader.LoadAsync(CancellationToken.None);
|
||||
|
||||
var manifest = Assert.Single(manifests);
|
||||
Assert.Equal("stellaops.sbom-indexer", manifest.Id);
|
||||
Assert.Equal("0.1.0", manifest.Version);
|
||||
Assert.Equal(manifestPath, manifest.SourcePath);
|
||||
Assert.Equal(Path.GetDirectoryName(manifestPath), manifest.SourceDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadDefaultAsync_ThrowsWhenNoManifests()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => loader.LoadDefaultAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ThrowsWhenRestartRequiredMissing()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var manifestPath = Path.Combine(temp.Path, "failure.manifest.json");
|
||||
await File.WriteAllTextAsync(manifestPath, BuildSampleManifestJson("stellaops.failure", requiresRestart: false));
|
||||
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => loader.LoadAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private static string BuildSampleManifestJson(string id, bool requiresRestart = true)
|
||||
{
|
||||
var manifest = new BuildxPluginManifest
|
||||
{
|
||||
SchemaVersion = BuildxPluginManifest.CurrentSchemaVersion,
|
||||
Id = id,
|
||||
DisplayName = "Sample",
|
||||
Version = "0.1.0",
|
||||
RequiresRestart = requiresRestart,
|
||||
EntryPoint = new BuildxPluginEntryPoint
|
||||
{
|
||||
Type = "dotnet",
|
||||
Executable = "StellaOps.Scanner.Sbomer.BuildXPlugin.dll"
|
||||
},
|
||||
Cas = new BuildxPluginCas
|
||||
{
|
||||
Protocol = "filesystem",
|
||||
DefaultRoot = "cas"
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(manifest, SerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
|
||||
internal sealed class TempDirectory : IDisposable, IAsyncDisposable
|
||||
{
|
||||
public string Path { get; }
|
||||
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cleanup();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Cleanup();
|
||||
GC.SuppressFinalize(this);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user