notify doctors work, audit work, new product advisory sprints
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
public readonly record struct ScanId(string Value)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new ScanId with a random GUID value.
|
||||
/// Creates a new ScanId with a provided GUID generator.
|
||||
/// </summary>
|
||||
public static ScanId New() => new(Guid.NewGuid().ToString("D"));
|
||||
public static ScanId New(IGuidProvider guidProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guidProvider);
|
||||
return new ScanId(guidProvider.NewGuid().ToString("D"));
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.WebService.Diagnostics;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
@@ -26,12 +27,12 @@ internal static class HealthEndpoints
|
||||
group.MapGet("/healthz", HandleHealth)
|
||||
.WithName("scanner.health")
|
||||
.Produces<HealthDocument>(StatusCodes.Status200OK)
|
||||
.AllowAnonymous();
|
||||
.RequireAuthorization(ScannerPolicies.ScansRead);
|
||||
|
||||
group.MapGet("/readyz", HandleReady)
|
||||
.WithName("scanner.ready")
|
||||
.Produces<ReadyDocument>(StatusCodes.Status200OK)
|
||||
.AllowAnonymous();
|
||||
.RequireAuthorization(ScannerPolicies.ScansRead);
|
||||
}
|
||||
|
||||
private static IResult HandleHealth(
|
||||
|
||||
@@ -20,6 +20,7 @@ using RuntimePolicyVerdict = StellaOps.Zastava.Core.Contracts.PolicyVerdict;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
// Suppress ASPDEPR002 for current minimal API route usage; revisit during endpoint modernization.
|
||||
#pragma warning disable ASPDEPR002
|
||||
|
||||
internal static class PolicyEndpoints
|
||||
|
||||
@@ -16,6 +16,7 @@ using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
// Suppress ASPDEPR002 for current minimal API route usage; revisit during endpoint modernization.
|
||||
#pragma warning disable ASPDEPR002
|
||||
|
||||
internal static class ReportEndpoints
|
||||
|
||||
@@ -134,45 +134,51 @@ internal static class WebhookEndpoints
|
||||
StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(source.WebhookSecretRef))
|
||||
{
|
||||
logger.LogWarning("Webhook secret not configured for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Webhook secret is not configured",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Determine signature to use
|
||||
var signature = signatureSha256 ?? signatureSha1 ?? gitlabToken ?? ExtractBearerToken(authorization);
|
||||
|
||||
// Verify signature if source has a webhook secret reference
|
||||
if (!string.IsNullOrEmpty(source.WebhookSecretRef))
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
logger.LogWarning("Webhook received without signature for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Missing webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
logger.LogWarning("Webhook received without signature for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Missing webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Resolve the webhook secret from the credential store
|
||||
var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct);
|
||||
var webhookSecret = secretCredential?.Token ?? secretCredential?.Password;
|
||||
// Resolve the webhook secret from the credential store
|
||||
var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct);
|
||||
var webhookSecret = secretCredential?.Token ?? secretCredential?.Password;
|
||||
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.InternalError,
|
||||
"Failed to resolve webhook secret",
|
||||
StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.InternalError,
|
||||
"Failed to resolve webhook secret",
|
||||
StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Invalid webhook signature for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Invalid webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Invalid webhook signature for source {SourceId}", sourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Invalid webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Parse the payload
|
||||
@@ -446,6 +452,16 @@ internal static class WebhookEndpoints
|
||||
StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(source.WebhookSecretRef))
|
||||
{
|
||||
logger.LogWarning("Webhook secret not configured for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Webhook secret is not configured",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Get signature from header
|
||||
string? signature = signatureHeader switch
|
||||
{
|
||||
@@ -456,42 +472,38 @@ internal static class WebhookEndpoints
|
||||
_ => null
|
||||
};
|
||||
|
||||
// Verify signature if source has a webhook secret reference
|
||||
if (!string.IsNullOrEmpty(source.WebhookSecretRef))
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
logger.LogWarning("Webhook received without signature for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Missing webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
logger.LogWarning("Webhook received without signature for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Missing webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Resolve the webhook secret from the credential store
|
||||
var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct);
|
||||
var webhookSecret = secretCredential?.Token ?? secretCredential?.Password;
|
||||
// Resolve the webhook secret from the credential store
|
||||
var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct);
|
||||
var webhookSecret = secretCredential?.Token ?? secretCredential?.Password;
|
||||
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.InternalError,
|
||||
"Failed to resolve webhook secret",
|
||||
StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.InternalError,
|
||||
"Failed to resolve webhook secret",
|
||||
StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Invalid webhook signature for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Invalid webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret))
|
||||
{
|
||||
logger.LogWarning("Invalid webhook signature for source {SourceId}", source.SourceId);
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Authentication,
|
||||
"Invalid webhook signature",
|
||||
StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
// Parse the payload
|
||||
|
||||
@@ -44,7 +44,7 @@ internal sealed class ScannerSurfaceSecretConfigurator : IConfigureOptions<Scann
|
||||
CasAccessSecret? secret = null;
|
||||
try
|
||||
{
|
||||
using var handle = _secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
using var handle = _secretProvider.Get(request);
|
||||
secret = SurfaceSecretParser.ParseCasAccessSecret(handle);
|
||||
}
|
||||
catch (SurfaceSecretNotFoundException)
|
||||
@@ -74,7 +74,7 @@ internal sealed class ScannerSurfaceSecretConfigurator : IConfigureOptions<Scann
|
||||
RegistryAccessSecret? secret = null;
|
||||
try
|
||||
{
|
||||
using var handle = _secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
using var handle = _secretProvider.Get(request);
|
||||
secret = SurfaceSecretParser.ParseRegistryAccessSecret(handle);
|
||||
}
|
||||
catch (SurfaceSecretNotFoundException)
|
||||
@@ -143,7 +143,7 @@ internal sealed class ScannerSurfaceSecretConfigurator : IConfigureOptions<Scann
|
||||
AttestationSecret? secret = null;
|
||||
try
|
||||
{
|
||||
using var handle = _secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
using var handle = _secretProvider.Get(request);
|
||||
secret = SurfaceSecretParser.ParseAttestationSecret(handle);
|
||||
}
|
||||
catch (SurfaceSecretNotFoundException)
|
||||
|
||||
@@ -18,6 +18,7 @@ using StellaOps.Auth.Client;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
@@ -120,6 +121,7 @@ else
|
||||
{
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
}
|
||||
builder.Services.AddDeterminismDefaults();
|
||||
builder.Services.AddScannerCache(builder.Configuration);
|
||||
builder.Services.AddSingleton<ServiceStatus>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Serialization;
|
||||
|
||||
internal static class OrchestratorEventSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false);
|
||||
private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);
|
||||
private static readonly JsonSerializerOptions CanonicalOptions = CreateOptions();
|
||||
private static readonly JsonSerializerOptions PrettyOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
public static string Serialize(OrchestratorEvent @event)
|
||||
=> JsonSerializer.Serialize(@event, CompactOptions);
|
||||
=> Encoding.UTF8.GetString(CanonJson.Canonicalize(@event, CanonicalOptions));
|
||||
|
||||
public static string SerializeIndented(OrchestratorEvent @event)
|
||||
=> JsonSerializer.Serialize(@event, PrettyOptions);
|
||||
{
|
||||
var canonicalBytes = CanonJson.Canonicalize(@event, CanonicalOptions);
|
||||
using var document = JsonDocument.Parse(canonicalBytes);
|
||||
return JsonSerializer.Serialize(document.RootElement, PrettyOptions);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateOptions(bool writeIndented)
|
||||
private static JsonSerializerOptions CreateOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = writeIndented,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
var baselineResolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver();
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
@@ -28,6 +29,7 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
private readonly ILogger<HumanApprovalAttestationService> _logger;
|
||||
private readonly HumanApprovalAttestationOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory attestation store. In production, this would be backed by a database.
|
||||
@@ -41,10 +43,12 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
public HumanApprovalAttestationService(
|
||||
ILogger<HumanApprovalAttestationService> logger,
|
||||
IOptions<HumanApprovalAttestationOptions> options,
|
||||
IGuidProvider guidProvider,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
@@ -79,7 +83,7 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
var ttl = input.ApprovalTtl ?? TimeSpan.FromDays(_options.DefaultApprovalTtlDays);
|
||||
var expiresAt = now.Add(ttl);
|
||||
|
||||
var approvalId = $"approval-{Guid.NewGuid():N}";
|
||||
var approvalId = $"approval-{_guidProvider.NewGuid():N}";
|
||||
|
||||
var statement = BuildStatement(input, approvalId, now, expiresAt);
|
||||
var attestationId = ComputeAttestationId(statement);
|
||||
|
||||
@@ -15,12 +15,14 @@ namespace StellaOps.Scanner.WebService.Services;
|
||||
public sealed class LayerSbomService : ILayerSbomService
|
||||
{
|
||||
private readonly ICompositionRecipeService _recipeService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
// In-memory cache for layer SBOMs (would be replaced with CAS in production)
|
||||
private static readonly ConcurrentDictionary<string, LayerSbomStore> LayerSbomCache = new(StringComparer.Ordinal);
|
||||
|
||||
public LayerSbomService(ICompositionRecipeService? recipeService = null)
|
||||
public LayerSbomService(TimeProvider timeProvider, ICompositionRecipeService? recipeService = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_recipeService = recipeService ?? new CompositionRecipeService();
|
||||
}
|
||||
|
||||
@@ -166,7 +168,7 @@ public sealed class LayerSbomService : ILayerSbomService
|
||||
{
|
||||
ScanId = scanId,
|
||||
ImageDigest = imageDigest,
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture),
|
||||
CreatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
||||
Recipe = new CompositionRecipe
|
||||
{
|
||||
Version = "1.0.0",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
@@ -13,6 +14,13 @@ namespace StellaOps.Scanner.WebService.Services;
|
||||
/// </summary>
|
||||
internal sealed class NullGitHubCodeScanningService : IGitHubCodeScanningService
|
||||
{
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public NullGitHubCodeScanningService(IGuidProvider guidProvider)
|
||||
{
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
public Task<GitHubUploadResult> UploadSarifAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
@@ -24,7 +32,7 @@ internal sealed class NullGitHubCodeScanningService : IGitHubCodeScanningService
|
||||
// Return a mock result for development/testing
|
||||
return Task.FromResult(new GitHubUploadResult
|
||||
{
|
||||
SarifId = $"mock-sarif-{Guid.NewGuid():N}",
|
||||
SarifId = $"mock-sarif-{_guidProvider.NewGuid():N}",
|
||||
Url = null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestation;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
@@ -186,7 +187,7 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier
|
||||
}
|
||||
|
||||
// Compute PAE (Pre-Authentication Encoding) per DSSE spec
|
||||
var pae = ComputePae(envelope.PayloadType, payloadBytes);
|
||||
var pae = DsseHelper.PreAuthenticationEncoding(envelope.PayloadType, payloadBytes);
|
||||
|
||||
// Try to verify at least one signature
|
||||
foreach (var sig in envelope.Signatures)
|
||||
@@ -587,29 +588,6 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ComputePae(string payloadType, byte[] payload)
|
||||
{
|
||||
// Pre-Authentication Encoding per DSSE spec:
|
||||
// PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
|
||||
const string DssePrefix = "DSSEv1";
|
||||
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new BinaryWriter(ms);
|
||||
|
||||
writer.Write(Encoding.UTF8.GetBytes(DssePrefix));
|
||||
writer.Write((byte)' ');
|
||||
writer.Write(BitConverter.GetBytes((long)typeBytes.Length));
|
||||
writer.Write((byte)' ');
|
||||
writer.Write(typeBytes);
|
||||
writer.Write((byte)' ');
|
||||
writer.Write(BitConverter.GetBytes((long)payload.Length));
|
||||
writer.Write((byte)' ');
|
||||
writer.Write(payload);
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static string? ExtractSignerIdentity(X509Certificate2 cert)
|
||||
{
|
||||
// Try to get SAN (Subject Alternative Name) email
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestation;
|
||||
using StellaOps.AirGap.Importer.Contracts;
|
||||
using StellaOps.AirGap.Importer.Validation;
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
@@ -257,7 +258,8 @@ internal sealed class OfflineKitImportService
|
||||
}
|
||||
|
||||
var trustRoots = BuildTrustRoots(resolution, options.TrustRootDirectory ?? string.Empty);
|
||||
var pae = BuildPreAuthEncoding(envelope.PayloadType, envelope.Payload);
|
||||
var payloadBytes = Convert.FromBase64String(envelope.Payload);
|
||||
var pae = DsseHelper.PreAuthenticationEncoding(envelope.PayloadType, payloadBytes);
|
||||
|
||||
var verified = 0;
|
||||
foreach (var signature in envelope.Signatures)
|
||||
@@ -310,26 +312,6 @@ internal sealed class OfflineKitImportService
|
||||
PublicKeys: publicKeys);
|
||||
}
|
||||
|
||||
private static byte[] BuildPreAuthEncoding(string payloadType, string payloadBase64)
|
||||
{
|
||||
const string paePrefix = "DSSEv1";
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64);
|
||||
var parts = new[] { paePrefix, payloadType, Encoding.UTF8.GetString(payloadBytes) };
|
||||
|
||||
var paeBuilder = new StringBuilder();
|
||||
paeBuilder.Append("PAE:");
|
||||
paeBuilder.Append(parts.Length);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
paeBuilder.Append(' ');
|
||||
paeBuilder.Append(part.Length);
|
||||
paeBuilder.Append(' ');
|
||||
paeBuilder.Append(part);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(paeBuilder.ToString());
|
||||
}
|
||||
|
||||
private static bool TryVerifySignature(TrustRootConfig trustRoots, DsseSignature signature, byte[] pae)
|
||||
{
|
||||
if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var keyBytes))
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
using StellaOps.Scanner.Storage.Models;
|
||||
@@ -29,6 +30,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
|
||||
private readonly IPlatformEventPublisher _publisher;
|
||||
private readonly IClassificationChangeTracker _classificationChangeTracker;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ReportEventDispatcher> _logger;
|
||||
private readonly string[] _apiBaseSegments;
|
||||
@@ -43,11 +45,13 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
IPlatformEventPublisher publisher,
|
||||
IClassificationChangeTracker classificationChangeTracker,
|
||||
IOptions<ScannerWebServiceOptions> options,
|
||||
IGuidProvider guidProvider,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ReportEventDispatcher> logger)
|
||||
{
|
||||
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
||||
_classificationChangeTracker = classificationChangeTracker ?? throw new ArgumentNullException(nameof(classificationChangeTracker));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
@@ -102,7 +106,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
|
||||
var reportEvent = new OrchestratorEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = _guidProvider.NewGuid(),
|
||||
Kind = OrchestratorEventKinds.ScannerReportReady,
|
||||
Version = 1,
|
||||
Tenant = tenant,
|
||||
@@ -124,7 +128,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
|
||||
var scanCompletedEvent = new OrchestratorEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventId = _guidProvider.NewGuid(),
|
||||
Kind = OrchestratorEventKinds.ScannerScanCompleted,
|
||||
Version = 1,
|
||||
Tenant = tenant,
|
||||
|
||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
@@ -29,7 +31,8 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
WriteIndented = false,
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
private readonly LinkRepository _linkRepository;
|
||||
@@ -152,7 +155,7 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
Artifacts = orderedArtifacts
|
||||
};
|
||||
|
||||
var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest, ManifestSerializerOptions);
|
||||
var manifestJson = CanonJson.Canonicalize(manifest, ManifestSerializerOptions);
|
||||
var manifestDigest = ComputeDigest(manifestJson);
|
||||
var manifestUri = BuildManifestUri(bucket, rootPrefix, tenant, manifestDigest);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Counterfactuals;
|
||||
using StellaOps.Scanner.Triage.Entities;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
@@ -21,15 +22,18 @@ public sealed class TriageStatusService : ITriageStatusService
|
||||
private readonly ITriageQueryService _queryService;
|
||||
private readonly ICounterfactualEngine? _counterfactualEngine;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public TriageStatusService(
|
||||
ILogger<TriageStatusService> logger,
|
||||
ITriageQueryService queryService,
|
||||
IGuidProvider guidProvider,
|
||||
TimeProvider timeProvider,
|
||||
ICounterfactualEngine? counterfactualEngine = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_queryService = queryService ?? throw new ArgumentNullException(nameof(queryService));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_counterfactualEngine = counterfactualEngine;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ public sealed class TriageStatusService : ITriageStatusService
|
||||
NewLane = newLane,
|
||||
PreviousVerdict = previousVerdict,
|
||||
NewVerdict = newVerdict,
|
||||
SnapshotId = $"snap-{Guid.NewGuid():N}",
|
||||
SnapshotId = $"snap-{_guidProvider.NewGuid():N}",
|
||||
AppliedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
@@ -105,7 +109,7 @@ public sealed class TriageStatusService : ITriageStatusService
|
||||
}
|
||||
|
||||
var previousVerdict = GetCurrentVerdict(finding);
|
||||
var vexStatementId = $"vex-{Guid.NewGuid():N}";
|
||||
var vexStatementId = $"vex-{_guidProvider.NewGuid():N}";
|
||||
|
||||
// Determine if verdict changes based on VEX status
|
||||
var verdictChanged = false;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.BouncyCastle/StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Cache/StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
|
||||
|
||||
11
src/Scanner/StellaOps.Scanner.WebService/TASKS.md
Normal file
11
src/Scanner/StellaOps.Scanner.WebService/TASKS.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Scanner WebService Task Board
|
||||
|
||||
This board tracks TODO/FIXME/HACK markers and audit follow-ups for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| TODO-WEB-001 | TODO | Load tenant-specific policy configuration in `src/Scanner/StellaOps.Scanner.WebService/Services/VexGateQueryService.cs`. |
|
||||
| TODO-WEB-002 | TODO | Implement CAS retrieval for slices in `src/Scanner/StellaOps.Scanner.WebService/Services/SliceQueryService.cs`. |
|
||||
| TODO-WEB-003 | TODO | Add VEX expiry once integrated in `src/Scanner/StellaOps.Scanner.WebService/Services/EvidenceCompositionService.cs`. |
|
||||
| PRAGMA-WEB-001 | DONE | Documented ASPDEPR002 suppressions in `src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs`, `src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs`, and `src/Scanner/StellaOps.Scanner.WebService/Endpoints/EpssEndpoints.cs`. |
|
||||
Reference in New Issue
Block a user