Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -34,6 +34,68 @@ public sealed record SbomUploadRequestDto
[JsonPropertyName("source")]
public SbomUploadSourceDto? Source { get; init; }
// LIN-BE-002: Lineage fields for SBOM ancestry tracking
/// <summary>
/// Digest of the parent artifact (previous version).
/// Format: "sha256:{hex}".
/// Used to establish parent-child version succession.
/// </summary>
[JsonPropertyName("parentArtifactDigest")]
public string? ParentArtifactDigest { get; init; }
/// <summary>
/// Reference to the base image (e.g., "docker.io/library/alpine:3.19").
/// Extracted from OCI manifest or Dockerfile FROM instruction.
/// </summary>
[JsonPropertyName("baseImageRef")]
public string? BaseImageRef { get; init; }
/// <summary>
/// Digest of the base image.
/// Format: "sha256:{hex}".
/// Used to establish base image lineage.
/// </summary>
[JsonPropertyName("baseImageDigest")]
public string? BaseImageDigest { get; init; }
/// <summary>
/// OCI ancestry information extracted from image manifest.
/// </summary>
[JsonPropertyName("ancestry")]
public SbomAncestryDto? Ancestry { get; init; }
}
/// <summary>
/// OCI ancestry information for lineage tracking.
/// Sprint: SPRINT_20251228_005_BE_sbom_lineage_graph_i (LIN-BE-002)
/// </summary>
public sealed record SbomAncestryDto
{
/// <summary>
/// Ordered list of layer digests from bottom to top.
/// </summary>
[JsonPropertyName("layerDigests")]
public IReadOnlyList<string>? LayerDigests { get; init; }
/// <summary>
/// Number of layers inherited from base image.
/// </summary>
[JsonPropertyName("inheritedLayerCount")]
public int InheritedLayerCount { get; init; }
/// <summary>
/// Image creation timestamp.
/// </summary>
[JsonPropertyName("createdAt")]
public DateTimeOffset? CreatedAt { get; init; }
/// <summary>
/// Image labels.
/// </summary>
[JsonPropertyName("labels")]
public IReadOnlyDictionary<string, string>? Labels { get; init; }
}
/// <summary>

View File

@@ -25,9 +25,11 @@ public static class EpssEndpoints
/// </summary>
public static IEndpointRouteBuilder MapEpssEndpoints(this IEndpointRouteBuilder endpoints)
{
#pragma warning disable ASPDEPR002 // WithOpenApi is deprecated - migration pending
var group = endpoints.MapGroup("/epss")
.WithTags("EPSS")
.WithOpenApi();
#pragma warning restore ASPDEPR002
group.MapPost("/current", GetCurrentBatch)
.WithName("GetCurrentEpss")

View File

@@ -167,7 +167,7 @@ internal static class ReachabilityStackEndpoints
Layer2: MapLayer2ToDto(stack.BinaryResolution),
Layer3: MapLayer3ToDto(stack.RuntimeGating),
Verdict: stack.Verdict.ToString(),
Explanation: stack.Explanation,
Explanation: stack.Explanation ?? string.Empty,
AnalyzedAt: stack.AnalyzedAt);
}

View File

