Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images. - Added symbols.json detailing function entry and sink points in the WordPress code. - Included runtime traces for function calls in both reachable and unreachable scenarios. - Developed OpenVEX files indicating vulnerability status and justification for both cases. - Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Infrastructure;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.InMemory;
|
||||
using StellaOps.Findings.Ledger.Options;
|
||||
using StellaOps.Findings.Ledger.Services;
|
||||
using StellaOps.Findings.Ledger.Workflow;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public sealed class FindingWorkflowServiceTests
|
||||
{
|
||||
private readonly InMemoryLedgerEventRepository _repository = new();
|
||||
private readonly IFindingWorkflowService _service;
|
||||
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.Parse("2025-11-07T18:30:15Z"));
|
||||
|
||||
public FindingWorkflowServiceTests()
|
||||
{
|
||||
var writeService = new LedgerEventWriteService(
|
||||
_repository,
|
||||
new NoOpMerkleScheduler(),
|
||||
NullLogger<LedgerEventWriteService>.Instance);
|
||||
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new LedgerServiceOptions
|
||||
{
|
||||
Attachments = new LedgerServiceOptions.AttachmentsOptions
|
||||
{
|
||||
Enabled = true,
|
||||
EncryptionKey = Convert.ToBase64String(Enumerable.Repeat((byte)0x22, 32).ToArray()),
|
||||
SignedUrlBase = "https://evidence.local/attachments",
|
||||
SignedUrlSecret = "signed-secret",
|
||||
SignedUrlLifetime = TimeSpan.FromMinutes(5),
|
||||
RequireConsoleCsrf = false
|
||||
}
|
||||
});
|
||||
|
||||
_service = new FindingWorkflowService(
|
||||
_repository,
|
||||
writeService,
|
||||
new FakeAttachmentEncryptionService(),
|
||||
new FakeAttachmentUrlSigner(),
|
||||
options,
|
||||
_timeProvider,
|
||||
NullLogger<FindingWorkflowService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssignAsync_WritesLedgerEventWithAssigneeAndAttachments()
|
||||
{
|
||||
var request = new AssignWorkflowRequest
|
||||
{
|
||||
TenantId = "tenant-east",
|
||||
PolicyVersion = "sha256:policy@1",
|
||||
FindingId = "finding|sha256:abc|CVE-2025-1234",
|
||||
ArtifactId = "sha256:abc",
|
||||
VulnerabilityId = "CVE-2025-1234",
|
||||
Actor = new WorkflowActor("user:alice", "operator", "Alice"),
|
||||
Assignee = new WorkflowAssignee("user:bob", "operator", "Bob"),
|
||||
Comment = "Taking ownership",
|
||||
Attachments = new[]
|
||||
{
|
||||
new WorkflowAttachmentMetadata(
|
||||
"att-1",
|
||||
"evidence.txt",
|
||||
"text/plain",
|
||||
128,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
new Dictionary<string, string> { ["scope"] = "triage" })
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _service.AssignAsync(request, CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(LedgerWriteStatus.Success);
|
||||
result.Record.Should().NotBeNull();
|
||||
var record = result.Record!;
|
||||
record.EventType.Should().Be(LedgerEventConstants.EventFindingAssignmentChanged);
|
||||
record.SequenceNumber.Should().Be(1);
|
||||
record.ChainId.Should().NotBe(Guid.Empty);
|
||||
record.OccurredAt.Should().Be(_timeProvider.UtcNow);
|
||||
|
||||
var payload = ExtractPayload(record);
|
||||
payload["action"]!.GetValue<string>().Should().Be("assign");
|
||||
payload["assignee"]!.AsObject()["id"]!.GetValue<string>().Should().Be("user:bob");
|
||||
var attachments = payload["attachments"]!.AsArray();
|
||||
attachments.Count.Should().Be(1);
|
||||
var attachmentNode = attachments[0]!.AsObject();
|
||||
attachmentNode["fileName"]!.GetValue<string>().Should().Be("evidence.txt");
|
||||
attachmentNode["signedUrl"]!.GetValue<string>().Should().StartWith("https://signed.local/");
|
||||
attachmentNode["envelope"]!.AsObject()["algorithm"]!.GetValue<string>().Should().Be("AES-256-GCM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcceptRiskAsync_DefaultsStatusAndPersistsJustification()
|
||||
{
|
||||
var request = new AcceptRiskWorkflowRequest
|
||||
{
|
||||
TenantId = "tenant-east",
|
||||
PolicyVersion = "sha256:policy@1",
|
||||
FindingId = "finding|sha256:abc|CVE-2025-1234",
|
||||
ArtifactId = "sha256:abc",
|
||||
VulnerabilityId = "CVE-2025-1234",
|
||||
Actor = new WorkflowActor("user:alice", "operator"),
|
||||
Justification = "Risk accepted for 30 days",
|
||||
ExpiresAt = _timeProvider.UtcNow.AddDays(30),
|
||||
RiskOwner = "ops-team"
|
||||
};
|
||||
|
||||
var result = await _service.AcceptRiskAsync(request, CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(LedgerWriteStatus.Success);
|
||||
var payload = ExtractPayload(result.Record!);
|
||||
payload["status"]!.GetValue<string>().Should().Be("accepted_risk");
|
||||
payload["justification"]!.GetValue<string>().Should().Be("Risk accepted for 30 days");
|
||||
payload["riskOwner"]!.GetValue<string>().Should().Be("ops-team");
|
||||
payload.TryGetPropertyValue("attachments", out _).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CommentAsync_ValidatesCommentPresence()
|
||||
{
|
||||
var request = new CommentWorkflowRequest
|
||||
{
|
||||
TenantId = "tenant-east",
|
||||
PolicyVersion = "sha256:policy@1",
|
||||
FindingId = "finding|sha256:abc|CVE-2025-1234",
|
||||
ArtifactId = "sha256:abc",
|
||||
VulnerabilityId = "CVE-2025-1234",
|
||||
Actor = new WorkflowActor("user:alice", "operator"),
|
||||
Comment = " "
|
||||
};
|
||||
|
||||
var result = await _service.CommentAsync(request, CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(LedgerWriteStatus.ValidationFailed);
|
||||
result.Errors.Should().Contain("comment_required");
|
||||
}
|
||||
|
||||
private static JsonObject ExtractPayload(LedgerEventRecord record)
|
||||
{
|
||||
var eventNode = record.EventBody["event"]?.AsObject()
|
||||
?? throw new InvalidOperationException("event node missing");
|
||||
return eventNode["payload"]?.AsObject()
|
||||
?? throw new InvalidOperationException("payload node missing");
|
||||
}
|
||||
|
||||
private sealed class NoOpMerkleScheduler : IMerkleAnchorScheduler
|
||||
{
|
||||
public Task EnqueueAsync(LedgerEventRecord record, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
public FakeTimeProvider(DateTimeOffset fixedTime) => UtcNow = fixedTime;
|
||||
|
||||
public DateTimeOffset UtcNow { get; }
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => UtcNow;
|
||||
}
|
||||
|
||||
private sealed class FakeAttachmentEncryptionService : IAttachmentEncryptionService
|
||||
{
|
||||
public AttachmentEncryptionResult CreateEnvelope(NormalizedAttachment attachment, DateTimeOffset now)
|
||||
=> new(
|
||||
Algorithm: "AES-256-GCM",
|
||||
Ciphertext: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("ciphertext")),
|
||||
Nonce: Convert.ToBase64String(new byte[] { 1, 2, 3 }),
|
||||
Tag: Convert.ToBase64String(new byte[] { 4, 5, 6 }),
|
||||
ExpiresAt: now.AddMinutes(5));
|
||||
}
|
||||
|
||||
private sealed class FakeAttachmentUrlSigner : IAttachmentUrlSigner
|
||||
{
|
||||
public AttachmentSignedUrl Sign(string attachmentId, DateTimeOffset now, TimeSpan lifetime)
|
||||
=> new(new Uri($"https://signed.local/{attachmentId}?sig=fake"), now.Add(lifetime));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user