Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -72,4 +72,16 @@ public sealed class CommandFactoryTests
|
||||
Assert.Contains(bun.Subcommands, command => string.Equals(command.Name, "inspect", StringComparison.Ordinal));
|
||||
Assert.Contains(bun.Subcommands, command => string.Equals(command.Name, "resolve", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ExposesSbomUploadCommand()
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var root = CommandFactory.Create(services, new StellaOpsCliOptions(), CancellationToken.None, loggerFactory);
|
||||
|
||||
var sbom = Assert.Single(root.Subcommands, command => string.Equals(command.Name, "sbom", StringComparison.Ordinal));
|
||||
|
||||
Assert.Contains(sbom.Subcommands, command => string.Equals(command.Name, "upload", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Testing;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
public sealed class SbomUploadCommandHandlersTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HandleSbomUploadAsync_ReturnsErrorOnInvalidValidation()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"sbom-{Guid.NewGuid():N}.json");
|
||||
await File.WriteAllTextAsync(tempPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.6\",\"components\":[]}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = new SbomUploadResponse
|
||||
{
|
||||
SbomId = "sbom-1",
|
||||
ArtifactRef = "example.com/app:1.0",
|
||||
ValidationResult = new SbomUploadValidationSummary
|
||||
{
|
||||
Valid = false,
|
||||
Errors = new[] { "Invalid SBOM." }
|
||||
}
|
||||
};
|
||||
|
||||
var provider = BuildServiceProvider(new StubSbomClient(response));
|
||||
var exitCode = await RunWithTestConsoleAsync(() =>
|
||||
CommandHandlers.HandleSbomUploadAsync(
|
||||
provider,
|
||||
tempPath,
|
||||
"example.com/app:1.0",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
json: false,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None));
|
||||
|
||||
Assert.Equal(18, exitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleSbomUploadAsync_ReturnsZeroOnSuccess()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"sbom-{Guid.NewGuid():N}.json");
|
||||
await File.WriteAllTextAsync(tempPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.6\",\"components\":[]}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = new SbomUploadResponse
|
||||
{
|
||||
SbomId = "sbom-2",
|
||||
ArtifactRef = "example.com/app:2.0",
|
||||
Digest = "sha256:abc",
|
||||
Format = "cyclonedx",
|
||||
FormatVersion = "1.6",
|
||||
AnalysisJobId = "job-1",
|
||||
ValidationResult = new SbomUploadValidationSummary
|
||||
{
|
||||
Valid = true,
|
||||
ComponentCount = 0,
|
||||
QualityScore = 1.0
|
||||
}
|
||||
};
|
||||
|
||||
var provider = BuildServiceProvider(new StubSbomClient(response));
|
||||
var exitCode = await RunWithTestConsoleAsync(() =>
|
||||
CommandHandlers.HandleSbomUploadAsync(
|
||||
provider,
|
||||
tempPath,
|
||||
"example.com/app:2.0",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
json: false,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None));
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static IServiceProvider BuildServiceProvider(ISbomClient client)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(client);
|
||||
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None)));
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static async Task<int> RunWithTestConsoleAsync(Func<Task<int>> action)
|
||||
{
|
||||
var original = AnsiConsole.Console;
|
||||
var testConsole = new TestConsole();
|
||||
try
|
||||
{
|
||||
AnsiConsole.Console = testConsole;
|
||||
return await action().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = original;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubSbomClient : ISbomClient
|
||||
{
|
||||
private readonly SbomUploadResponse? _response;
|
||||
|
||||
public StubSbomClient(SbomUploadResponse? response)
|
||||
{
|
||||
_response = response;
|
||||
}
|
||||
|
||||
public Task<SbomListResponse> ListAsync(SbomListRequest request, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<SbomDetailResponse?> GetAsync(string sbomId, string? tenant, bool includeComponents, bool includeVulnerabilities, bool includeLicenses, bool explain, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<SbomCompareResponse?> CompareAsync(SbomCompareRequest request, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<(Stream Content, SbomExportResult? Result)> ExportAsync(SbomExportRequest request, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<SbomUploadResponse?> UploadAsync(SbomUploadRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(_response);
|
||||
|
||||
public Task<ParityMatrixResponse> GetParityMatrixAsync(string? tenant, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sprint5100_CommandTests.cs
|
||||
// Sprint: SPRINT_5100_0002_0002 / SPRINT_5100_0002_0003
|
||||
// Description: CLI command tree tests for replay and delta commands
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
using StellaOps.Cli.Commands;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
public class Sprint5100_CommandTests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
public Sprint5100_CommandTests()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
_services = serviceCollection.BuildServiceProvider();
|
||||
_verboseOption = new Option<bool>("--verbose", "-v") { Description = "Verbose output" };
|
||||
_cancellationToken = CancellationToken.None;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplayCommand_CreatesCommandTree()
|
||||
{
|
||||
var command = ReplayCommandGroup.BuildReplayCommand(_verboseOption, _cancellationToken);
|
||||
|
||||
Assert.Equal("replay", command.Name);
|
||||
Assert.Contains("Replay scans", command.Description);
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "verify"));
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "diff"));
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "batch"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplayCommand_ParsesWithManifest()
|
||||
{
|
||||
var command = ReplayCommandGroup.BuildReplayCommand(_verboseOption, _cancellationToken);
|
||||
var root = new RootCommand { command };
|
||||
|
||||
var result = root.Parse("replay --manifest run-manifest.json");
|
||||
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaCommand_CreatesCommandTree()
|
||||
{
|
||||
var command = DeltaCommandGroup.BuildDeltaCommand(_verboseOption, _cancellationToken);
|
||||
|
||||
Assert.Equal("delta", command.Name);
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "compute"));
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "check"));
|
||||
Assert.NotNull(command.Subcommands.FirstOrDefault(c => c.Name == "attach"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaCompute_ParsesRequiredOptions()
|
||||
{
|
||||
var command = DeltaCommandGroup.BuildDeltaCommand(_verboseOption, _cancellationToken);
|
||||
var root = new RootCommand { command };
|
||||
|
||||
var result = root.Parse("delta compute --base base.json --head head.json");
|
||||
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaCheck_RequiresDeltaOption()
|
||||
{
|
||||
var command = DeltaCommandGroup.BuildDeltaCommand(_verboseOption, _cancellationToken);
|
||||
var root = new RootCommand { command };
|
||||
|
||||
var result = root.Parse("delta check");
|
||||
|
||||
Assert.NotEmpty(result.Errors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Configuration;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
public sealed class VerifyImageCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ExposesVerifyImageCommand()
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var root = CommandFactory.Create(services, new StellaOpsCliOptions(), CancellationToken.None, loggerFactory);
|
||||
|
||||
var verify = Assert.Single(root.Subcommands, command => string.Equals(command.Name, "verify", StringComparison.Ordinal));
|
||||
var image = Assert.Single(verify.Subcommands, command => string.Equals(command.Name, "image", StringComparison.Ordinal));
|
||||
|
||||
Assert.Contains(image.Options, option => option.HasAlias("--require"));
|
||||
Assert.Contains(image.Options, option => option.HasAlias("--trust-policy"));
|
||||
Assert.Contains(image.Options, option => option.HasAlias("--output"));
|
||||
Assert.Contains(image.Options, option => option.HasAlias("--strict"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Testing;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
public sealed class VerifyImageHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseImageReference_WithDigest_Parses()
|
||||
{
|
||||
var (registry, repository, digest) = CommandHandlers.ParseImageReference("gcr.io/myproject/myapp@sha256:abc123");
|
||||
|
||||
Assert.Equal("gcr.io", registry);
|
||||
Assert.Equal("myproject/myapp", repository);
|
||||
Assert.Equal("sha256:abc123", digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleVerifyImageAsync_ValidResult_ReturnsZero()
|
||||
{
|
||||
var result = new ImageVerificationResult
|
||||
{
|
||||
ImageReference = "registry.example.com/app@sha256:deadbeef",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
IsValid = true
|
||||
};
|
||||
|
||||
var provider = BuildServices(new StubVerifier(result));
|
||||
var originalExit = Environment.ExitCode;
|
||||
|
||||
try
|
||||
{
|
||||
await CaptureConsoleAsync(async _ =>
|
||||
{
|
||||
var exitCode = await CommandHandlers.HandleVerifyImageAsync(
|
||||
provider,
|
||||
"registry.example.com/app@sha256:deadbeef",
|
||||
new[] { "sbom" },
|
||||
trustPolicy: null,
|
||||
output: "json",
|
||||
strict: false,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
});
|
||||
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleVerifyImageAsync_InvalidResult_ReturnsOne()
|
||||
{
|
||||
var result = new ImageVerificationResult
|
||||
{
|
||||
ImageReference = "registry.example.com/app@sha256:deadbeef",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
IsValid = false
|
||||
};
|
||||
|
||||
var provider = BuildServices(new StubVerifier(result));
|
||||
var originalExit = Environment.ExitCode;
|
||||
|
||||
try
|
||||
{
|
||||
await CaptureConsoleAsync(async _ =>
|
||||
{
|
||||
var exitCode = await CommandHandlers.HandleVerifyImageAsync(
|
||||
provider,
|
||||
"registry.example.com/app@sha256:deadbeef",
|
||||
new[] { "sbom" },
|
||||
trustPolicy: null,
|
||||
output: "json",
|
||||
strict: true,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, exitCode);
|
||||
});
|
||||
|
||||
Assert.Equal(1, Environment.ExitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildServices(IImageAttestationVerifier verifier)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
services.AddSingleton(new StellaOpsCliOptions());
|
||||
services.AddSingleton(verifier);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static async Task CaptureConsoleAsync(Func<TestConsole, Task> action)
|
||||
{
|
||||
var testConsole = new TestConsole();
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
|
||||
try
|
||||
{
|
||||
AnsiConsole.Console = testConsole;
|
||||
Console.SetOut(writer);
|
||||
await action(testConsole).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubVerifier : IImageAttestationVerifier
|
||||
{
|
||||
private readonly ImageVerificationResult _result;
|
||||
|
||||
public StubVerifier(ImageVerificationResult result)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public Task<ImageVerificationResult> VerifyAsync(ImageVerificationRequest request, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Services;
|
||||
|
||||
public sealed class ImageAttestationVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyAsync_MissingAttestation_Strict_ReturnsFail()
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
var registry = new StubRegistryClient("sha256:deadbeef", new OciReferrersResponse());
|
||||
var policy = new TrustPolicyContext();
|
||||
var verifier = new ImageAttestationVerifier(
|
||||
registry,
|
||||
new StubTrustPolicyLoader(policy),
|
||||
new StubDsseVerifier(),
|
||||
loggerFactory.CreateLogger<ImageAttestationVerifier>());
|
||||
|
||||
var result = await verifier.VerifyAsync(new ImageVerificationRequest
|
||||
{
|
||||
Reference = "registry.example.com/app@sha256:deadbeef",
|
||||
RequiredTypes = new[] { "sbom" },
|
||||
Strict = true
|
||||
});
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Single(result.Attestations);
|
||||
Assert.Equal(AttestationStatus.Missing, result.Attestations[0].Status);
|
||||
Assert.Contains("sbom", result.MissingTypes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_MissingAttestation_NotStrict_Passes()
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
var registry = new StubRegistryClient("sha256:deadbeef", new OciReferrersResponse());
|
||||
var policy = new TrustPolicyContext();
|
||||
var verifier = new ImageAttestationVerifier(
|
||||
registry,
|
||||
new StubTrustPolicyLoader(policy),
|
||||
new StubDsseVerifier(),
|
||||
loggerFactory.CreateLogger<ImageAttestationVerifier>());
|
||||
|
||||
var result = await verifier.VerifyAsync(new ImageVerificationRequest
|
||||
{
|
||||
Reference = "registry.example.com/app@sha256:deadbeef",
|
||||
RequiredTypes = new[] { "sbom" },
|
||||
Strict = false
|
||||
});
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Single(result.Attestations);
|
||||
Assert.Equal(AttestationStatus.Missing, result.Attestations[0].Status);
|
||||
}
|
||||
|
||||
private sealed class StubRegistryClient : IOciRegistryClient
|
||||
{
|
||||
private readonly string _digest;
|
||||
private readonly OciReferrersResponse _referrers;
|
||||
|
||||
public StubRegistryClient(string digest, OciReferrersResponse referrers)
|
||||
{
|
||||
_digest = digest;
|
||||
_referrers = referrers;
|
||||
}
|
||||
|
||||
public Task<string> ResolveDigestAsync(OciImageReference reference, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_digest);
|
||||
|
||||
public Task<OciReferrersResponse> ListReferrersAsync(OciImageReference reference, string digest, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_referrers);
|
||||
|
||||
public Task<OciManifest> GetManifestAsync(OciImageReference reference, string digest, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new OciManifest());
|
||||
|
||||
public Task<byte[]> GetBlobAsync(OciImageReference reference, string digest, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Array.Empty<byte>());
|
||||
}
|
||||
|
||||
private sealed class StubTrustPolicyLoader : ITrustPolicyLoader
|
||||
{
|
||||
private readonly TrustPolicyContext _context;
|
||||
|
||||
public StubTrustPolicyLoader(TrustPolicyContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<TrustPolicyContext> LoadAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_context);
|
||||
}
|
||||
|
||||
private sealed class StubDsseVerifier : IDsseSignatureVerifier
|
||||
{
|
||||
public DsseSignatureVerificationResult Verify(
|
||||
string payloadType,
|
||||
string payloadBase64,
|
||||
IReadOnlyList<DsseSignatureInput> signatures,
|
||||
TrustPolicyContext policy)
|
||||
{
|
||||
return new DsseSignatureVerificationResult
|
||||
{
|
||||
IsValid = true,
|
||||
KeyId = signatures.Count > 0 ? signatures[0].KeyId : null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Services;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Services;
|
||||
|
||||
public sealed class TrustPolicyLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesYamlAndKeys()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"stellaops-trust-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var keyPath = Path.Combine(tempDir, "test-key.pem");
|
||||
File.WriteAllText(keyPath, GenerateRsaPublicKeyPem());
|
||||
|
||||
var policyPath = Path.Combine(tempDir, "trust-policy.yaml");
|
||||
var yaml = $@"
|
||||
version: ""1""
|
||||
attestations:
|
||||
sbom:
|
||||
required: true
|
||||
signers:
|
||||
- identity: ""builder@example.com""
|
||||
defaults:
|
||||
requireRekor: true
|
||||
maxAge: ""168h""
|
||||
keys:
|
||||
- id: ""builder-key""
|
||||
path: ""{Path.GetFileName(keyPath)}""
|
||||
algorithm: ""rsa-pss-sha256""
|
||||
";
|
||||
File.WriteAllText(policyPath, yaml.Trim(), Encoding.UTF8);
|
||||
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
|
||||
var loader = new TrustPolicyLoader(loggerFactory.CreateLogger<TrustPolicyLoader>());
|
||||
|
||||
var context = await loader.LoadAsync(policyPath, CancellationToken.None);
|
||||
|
||||
Assert.True(context.RequireRekor);
|
||||
Assert.Equal(TimeSpan.FromHours(168), context.MaxAge);
|
||||
Assert.Single(context.Keys);
|
||||
Assert.Equal("builder-key", context.Keys[0].KeyId);
|
||||
Assert.NotEmpty(context.Keys[0].Fingerprint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateRsaPublicKeyPem()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var publicKey = rsa.ExportSubjectPublicKeyInfo();
|
||||
var base64 = Convert.ToBase64String(publicKey);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-----BEGIN PUBLIC KEY-----");
|
||||
for (var i = 0; i < base64.Length; i += 64)
|
||||
{
|
||||
var chunk = base64.Substring(i, Math.Min(64, base64.Length - i));
|
||||
sb.AppendLine(chunk);
|
||||
}
|
||||
sb.AppendLine("-----END PUBLIC KEY-----");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user