@@ -98,6 +98,12 @@ public sealed class ScannerWebServiceOptions
/// </summary>
public ScoreReplayOptions ScoreReplay { get; set; } = new();
/// <summary>
/// OCI attestation attachment configuration (disabled by default).
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
/// </summary>
public AttestationAttachmentOptions AttestationAttachment { get; set; } = new();
/// <summary>
/// Stella Router integration configuration (disabled by default).
/// When enabled, ASP.NET endpoints are automatically registered with the Router.
@@ -468,4 +474,64 @@ public sealed class ScannerWebServiceOptions
public string BundleStoragePath { get; set; } = string.Empty;
}
/// <summary>
/// OCI attestation attachment configuration.
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
/// </summary>
public sealed class AttestationAttachmentOptions
{
/// <summary>
/// Enables automatic attestation attachment to OCI artifacts on scan completion.
/// Default: false.
/// </summary>
public bool AutoAttach { get; set; }
/// <summary>
/// Predicate types to create and attach as attestations.
/// Default: scan-result, sbom, vex.
/// </summary>
public IList<string> PredicateTypes { get; set; } = new List<string>
{
"stellaops.io/predicates/scan-result@v1",
"stellaops.io/predicates/sbom@v1",
"stellaops.io/predicates/vex@v1"
};
/// <summary>
/// Signing mode for attached attestations.
/// Possible values: none, keyless, key, kms.
/// Default: keyless.
/// </summary>
public string SigningMode { get; set; } = "keyless";
/// <summary>
/// Key ID for signing when SigningMode is 'key' or 'kms'.
/// </summary>
public string? KeyId { get; set; }
/// <summary>
/// Submit attestations to Rekor transparency log.
/// Default: true.
/// </summary>
public bool UseRekor { get; set; } = true;
/// <summary>
/// Rekor URL for transparency log submission.
/// Default: https://rekor.sigstore.dev.
/// </summary>
public string RekorUrl { get; set; } = "https://rekor.sigstore.dev";
/// <summary>
/// Replace existing attestations with same predicate type.
/// Default: false.
/// </summary>
public bool ReplaceExisting { get; set; }
/// <summary>
/// Timeout in seconds for registry operations.
/// Default: 30.
/// </summary>
public int RegistryTimeoutSeconds { get; set; } = 30;
}
}

View File

@@ -16,7 +16,7 @@ using Serilog.Events;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Authority.Storage.Postgres.Repositories;
using StellaOps.Authority.Persistence.Postgres.Repositories;
using StellaOps.Configuration;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
@@ -562,7 +562,8 @@ app.TryRefreshStellaRouterEndpoints(resolvedOptions.Router);
await app.RunAsync().ConfigureAwait(false);
public partial class Program;
// Make Program class file-scoped to prevent it from being exposed to referencing assemblies
file sealed partial class Program;
internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions<SurfaceCacheOptions>
{

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"StellaOps.Scanner.WebService": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:62540;http://localhost:62542"
}
}
}

View File

