release orchestration strengthening
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scanner.Emit.Spdx;
|
||||
using StellaOps.Scanner.WebService.Constants;
|
||||
@@ -223,8 +224,8 @@ internal static class ExportEndpoints
|
||||
string scanId,
|
||||
string? format,
|
||||
string? profile,
|
||||
IScanCoordinator coordinator,
|
||||
ISbomExportService sbomExportService,
|
||||
[FromServices] IScanCoordinator coordinator,
|
||||
[FromServices] ISbomExportService sbomExportService,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -350,9 +351,9 @@ internal static class ExportEndpoints
|
||||
string? compression,
|
||||
bool? includeRekor,
|
||||
bool? includeSchemas,
|
||||
IScanCoordinator coordinator,
|
||||
ISbomExportService sbomExportService,
|
||||
ISignedSbomArchiveBuilder archiveBuilder,
|
||||
[FromServices] IScanCoordinator coordinator,
|
||||
[FromServices] ISbomExportService sbomExportService,
|
||||
[FromServices] ISignedSbomArchiveBuilder archiveBuilder,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -418,9 +419,9 @@ internal static class ExportEndpoints
|
||||
SbomFormat = sbomFormatString,
|
||||
DsseEnvelopeBytes = CreatePlaceholderDsseEnvelope(sbomExport.Bytes),
|
||||
SigningCertPem = "-----BEGIN CERTIFICATE-----\nPlaceholder certificate for unsigned export\n-----END CERTIFICATE-----",
|
||||
ImageRef = snapshot.ImageRef ?? "unknown",
|
||||
ImageDigest = snapshot.ImageDigest ?? "sha256:unknown",
|
||||
Platform = snapshot.Platform,
|
||||
ImageRef = snapshot.Target.Reference ?? "unknown",
|
||||
ImageDigest = snapshot.Target.Digest ?? "sha256:unknown",
|
||||
Platform = null,
|
||||
ComponentCount = sbomExport.ComponentCount,
|
||||
PackageCount = sbomExport.ComponentCount, // Approximation
|
||||
FileCount = 0,
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.WebService.Diagnostics;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
|
||||
|
||||
@@ -361,60 +361,12 @@ internal static class ReachabilityEndpoints
|
||||
detail: "Requested scan could not be located.");
|
||||
}
|
||||
|
||||
// Determine export format (default to json-lines for determinism)
|
||||
var exportFormat = (format?.ToLowerInvariant()) switch
|
||||
{
|
||||
"graphson" => "graphson",
|
||||
"ndjson" or "json-lines" => "json-lines",
|
||||
_ => "json-lines"
|
||||
};
|
||||
|
||||
var options = new TraceExportOptions
|
||||
{
|
||||
Format = exportFormat,
|
||||
IncludeRuntimeEvidence = includeRuntimeEvidence ?? true,
|
||||
MinReachabilityScore = minReachabilityScore,
|
||||
RuntimeConfirmedOnly = runtimeConfirmedOnly ?? false
|
||||
};
|
||||
|
||||
var export = await queryService.ExportTracesAsync(parsed, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (export is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotFound,
|
||||
"No reachability data",
|
||||
StatusCodes.Status404NotFound,
|
||||
detail: "No reachability data found for this scan.");
|
||||
}
|
||||
|
||||
var response = new ReachabilityTraceExportDto(
|
||||
Format: export.Format,
|
||||
CanonicalizationMethod: "StellaOps.Canonical.Json",
|
||||
ContentDigest: export.ContentDigest,
|
||||
Timestamp: export.Timestamp,
|
||||
NodeCount: export.Nodes.Count,
|
||||
EdgeCount: export.Edges.Count,
|
||||
RuntimeCoverage: export.RuntimeCoverage,
|
||||
AverageReachabilityScore: export.AverageReachabilityScore,
|
||||
Nodes: export.Nodes.Select(n => new TraceNodeDto(
|
||||
Id: n.Id,
|
||||
SymbolId: n.SymbolId,
|
||||
ReachabilityScore: n.ReachabilityScore,
|
||||
RuntimeConfirmed: n.RuntimeConfirmed,
|
||||
RuntimeObservationCount: n.RuntimeObservationCount,
|
||||
Evidence: n.Evidence)).ToList(),
|
||||
Edges: export.Edges.Select(e => new TraceEdgeDto(
|
||||
From: e.From,
|
||||
To: e.To,
|
||||
Kind: e.Kind,
|
||||
Confidence: e.Confidence,
|
||||
RuntimeConfirmed: e.RuntimeConfirmed,
|
||||
RuntimeObservationCount: e.RuntimeObservationCount,
|
||||
Evidence: e.Evidence)).ToList());
|
||||
|
||||
return Json(response, StatusCodes.Status200OK);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotImplemented,
|
||||
"Trace export not available",
|
||||
StatusCodes.Status501NotImplemented,
|
||||
detail: "Reachability trace export is not supported by the current query service.");
|
||||
}
|
||||
|
||||
private static IResult Json<T>(T value, int statusCode)
|
||||
|
||||
@@ -89,7 +89,6 @@ internal static class ScanEndpoints
|
||||
scans.MapEvidenceEndpoints();
|
||||
scans.MapApprovalEndpoints();
|
||||
scans.MapManifestEndpoints();
|
||||
scans.MapLayerSbomEndpoints(); // Sprint: SPRINT_20260106_003_001
|
||||
scans.MapGitHubCodeScanningEndpoints(); // Sprint: SPRINT_20260109_010_002
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ using StellaOps.Scanner.Core;
|
||||
using StellaOps.Scanner.Core.Configuration;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.TrustAnchors;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.ReachabilityDrift.DependencyInjection;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
@@ -141,6 +142,8 @@ builder.Services.AddSingleton<ISarifExportService, ScanFindingsSarifExportServic
|
||||
|
||||
builder.Services.AddSingleton<ICycloneDxExportService, NullCycloneDxExportService>();
|
||||
builder.Services.AddSingleton<IOpenVexExportService, NullOpenVexExportService>();
|
||||
builder.Services.AddSingleton<ISpdxComposer, SpdxComposer>();
|
||||
builder.Services.AddSingleton<ISbomExportService, SbomExportService>();
|
||||
|
||||
// GitHub Code Scanning integration (Sprint: SPRINT_20260109_010_002)
|
||||
builder.Services.AddSingleton<IGitHubCodeScanningService, NullGitHubCodeScanningService>();
|
||||
|
||||
@@ -369,27 +369,26 @@ public sealed class EvidenceBundleExporter : IEvidenceBundleExporter
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Add DSSE-signed binary diff if attestation refs are present
|
||||
if (evidence.BinaryDiff.AttestationRef is not null)
|
||||
if (evidence.BinaryDiff.Attestation is not null)
|
||||
{
|
||||
var dsseWrapper = new
|
||||
{
|
||||
payloadType = "application/vnd.stellaops.binary-diff+json",
|
||||
payload = evidence.BinaryDiff,
|
||||
attestationRef = evidence.BinaryDiff.AttestationRef
|
||||
attestationRef = evidence.BinaryDiff.Attestation
|
||||
};
|
||||
await AddJsonFileAsync("binary-diff.dsse.json", dsseWrapper, streams, entries, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Add delta proof summary for semantic fingerprint changes
|
||||
if (evidence.BinaryDiff.SemanticDiff is not null)
|
||||
if (evidence.BinaryDiff.HasSemanticDiff)
|
||||
{
|
||||
var deltaProof = new
|
||||
{
|
||||
previousFingerprint = evidence.BinaryDiff.SemanticDiff.PreviousFingerprint,
|
||||
currentFingerprint = evidence.BinaryDiff.SemanticDiff.CurrentFingerprint,
|
||||
similarityScore = evidence.BinaryDiff.SemanticDiff.SimilarityScore,
|
||||
semanticChanges = evidence.BinaryDiff.SemanticDiff.SemanticChanges,
|
||||
previousBinaryDigest = evidence.BinaryDiff.PreviousBinaryDigest,
|
||||
currentBinaryDigest = evidence.BinaryDiff.CurrentBinaryDigest,
|
||||
similarityScore = evidence.BinaryDiff.SemanticSimilarity ?? evidence.BinaryDiff.SimilarityScore,
|
||||
functionChangeCount = evidence.BinaryDiff.FunctionChangeCount,
|
||||
securityChangeCount = evidence.BinaryDiff.SecurityChangeCount
|
||||
};
|
||||
|
||||
@@ -140,24 +140,6 @@ public sealed record StateFlipSummary
|
||||
/// </summary>
|
||||
public string? VerifyCommand { get; init; }
|
||||
}
|
||||
/// </summary>
|
||||
public required int NetChange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this PR should be blocked based on policy.
|
||||
/// </summary>
|
||||
public required bool ShouldBlockPr { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary.
|
||||
/// </summary>
|
||||
public required string Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual state flips.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<StateFlip> Flips { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual state flip.
|
||||
|
||||
@@ -371,9 +371,9 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
annotationResult.CommentBody!,
|
||||
cancellationToken);
|
||||
|
||||
if (commentResult.Success && commentResult.Value != null)
|
||||
if (commentResult.Success && commentResult.Data != null)
|
||||
{
|
||||
commentUrl = commentResult.Value.Url;
|
||||
commentUrl = commentResult.Data.Url;
|
||||
_logger.LogInformation(
|
||||
"Posted PR comment for {Owner}/{Repo}#{PrNumber}: {Url}",
|
||||
context.Owner,
|
||||
@@ -384,12 +384,11 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
else if (!commentResult.Success)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Failed to post PR comment for {Owner}/{Repo}#{PrNumber}: {Error} (Code: {Code})",
|
||||
"Failed to post PR comment for {Owner}/{Repo}#{PrNumber}: {Error}",
|
||||
context.Owner,
|
||||
context.Repository,
|
||||
context.PrNumber.ToString(CultureInfo.InvariantCulture),
|
||||
commentResult.ErrorMessage ?? "unknown",
|
||||
commentResult.ErrorCode ?? "N/A");
|
||||
commentResult.Error ?? "unknown");
|
||||
}
|
||||
|
||||
// Post status check
|
||||
@@ -403,7 +402,7 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
|
||||
if (statusResult.Success)
|
||||
{
|
||||
statusCheckResult = statusResult.Value?.State.ToString().ToLowerInvariant();
|
||||
statusCheckResult = statusResult.Data?.State.ToString().ToLowerInvariant();
|
||||
_logger.LogInformation(
|
||||
"Posted status check for {Owner}/{Repo}@{Sha}: {State}",
|
||||
context.Owner,
|
||||
@@ -531,13 +530,12 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
if (!lastResult.IsTransient)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"{Operation} failed for {Owner}/{Repo}#{PrNumber} with non-transient error: {Error} (Code: {Code})",
|
||||
"{Operation} failed for {Owner}/{Repo}#{PrNumber} with non-transient error: {Error}",
|
||||
operationName,
|
||||
context.Owner,
|
||||
context.Repository,
|
||||
context.PrNumber.ToString(CultureInfo.InvariantCulture),
|
||||
lastResult.ErrorMessage ?? "unknown",
|
||||
lastResult.ErrorCode ?? "N/A");
|
||||
lastResult.Error ?? "unknown");
|
||||
return lastResult;
|
||||
}
|
||||
|
||||
@@ -553,7 +551,7 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
backoffMs.ToString(CultureInfo.InvariantCulture),
|
||||
attempt.ToString(CultureInfo.InvariantCulture),
|
||||
MaxRetryAttempts.ToString(CultureInfo.InvariantCulture),
|
||||
lastResult.ErrorMessage ?? "unknown");
|
||||
lastResult.Error ?? "unknown");
|
||||
|
||||
await Task.Delay(backoffMs, cancellationToken);
|
||||
backoffMs *= 2; // Exponential backoff
|
||||
@@ -567,7 +565,7 @@ public sealed class PrAnnotationWebhookHandler : IPrAnnotationWebhookHandler
|
||||
context.Repository,
|
||||
context.PrNumber.ToString(CultureInfo.InvariantCulture),
|
||||
MaxRetryAttempts.ToString(CultureInfo.InvariantCulture),
|
||||
lastResult?.ErrorMessage ?? "unknown");
|
||||
lastResult?.Error ?? "unknown");
|
||||
|
||||
return lastResult!;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../Integrations/__Libraries/StellaOps.Integrations.Contracts/StellaOps.Integrations.Contracts.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Orchestration/StellaOps.Scanner.Orchestration.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Sources/StellaOps.Scanner.Sources.csproj" />
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class ApprovalEndpointsTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class ApprovalEndpointsTests : IAsyncLifetime
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
|
||||
@@ -9,7 +9,6 @@ using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.TestKit.Fixtures;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests.Contract;
|
||||
|
||||
@@ -23,12 +22,10 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
|
||||
{
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
private readonly string _snapshotPath;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ScannerOpenApiContractTests(ScannerApplicationFactory factory, ITestOutputHelper output)
|
||||
public ScannerOpenApiContractTests(ScannerApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_output = output;
|
||||
_snapshotPath = Path.Combine(AppContext.BaseDirectory, "Contract", "Expected", "scanner-openapi.json");
|
||||
}
|
||||
|
||||
@@ -79,15 +76,7 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
|
||||
Assert.Fail(message);
|
||||
}
|
||||
|
||||
// Log non-breaking changes for awareness
|
||||
if (changes.NonBreakingChanges.Count > 0)
|
||||
{
|
||||
_output.WriteLine("Non-breaking API changes detected:");
|
||||
foreach (var change in changes.NonBreakingChanges)
|
||||
{
|
||||
_output.WriteLine($" + {change}");
|
||||
}
|
||||
}
|
||||
// Non-breaking changes are allowed in contract checks.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class EpssEndpointsTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
_epssProvider = new InMemoryEpssProvider();
|
||||
@@ -41,7 +41,7 @@ public sealed class EpssEndpointsTests : IAsyncLifetime
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
|
||||
@@ -84,8 +84,8 @@ public sealed class EvidenceBundleExporterBinaryDiffTests
|
||||
|
||||
using var reader = new StreamReader(deltaProofEntry.Open());
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.Contains("previousFingerprint", content);
|
||||
Assert.Contains("currentFingerprint", content);
|
||||
Assert.Contains("previousBinaryDigest", content);
|
||||
Assert.Contains("currentBinaryDigest", content);
|
||||
Assert.Contains("similarityScore", content);
|
||||
}
|
||||
|
||||
@@ -181,54 +181,81 @@ public sealed class EvidenceBundleExporterBinaryDiffTests
|
||||
CveId = "CVE-2026-1234",
|
||||
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
||||
CacheKey = "cache-key-001",
|
||||
Manifests = new ManifestsDto
|
||||
Manifests = new ManifestHashesDto
|
||||
{
|
||||
ArtifactDigest = "sha256:abc123",
|
||||
ManifestHash = "sha256:manifest",
|
||||
FeedSnapshotHash = "sha256:feed",
|
||||
PolicyHash = "sha256:policy"
|
||||
}
|
||||
},
|
||||
Verification = new VerificationStatusDto
|
||||
{
|
||||
Status = "unknown",
|
||||
HashesVerified = false,
|
||||
AttestationsVerified = false,
|
||||
EvidenceComplete = false
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero)
|
||||
};
|
||||
}
|
||||
|
||||
private static UnifiedEvidenceResponseDto CreateEvidenceWithBinaryDiff()
|
||||
{
|
||||
var evidence = CreateMinimalEvidence();
|
||||
evidence.BinaryDiff = new BinaryDiffEvidenceDto
|
||||
return CreateMinimalEvidence() with
|
||||
{
|
||||
Status = "available",
|
||||
DiffType = "semantic",
|
||||
PreviousBinaryDigest = "sha256:old123",
|
||||
CurrentBinaryDigest = "sha256:new456",
|
||||
SimilarityScore = 0.95,
|
||||
FunctionChangeCount = 3,
|
||||
SecurityChangeCount = 1
|
||||
BinaryDiff = new BinaryDiffEvidenceDto
|
||||
{
|
||||
Status = "available",
|
||||
DiffType = "semantic",
|
||||
PreviousBinaryDigest = "sha256:old123",
|
||||
CurrentBinaryDigest = "sha256:new456",
|
||||
SimilarityScore = 0.95,
|
||||
FunctionChangeCount = 3,
|
||||
SecurityChangeCount = 1
|
||||
}
|
||||
};
|
||||
return evidence;
|
||||
}
|
||||
|
||||
private static UnifiedEvidenceResponseDto CreateEvidenceWithBinaryDiffAndAttestation()
|
||||
{
|
||||
var evidence = CreateEvidenceWithBinaryDiff();
|
||||
evidence.BinaryDiff!.AttestationRef = new AttestationRefDto
|
||||
return CreateMinimalEvidence() with
|
||||
{
|
||||
Id = "attest-12345",
|
||||
RekorLogIndex = 123456789,
|
||||
BundleDigest = "sha256:bundle123"
|
||||
BinaryDiff = new BinaryDiffEvidenceDto
|
||||
{
|
||||
Status = "available",
|
||||
DiffType = "semantic",
|
||||
PreviousBinaryDigest = "sha256:old123",
|
||||
CurrentBinaryDigest = "sha256:new456",
|
||||
SimilarityScore = 0.95,
|
||||
FunctionChangeCount = 3,
|
||||
SecurityChangeCount = 1,
|
||||
Attestation = new AttestationRefDto
|
||||
{
|
||||
Id = "attest-12345",
|
||||
PredicateType = "https://stellaops.dev/attestation/binary-diff/v1",
|
||||
RekorLogIndex = 123456789,
|
||||
EnvelopeDigest = "sha256:bundle123"
|
||||
}
|
||||
}
|
||||
};
|
||||
return evidence;
|
||||
}
|
||||
|
||||
private static UnifiedEvidenceResponseDto CreateEvidenceWithSemanticDiff()
|
||||
{
|
||||
var evidence = CreateEvidenceWithBinaryDiff();
|
||||
evidence.BinaryDiff!.SemanticDiff = new BinarySemanticDiffDto
|
||||
return CreateMinimalEvidence() with
|
||||
{
|
||||
PreviousFingerprint = "fp:abc123",
|
||||
CurrentFingerprint = "fp:def456",
|
||||
SimilarityScore = 0.92,
|
||||
SemanticChanges = new List<string> { "control_flow_modified", "data_flow_changed" }
|
||||
BinaryDiff = new BinaryDiffEvidenceDto
|
||||
{
|
||||
Status = "available",
|
||||
DiffType = "semantic",
|
||||
PreviousBinaryDigest = "sha256:old123",
|
||||
CurrentBinaryDigest = "sha256:new456",
|
||||
SimilarityScore = 0.95,
|
||||
FunctionChangeCount = 3,
|
||||
SecurityChangeCount = 1,
|
||||
HasSemanticDiff = true,
|
||||
SemanticSimilarity = 0.92
|
||||
}
|
||||
};
|
||||
return evidence;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,9 @@ public sealed class LayerSbomEndpointsTests
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
var coordinator = new StubScanCoordinator();
|
||||
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
||||
coordinator.AddScan(scanId, "sha256:image123");
|
||||
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
||||
{
|
||||
Valid = true,
|
||||
@@ -342,6 +344,8 @@ public sealed class LayerSbomEndpointsTests
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
services.RemoveAll<IScanCoordinator>();
|
||||
services.AddSingleton<IScanCoordinator>(coordinator);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
@@ -362,7 +366,9 @@ public sealed class LayerSbomEndpointsTests
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
var coordinator = new StubScanCoordinator();
|
||||
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
||||
coordinator.AddScan(scanId, "sha256:image123");
|
||||
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
||||
{
|
||||
Valid = false,
|
||||
@@ -376,6 +382,8 @@ public sealed class LayerSbomEndpointsTests
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
services.RemoveAll<IScanCoordinator>();
|
||||
services.AddSingleton<IScanCoordinator>(coordinator);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
@@ -400,6 +408,8 @@ public sealed class LayerSbomEndpointsTests
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
services.RemoveAll<IScanCoordinator>();
|
||||
services.AddSingleton<IScanCoordinator, StubScanCoordinator>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Attestation;
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
@@ -61,7 +62,9 @@ public sealed class OfflineKitEndpointsTests
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
content.Add(new StringContent(dsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
|
||||
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(dsseJson));
|
||||
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/import", content);
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
@@ -127,7 +130,9 @@ public sealed class OfflineKitEndpointsTests
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
content.Add(new StringContent(invalidDsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
|
||||
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(invalidDsseJson));
|
||||
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/import", content);
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
|
||||
@@ -178,7 +183,9 @@ public sealed class OfflineKitEndpointsTests
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
content.Add(new StringContent(invalidDsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
|
||||
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(invalidDsseJson));
|
||||
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/import", content);
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
@@ -609,7 +616,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
var pae = BuildPae(payloadType, payloadBase64);
|
||||
var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payloadBytes);
|
||||
var signature = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||
var signatureBase64 = Convert.ToBase64String(signature);
|
||||
|
||||
@@ -623,25 +630,6 @@ public sealed class OfflineKitEndpointsTests
|
||||
return (fingerprint, pem.ToString(), dsseJson);
|
||||
}
|
||||
|
||||
private static byte[] BuildPae(string payloadType, string payloadBase64)
|
||||
{
|
||||
var payloadText = Encoding.UTF8.GetString(Convert.FromBase64String(payloadBase64));
|
||||
var parts = new[] { "DSSEv1", payloadType, payloadText };
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("PAE:");
|
||||
builder.Append(parts.Length);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
builder.Append(' ');
|
||||
builder.Append(part.Length);
|
||||
builder.Append(' ');
|
||||
builder.Append(part);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(builder.ToString());
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory()
|
||||
|
||||
@@ -10,8 +10,8 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
public sealed class PlatformEventPublisherRegistrationTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NullPublisherRegisteredWhenEventsDisabled()
|
||||
[Fact]
|
||||
public async Task NullPublisherRegisteredWhenEventsDisabled()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
@@ -26,8 +26,8 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RedisPublisherRegisteredWhenEventsEnabled()
|
||||
[Fact]
|
||||
public async Task RedisPublisherRegisteredWhenEventsEnabled()
|
||||
{
|
||||
var originalEnabled = Environment.GetEnvironmentVariable("SCANNER__EVENTS__ENABLED");
|
||||
var originalDriver = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DRIVER");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
@@ -263,6 +264,24 @@ public sealed class PrAnnotationServiceTests
|
||||
/// </summary>
|
||||
private sealed class FakeReachabilityQueryService : IReachabilityQueryService
|
||||
{
|
||||
public Task<IReadOnlyList<ComponentReachability>> GetComponentsAsync(
|
||||
ScanId scanId,
|
||||
string? purlFilter,
|
||||
string? statusFilter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ComponentReachability>>(Array.Empty<ComponentReachability>());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ReachabilityFinding>> GetFindingsAsync(
|
||||
ScanId scanId,
|
||||
string? cveFilter,
|
||||
string? statusFilter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ReachabilityFinding>>(Array.Empty<ReachabilityFinding>());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<string, ReachabilityState>> GetReachabilityStatesAsync(
|
||||
string graphId,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -104,10 +104,10 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
return this;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
initializationTask ??= InitializeCoreAsync();
|
||||
return initializationTask;
|
||||
return new ValueTask(initializationTask);
|
||||
}
|
||||
|
||||
private async Task InitializeCoreAsync()
|
||||
@@ -135,9 +135,7 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
@@ -23,9 +22,9 @@ public sealed class ScannerApplicationFixture : IAsyncLifetime
|
||||
return client;
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Factory.InitializeAsync();
|
||||
public ValueTask InitializeAsync() => Factory.InitializeAsync();
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_authenticatedFactory = null;
|
||||
await Factory.DisposeAsync();
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
@@ -37,7 +37,7 @@ public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
|
||||
@@ -558,7 +558,7 @@ public sealed class SignedSbomArchiveBuilderTests : IDisposable
|
||||
|
||||
return new SignedSbomArchiveRequest
|
||||
{
|
||||
ScanId = ScanId.CreateNew(),
|
||||
ScanId = new ScanId("scan-test-001"),
|
||||
SbomBytes = sbomBytes,
|
||||
SbomFormat = "spdx-2.3",
|
||||
DsseEnvelopeBytes = dsseBytes,
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Spdx;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
@@ -19,25 +22,43 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
/// Sprint: SPRINT_20260107_004_002 Task SG-015
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplicationFixture>
|
||||
public sealed class Spdx3ExportEndpointsTests : IAsyncLifetime
|
||||
{
|
||||
private const string BasePath = "/api/scans";
|
||||
private readonly ScannerApplicationFixture _fixture;
|
||||
private const string BasePath = "/api/v1/scans";
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private InMemoryLayerSbomService _layerSbomService = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public Spdx3ExportEndpointsTests(ScannerApplicationFixture fixture)
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_fixture = fixture;
|
||||
_layerSbomService = new InMemoryLayerSbomService();
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(_layerSbomService);
|
||||
},
|
||||
useTestAuthentication: true);
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "test.valid.token");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSbomExport_WithFormatSpdx3_ReturnsSpdx3Document()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
var scanId = await CreateScanWithSbomAsync(client);
|
||||
var scanId = await CreateScanWithSbomAsync();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3");
|
||||
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -59,11 +80,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
public async Task GetSbomExport_WithProfileLite_ReturnsLiteProfile()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
var scanId = await CreateScanWithSbomAsync(client);
|
||||
var scanId = await CreateScanWithSbomAsync();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=lite");
|
||||
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=lite");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -85,11 +105,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
public async Task GetSbomExport_DefaultFormat_ReturnsSpdx2ForBackwardCompatibility()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
var scanId = await CreateScanWithSbomAsync(client);
|
||||
var scanId = await CreateScanWithSbomAsync();
|
||||
|
||||
// Act - no format specified
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom");
|
||||
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -101,11 +120,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
public async Task GetSbomExport_WithFormatCycloneDx_ReturnsCycloneDxDocument()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
var scanId = await CreateScanWithSbomAsync(client);
|
||||
var scanId = await CreateScanWithSbomAsync();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=cyclonedx");
|
||||
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=cyclonedx");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -118,10 +136,8 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
public async Task GetSbomExport_ScanNotFound_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"{BasePath}/nonexistent-scan/exports/sbom?format=spdx3");
|
||||
var response = await _client.GetAsync($"{BasePath}/nonexistent-scan/exports/sbom?format=spdx3");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
@@ -131,11 +147,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
public async Task GetSbomExport_SoftwareProfile_IncludesLicenseInfo()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient();
|
||||
var scanId = await CreateScanWithSbomAsync(client);
|
||||
var scanId = await CreateScanWithSbomAsync();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=software");
|
||||
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=software");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -153,21 +168,57 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
|
||||
packages.Should().NotBeEmpty("Software profile should include package elements");
|
||||
}
|
||||
|
||||
private async Task<string> CreateScanWithSbomAsync(HttpClient client)
|
||||
private async Task<string> CreateScanWithSbomAsync()
|
||||
{
|
||||
// Create a scan via the API
|
||||
var submitRequest = new
|
||||
{
|
||||
image = "registry.example.com/test:latest",
|
||||
digest = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
|
||||
image = new
|
||||
{
|
||||
reference = "registry.example.com/test:latest",
|
||||
digest = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
|
||||
}
|
||||
};
|
||||
|
||||
var submitResponse = await client.PostAsJsonAsync($"{BasePath}/", submitRequest);
|
||||
var submitResponse = await _client.PostAsJsonAsync($"{BasePath}/", submitRequest);
|
||||
submitResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var submitResult = await submitResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var scanId = submitResult.GetProperty("scanId").GetString();
|
||||
|
||||
if (scanId is not null)
|
||||
{
|
||||
var layerDigest = $"sha256:{Guid.NewGuid():N}";
|
||||
_layerSbomService.AddScan(scanId, submitRequest.image.digest, new[]
|
||||
{
|
||||
new LayerSummary
|
||||
{
|
||||
LayerDigest = layerDigest,
|
||||
Order = 1,
|
||||
HasSbom = true,
|
||||
ComponentCount = 2
|
||||
}
|
||||
});
|
||||
|
||||
var spdx2Bytes = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
spdxVersion = "SPDX-2.3",
|
||||
SPDXID = "SPDXRef-DOCUMENT",
|
||||
name = "test",
|
||||
packages = Array.Empty<object>()
|
||||
});
|
||||
_layerSbomService.AddLayerSbom(scanId, layerDigest, "spdx", spdx2Bytes);
|
||||
|
||||
var cdxBytes = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
bomFormat = "CycloneDX",
|
||||
specVersion = "1.7",
|
||||
version = 1,
|
||||
components = Array.Empty<object>()
|
||||
});
|
||||
_layerSbomService.AddLayerSbom(scanId, layerDigest, "cdx", cdxBytes);
|
||||
}
|
||||
|
||||
// Wait briefly for scan to initialize (in real tests, this would poll for completion)
|
||||
await Task.Delay(100);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user