notify doctors work, audit work, new product advisory sprints

This commit is contained in:
master
2026-01-13 08:36:29 +02:00
parent b8868a5f13
commit 9ca7cb183e
343 changed files with 24492 additions and 3544 deletions

View File

@@ -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;

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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",

View File

@@ -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
});
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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" />

View 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`. |