up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class LinksetResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ResolveAsync_MapsSeveritiesAndConflicts()
|
||||
{
|
||||
var linkset = new AdvisoryLinkset(
|
||||
TenantId: "tenant-a",
|
||||
Source: "osv",
|
||||
AdvisoryId: "CVE-2025-0001",
|
||||
ObservationIds: ImmutableArray<string>.Empty,
|
||||
Normalized: new AdvisoryLinksetNormalized(
|
||||
Purls: new[] { "pkg:npm/demo@1.0.0" },
|
||||
Cpes: Array.Empty<string>(),
|
||||
Versions: Array.Empty<string>(),
|
||||
Ranges: Array.Empty<Dictionary<string, object?>>(),
|
||||
Severities: new[]
|
||||
{
|
||||
new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["source"] = "nvd",
|
||||
["type"] = "cvssv3",
|
||||
["score"] = 9.8,
|
||||
["vector"] = "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
["labels"] = new Dictionary<string, object?> { ["preferred"] = "true" }
|
||||
}
|
||||
}),
|
||||
Provenance: null,
|
||||
Confidence: 0.91,
|
||||
Conflicts: new[] { new AdvisoryLinksetConflict("severity", "disagree", new[] { "cvssv2", "cvssv3" }) },
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
BuiltByJobId: "job-1");
|
||||
|
||||
var resolver = new LinksetResolver(
|
||||
new FakeLinksetQueryService(linkset),
|
||||
new FakeSurfaceEnvironment(),
|
||||
NullLogger<LinksetResolver>.Instance);
|
||||
|
||||
var result = await resolver.ResolveAsync(new[]
|
||||
{
|
||||
new PolicyPreviewFindingDto { Id = "CVE-2025-0001" }
|
||||
}, CancellationToken.None);
|
||||
|
||||
var summary = Assert.Single(result);
|
||||
Assert.Equal("CVE-2025-0001", summary.AdvisoryId);
|
||||
Assert.Equal("osv", summary.Source);
|
||||
Assert.Equal(0.91, summary.Confidence);
|
||||
|
||||
var severity = Assert.Single(summary.Severities!);
|
||||
Assert.Equal("nvd", severity.Source);
|
||||
Assert.Equal("cvssv3", severity.Type);
|
||||
Assert.Equal(9.8, severity.Score);
|
||||
Assert.Equal("AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", severity.Vector);
|
||||
Assert.NotNull(severity.Labels);
|
||||
|
||||
var conflict = Assert.Single(summary.Conflicts!);
|
||||
Assert.Equal("severity", conflict.Field);
|
||||
Assert.Equal("disagree", conflict.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ReturnsEmptyWhenNoIds()
|
||||
{
|
||||
var resolver = new LinksetResolver(
|
||||
new FakeLinksetQueryService(),
|
||||
new FakeSurfaceEnvironment(),
|
||||
NullLogger<LinksetResolver>.Instance);
|
||||
|
||||
var result = await resolver.ResolveAsync(Array.Empty<PolicyPreviewFindingDto>(), CancellationToken.None);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
private sealed class FakeLinksetQueryService : IAdvisoryLinksetQueryService
|
||||
{
|
||||
private readonly AdvisoryLinkset[] _linksets;
|
||||
|
||||
public FakeLinksetQueryService(params AdvisoryLinkset[] linksets)
|
||||
{
|
||||
_linksets = linksets;
|
||||
}
|
||||
|
||||
public Task<AdvisoryLinksetQueryResult> QueryAsync(AdvisoryLinksetQueryOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var matched = _linksets
|
||||
.Where(ls => options.AdvisoryIds?.Contains(ls.AdvisoryId, StringComparer.OrdinalIgnoreCase) == true)
|
||||
.ToImmutableArray();
|
||||
return Task.FromResult(new AdvisoryLinksetQueryResult(matched, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public SurfaceEnvironmentSettings Settings { get; } = new()
|
||||
{
|
||||
Tenant = "tenant-a"
|
||||
};
|
||||
|
||||
public IReadOnlyDictionary<string, string> RawVariables { get; } = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["SCANNER__TENANT"] = "tenant-a"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing.Surface;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
public sealed class HmacDsseEnvelopeSignerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SignAsync_UsesHmac_WhenSecretProvided()
|
||||
{
|
||||
var options = BuildOptions(signing =>
|
||||
{
|
||||
signing.EnableDsseSigning = true;
|
||||
signing.SharedSecret = "a2V5LXNlY3JldA=="; // base64("key-secret")
|
||||
signing.KeyId = "scanner-hmac";
|
||||
});
|
||||
|
||||
var signer = new HmacDsseEnvelopeSigner(options, NullLogger<HmacDsseEnvelopeSigner>.Instance);
|
||||
var payload = Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
|
||||
|
||||
var envelope = await signer.SignAsync("application/json", payload, "test.kind", "root", view: null, CancellationToken.None);
|
||||
|
||||
var json = JsonDocument.Parse(envelope.Content.Span);
|
||||
var sig = json.RootElement.GetProperty("signatures")[0].GetProperty("sig").GetString();
|
||||
|
||||
var expectedSig = ComputeExpectedSignature("application/json", payload, "a2V5LXNlY3JldA==");
|
||||
Assert.Equal(expectedSig, sig);
|
||||
Assert.Equal("application/vnd.dsse+json", envelope.MediaType);
|
||||
Assert.Equal("scanner-hmac", json.RootElement.GetProperty("signatures")[0].GetProperty("keyid").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignAsync_FallsBackToDeterministic_WhenSecretMissing()
|
||||
{
|
||||
var options = BuildOptions(signing =>
|
||||
{
|
||||
signing.EnableDsseSigning = true;
|
||||
signing.SharedSecret = null;
|
||||
signing.SharedSecretFile = null;
|
||||
signing.AllowDeterministicFallback = true;
|
||||
});
|
||||
|
||||
var signer = new HmacDsseEnvelopeSigner(options, NullLogger<HmacDsseEnvelopeSigner>.Instance);
|
||||
var payload = Encoding.UTF8.GetBytes("abc");
|
||||
|
||||
var envelope = await signer.SignAsync("text/plain", payload, "kind", "root", view: null, CancellationToken.None);
|
||||
var json = JsonDocument.Parse(envelope.Content.Span);
|
||||
var sig = json.RootElement.GetProperty("signatures")[0].GetProperty("sig").GetString();
|
||||
|
||||
// Deterministic signer encodes sha256 hex of payload as signature.
|
||||
var expected = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(payload)).ToLowerInvariant();
|
||||
var expectedBase64Url = Base64UrlEncode(Encoding.UTF8.GetBytes(expected));
|
||||
Assert.Equal(expectedBase64Url, sig);
|
||||
}
|
||||
|
||||
private static IOptions<ScannerWorkerOptions> BuildOptions(Action<ScannerWorkerOptions.SigningOptions> configure)
|
||||
{
|
||||
var options = new ScannerWorkerOptions();
|
||||
configure(options.Signing);
|
||||
return Microsoft.Extensions.Options.Options.Create(options);
|
||||
}
|
||||
|
||||
private static string ComputeExpectedSignature(string payloadType, byte[] payload, string base64Secret)
|
||||
{
|
||||
var secret = Convert.FromBase64String(base64Secret);
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(secret);
|
||||
var pae = BuildPae(payloadType, payload);
|
||||
var signature = hmac.ComputeHash(pae);
|
||||
return Base64UrlEncode(signature);
|
||||
}
|
||||
|
||||
private static byte[] BuildPae(string payloadType, byte[] payload)
|
||||
{
|
||||
const string prefix = "DSSEv1";
|
||||
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var typeLen = Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
|
||||
var payloadLen = Encoding.UTF8.GetBytes(payload.Length.ToString());
|
||||
|
||||
var total = prefix.Length + 1 + typeLen.Length + 1 + typeBytes.Length + 1 + payloadLen.Length + 1 + payload.Length;
|
||||
var buffer = new byte[total];
|
||||
var offset = 0;
|
||||
|
||||
Encoding.UTF8.GetBytes(prefix, buffer.AsSpan(offset));
|
||||
offset += prefix.Length;
|
||||
buffer[offset++] = 0x20;
|
||||
|
||||
typeLen.CopyTo(buffer.AsSpan(offset));
|
||||
offset += typeLen.Length;
|
||||
buffer[offset++] = 0x20;
|
||||
|
||||
typeBytes.CopyTo(buffer.AsSpan(offset));
|
||||
offset += typeBytes.Length;
|
||||
buffer[offset++] = 0x20;
|
||||
|
||||
payloadLen.CopyTo(buffer.AsSpan(offset));
|
||||
offset += payloadLen.Length;
|
||||
buffer[offset++] = 0x20;
|
||||
|
||||
payload.CopyTo(buffer.AsSpan(offset));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static string Base64UrlEncode(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var base64 = Convert.ToBase64String(data);
|
||||
return base64.TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user