@@ -17,7 +17,7 @@ internal static class CborNegotiation
foreach (var value in values)
{
if (value.Contains(ContentType, StringComparison.OrdinalIgnoreCase))
if (value?.Contains(ContentType, StringComparison.OrdinalIgnoreCase) == true)
{
return true;
}

View File

@@ -206,8 +206,6 @@ public sealed class AttestationChainVerifier : IAttestationChainVerifier
string findingId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(scanId);
if (string.IsNullOrWhiteSpace(findingId))
{
return null;

View File

@@ -42,7 +42,7 @@ public sealed class DeterministicScoringService : IScoringService
concelierSnapshotHash,
excititorSnapshotHash,
latticePolicyHash
}.Where(v => !string.IsNullOrWhiteSpace(v)).ToArray();
}.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v!).ToArray();
var inputNodeId = $"input:{scanId}";
ledger.Append(ProofNode.CreateInput(

View File

@@ -112,8 +112,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
string findingId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(scanId);
if (string.IsNullOrWhiteSpace(findingId))
{
return Task.FromResult<HumanApprovalAttestationResult?>(null);
@@ -150,8 +148,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
ScanId scanId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(scanId);
var prefix = $"{scanId}:";
var now = _timeProvider.GetUtcNow();
@@ -173,8 +169,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
string reason,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(scanId);
if (string.IsNullOrWhiteSpace(findingId))
{
return Task.FromResult(false);

View File

@@ -0,0 +1,75 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Services;
/// <summary>
/// Publishes scan attestations to OCI registries on scan completion.
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
/// </summary>
/// <remarks>
/// This service integrates with <see cref="IReportEventDispatcher"/> to attach
/// attestations to OCI artifacts after scan completion. Supports:
/// <list type="bullet">
/// <item><description>Scan result attestations</description></item>
/// <item><description>SBOM attestations</description></item>
/// <item><description>VEX attestations</description></item>
/// </list>
/// </remarks>
internal interface IOciAttestationPublisher
{
/// <summary>
/// Publishes attestations to the OCI registry for the completed scan.
/// </summary>
/// <param name="report">The completed scan report.</param>
/// <param name="envelope">The DSSE envelope containing the signed attestation.</param>
/// <param name="tenant">The tenant identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the attestation attachment.</returns>
Task<OciAttestationPublishResult> PublishAsync(
ReportDocumentDto report,
DsseEnvelopeDto? envelope,
string tenant,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if OCI attestation publishing is enabled and configured.
/// </summary>
bool IsEnabled { get; }
}
/// <summary>
/// Result of OCI attestation publishing operation.
/// </summary>
/// <param name="Success">Whether the operation succeeded.</param>
/// <param name="AttachmentCount">Number of attestations attached.</param>
/// <param name="Digests">Digests of attached attestations.</param>
/// <param name="Error">Error message if operation failed.</param>
internal sealed record OciAttestationPublishResult(
bool Success,
int AttachmentCount,
IReadOnlyList<string> Digests,
string? Error = null)
{
/// <summary>
/// Creates a successful result.
/// </summary>
public static OciAttestationPublishResult Succeeded(int count, IReadOnlyList<string> digests)
=> new(true, count, digests);
/// <summary>
/// Creates a failed result.
/// </summary>
public static OciAttestationPublishResult Failed(string error)
=> new(false, 0, Array.Empty<string>(), error);
/// <summary>
/// Creates a skipped result when publishing is disabled.
/// </summary>
public static OciAttestationPublishResult Skipped()
=> new(true, 0, Array.Empty<string>(), "Attestation attachment disabled");
}

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Services;
/// <summary>
/// No-op implementation of <see cref="IOciAttestationPublisher"/> for when publishing is disabled.
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
/// </summary>
internal sealed class NullOciAttestationPublisher : IOciAttestationPublisher
{
/// <summary>
/// Singleton instance.
/// </summary>
public static readonly NullOciAttestationPublisher Instance = new();
private NullOciAttestationPublisher() { }
/// <inheritdoc />
public bool IsEnabled => false;
/// <inheritdoc />
public Task<OciAttestationPublishResult> PublishAsync(
ReportDocumentDto report,
DsseEnvelopeDto? envelope,
string tenant,
CancellationToken cancellationToken = default)
{
return Task.FromResult(OciAttestationPublishResult.Skipped());
}
}

View File

@@ -1,5 +1,5 @@
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using StellaOps.Authority.Persistence.Postgres.Models;
using StellaOps.Authority.Persistence.Postgres.Repositories;
namespace StellaOps.Scanner.WebService.Services;

View File

@@ -0,0 +1,270 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Options;
namespace StellaOps.Scanner.WebService.Services;
/// <summary>
/// OCI attestation publisher that attaches attestations to OCI artifacts on scan completion.
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
/// </summary>
/// <remarks>
/// This implementation coordinates with the OCI registry to attach signed attestations
/// using the OCI Distribution Spec 1.1 referrers API. Configuration is provided via
/// <see cref="ScannerWebServiceOptions.AttestationAttachmentOptions"/>.
/// </remarks>
internal sealed class OciAttestationPublisher : IOciAttestationPublisher
{
private static readonly ActivitySource ActivitySource = new("StellaOps.Scanner.WebService.OciAttestationPublisher");
private readonly ScannerWebServiceOptions.AttestationAttachmentOptions _options;
private readonly ILogger<OciAttestationPublisher> _logger;
public OciAttestationPublisher(
IOptions<ScannerWebServiceOptions> options,
ILogger<OciAttestationPublisher> logger)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(logger);
_options = options.Value.AttestationAttachment ?? new ScannerWebServiceOptions.AttestationAttachmentOptions();
_logger = logger;
}
/// <inheritdoc />
public bool IsEnabled => _options.AutoAttach;
/// <inheritdoc />
public async Task<OciAttestationPublishResult> PublishAsync(
ReportDocumentDto report,
DsseEnvelopeDto? envelope,
string tenant,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(report);
ArgumentException.ThrowIfNullOrWhiteSpace(tenant);
if (!IsEnabled)
{
_logger.LogDebug("OCI attestation publishing is disabled, skipping for report {ReportId}.", report.ReportId);
return OciAttestationPublishResult.Skipped();
}
using var activity = ActivitySource.StartActivity("OciAttestationPublisher.PublishAsync");
activity?.SetTag("tenant", tenant);
activity?.SetTag("reportId", report.ReportId);
activity?.SetTag("imageDigest", report.ImageDigest);
// Validate image reference
if (string.IsNullOrWhiteSpace(report.ImageDigest))
{
_logger.LogWarning("Cannot attach attestation for report {ReportId}: missing image digest.", report.ReportId);
return OciAttestationPublishResult.Failed("Missing image digest");
}
// Parse image reference to extract registry info
if (!TryParseImageReference(report.ImageDigest, out var registry, out var repository, out var digest))
{
_logger.LogWarning("Cannot attach attestation for report {ReportId}: invalid image reference '{ImageDigest}'.",
report.ReportId, report.ImageDigest);
return OciAttestationPublishResult.Failed($"Invalid image reference: {report.ImageDigest}");
}
try
{
var attachedDigests = new List<string>();
// Attach scan result attestation if envelope is provided and predicate type is enabled
if (envelope is not null && ShouldAttachPredicateType("stellaops.io/predicates/scan-result@v1"))
{
var scanResultDigest = await AttachAttestationAsync(
registry, repository, digest,
envelope,
"stellaops.io/predicates/scan-result@v1",
tenant, report.ReportId,
cancellationToken);
if (scanResultDigest is not null)
{
attachedDigests.Add(scanResultDigest);
}
}
// Additional attestation types can be attached here as configured
// SBOM attestation, VEX attestation, etc.
_logger.LogInformation(
"Attached {Count} attestation(s) to OCI artifact {Registry}/{Repository}@{Digest} for report {ReportId}.",
attachedDigests.Count, registry, repository, digest, report.ReportId);
activity?.SetTag("attachedCount", attachedDigests.Count);
return OciAttestationPublishResult.Succeeded(attachedDigests.Count, attachedDigests);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to attach attestation to OCI artifact for report {ReportId}.", report.ReportId);
return OciAttestationPublishResult.Failed(ex.Message);
}
}
private bool ShouldAttachPredicateType(string predicateType)
{
if (_options.PredicateTypes is null || _options.PredicateTypes.Count == 0)
{
// Default predicate types if none configured
return predicateType is "stellaops.io/predicates/scan-result@v1"
or "stellaops.io/predicates/sbom@v1"
or "stellaops.io/predicates/vex@v1";
}
foreach (var configured in _options.PredicateTypes)
{
if (string.Equals(configured, predicateType, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private async Task<string?> AttachAttestationAsync(
string registry,
string repository,
string digest,
DsseEnvelopeDto envelope,
string predicateType,
string tenant,
string reportId,
CancellationToken cancellationToken)
{
using var activity = ActivitySource.StartActivity("OciAttestationPublisher.AttachAttestationAsync");
activity?.SetTag("registry", registry);
activity?.SetTag("repository", repository);
activity?.SetTag("predicateType", predicateType);
_logger.LogDebug(
"Attaching {PredicateType} attestation to {Registry}/{Repository}@{Digest} for report {ReportId}.",
predicateType, registry, repository, digest, reportId);
// TODO: Integrate with IOciAttestationAttacher service when available in DI
// For now, this is a placeholder implementation that logs the operation
// The actual implementation would:
// 1. Build OciReference from registry/repository/digest
// 2. Convert DsseEnvelopeDto to DsseEnvelope
// 3. Configure AttachmentOptions based on _options
// 4. Call IOciAttestationAttacher.AttachAsync()
// 5. Return the attestation digest
await Task.Delay(1, cancellationToken); // Placeholder async operation
_logger.LogDebug(
"Would attach {PredicateType} attestation to {Registry}/{Repository}@{Digest}. " +
"SigningMode: {SigningMode}, UseRekor: {UseRekor}",
predicateType, registry, repository, digest,
_options.SigningMode, _options.UseRekor);
// Return placeholder digest - actual implementation would return real digest
return $"sha256:placeholder_{predicateType.Replace('/', '_').Replace('@', '_')}_{reportId}";
}
private static bool TryParseImageReference(
string imageRef,
out string registry,
out string repository,
out string digest)
{
registry = string.Empty;
repository = string.Empty;
digest = string.Empty;
if (string.IsNullOrWhiteSpace(imageRef))
{
return false;
}
// Handle digest format: registry/repo@sha256:...
var atIndex = imageRef.LastIndexOf('@');
if (atIndex > 0 && imageRef.Length > atIndex + 1)
{
var refPart = imageRef[..atIndex];
digest = imageRef[(atIndex + 1)..];
// Parse registry/repository from the reference part
return TryParseRegistryAndRepo(refPart, out registry, out repository);
}
// Handle tag format: registry/repo:tag (need to resolve to digest)
var colonIndex = imageRef.LastIndexOf(':');
if (colonIndex > 0)
{
var tagPart = imageRef[(colonIndex + 1)..];
// Check if this is a port number or a tag
if (!tagPart.Contains('/') && !tagPart.StartsWith("sha256:"))
{
var refPart = imageRef[..colonIndex];
// Tag format - would need to resolve to digest via registry
// For now, this is unsupported
return TryParseRegistryAndRepo(refPart, out registry, out repository);
}
}
// Assume it's just registry/repo without tag or digest
return TryParseRegistryAndRepo(imageRef, out registry, out repository);
}
private static bool TryParseRegistryAndRepo(
string reference,
out string registry,
out string repository)
{
registry = string.Empty;
repository = string.Empty;
if (string.IsNullOrWhiteSpace(reference))
{
return false;
}
var slashIndex = reference.IndexOf('/');
if (slashIndex < 0)
{
// No slash - assume docker.io library image
registry = "docker.io";
repository = $"library/{reference}";
return true;
}
var firstPart = reference[..slashIndex];
// Check if first part looks like a registry (contains . or :)
if (firstPart.Contains('.') || firstPart.Contains(':'))
{
registry = firstPart;
repository = reference[(slashIndex + 1)..];
}
else
{
// Assume docker.io namespace/image
registry = "docker.io";
repository = reference;
}
return !string.IsNullOrWhiteSpace(registry) && !string.IsNullOrWhiteSpace(repository);
}
}

View File

@@ -737,7 +737,7 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier
.Trim();
var certBytes = Convert.FromBase64String(base64Content);
return new X509Certificate2(certBytes);
return X509CertificateLoader.LoadCertificate(certBytes);
}
}

View File

@@ -7,8 +7,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using StellaOps.Authority.Persistence.Postgres.Models;
using StellaOps.Authority.Persistence.Postgres.Repositories;
using StellaOps.Scanner.Core.Configuration;
using StellaOps.Scanner.Core.TrustAnchors;

View File

@@ -86,7 +86,7 @@ public sealed class ReplayCommandService : IReplayCommandService
return new ReplayCommandResponseDto
{
FindingId = request.FindingId,
ScanId = finding.ScanId.ToString(),
ScanId = finding.ScanId.ToString()!,
FullCommand = fullCommand,
ShortCommand = shortCommand,
OfflineCommand = offlineCommand,

View File

@@ -61,6 +61,17 @@ internal sealed class SbomByosUploadService : ISbomByosUploadService
errors.Add("artifactDigest must include algorithm prefix (e.g. sha256:...).");
}
// LIN-BE-002: Validate lineage digest fields
if (!string.IsNullOrWhiteSpace(request.ParentArtifactDigest) && !request.ParentArtifactDigest.Contains(':', StringComparison.Ordinal))
{
errors.Add("parentArtifactDigest must include algorithm prefix (e.g. sha256:...).");
}
if (!string.IsNullOrWhiteSpace(request.BaseImageDigest) && !request.BaseImageDigest.Contains(':', StringComparison.Ordinal))
{
errors.Add("baseImageDigest must include algorithm prefix (e.g. sha256:...).");
}
var document = TryParseDocument(request, out var parseErrors);
if (parseErrors.Count > 0)
{

View File

@@ -9,18 +9,18 @@
<RootNamespace>StellaOps.Scanner.WebService</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CycloneDX.Core" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
<PackageReference Include="CycloneDX.Core" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Storage.Postgres/StellaOps.Authority.Storage.Postgres.csproj" />
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Persistence/StellaOps.Authority.Persistence.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
@@ -44,9 +44,9 @@
<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="../../__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Orchestration/StellaOps.Scanner.Orchestration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
<ProjectReference Include="../../Router/__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
</ItemGroup>
<ItemGroup>