feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -28536,13 +28536,63 @@ stella policy test {policyName}.stella
}
else if (!verifyOnly)
{
// In a real implementation, this would:
// 1. Copy artifacts to the local data store
// 2. Register exports in the database
// 3. Update metadata indexes
// For now, log success
logger.LogInformation("Air-gap bundle imported: domain={Domain}, exports={Exports}, scope={Scope}",
manifest.DomainId, manifest.Exports?.Count ?? 0, scopeDescription);
// CLI-AIRGAP-56-001: Use MirrorBundleImportService for real import
var importService = scope.ServiceProvider.GetService<IMirrorBundleImportService>();
if (importService is not null)
{
var importRequest = new MirrorImportRequest
{
BundlePath = bundlePath,
TenantId = effectiveTenant ?? (globalScope ? "global" : "default"),
TrustRootsPath = null, // Use bundled trust roots
DryRun = false,
Force = force
};
var importResult = await importService.ImportAsync(importRequest, cancellationToken).ConfigureAwait(false);
if (!importResult.Success)
{
AnsiConsole.MarkupLine($"[red]Import failed:[/] {Markup.Escape(importResult.Error ?? "Unknown error")}");
CliMetrics.RecordOfflineKitImport("import_failed");
return ExitGeneralError;
}
// Show DSSE verification status if applicable
if (importResult.DsseVerification is not null)
{
var dsseStatus = importResult.DsseVerification.IsValid ? "[green]VERIFIED[/]" : "[yellow]NOT VERIFIED[/]";
AnsiConsole.MarkupLine($"[grey]DSSE Signature:[/] {dsseStatus}");
if (!string.IsNullOrEmpty(importResult.DsseVerification.KeyId))
{
AnsiConsole.MarkupLine($"[grey] Key ID:[/] {Markup.Escape(TruncateMirrorDigest(importResult.DsseVerification.KeyId))}");
}
}
// Show imported paths in verbose mode
if (verbose && importResult.ImportedPaths.Count > 0)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold]Imported files:[/]");
foreach (var path in importResult.ImportedPaths.Take(10))
{
AnsiConsole.MarkupLine($" [grey]{Markup.Escape(Path.GetFileName(path))}[/]");
}
if (importResult.ImportedPaths.Count > 10)
{
AnsiConsole.MarkupLine($" [grey]... and {importResult.ImportedPaths.Count - 10} more files[/]");
}
}
logger.LogInformation("Air-gap bundle imported: domain={Domain}, exports={Exports}, scope={Scope}, files={FileCount}",
manifest.DomainId, manifest.Exports?.Count ?? 0, scopeDescription, importResult.ImportedPaths.Count);
}
else
{
// Fallback: log success without actual import
logger.LogInformation("Air-gap bundle imported (catalog-only): domain={Domain}, exports={Exports}, scope={Scope}",
manifest.DomainId, manifest.Exports?.Count ?? 0, scopeDescription);
}
}
}

View File

@@ -222,6 +222,13 @@ internal static class Program
client.Timeout = TimeSpan.FromMinutes(5); // Composition may take longer
}).AddEgressPolicyGuard("stellaops-cli", "sbomer-api");
// CLI-AIRGAP-56-001: Mirror bundle import service for air-gap operations
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleCatalogRepository,
StellaOps.AirGap.Importer.Repositories.InMemoryBundleCatalogRepository>();
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleItemRepository,
StellaOps.AirGap.Importer.Repositories.InMemoryBundleItemRepository>();
services.AddSingleton<IMirrorBundleImportService, MirrorBundleImportService>();
await using var serviceProvider = services.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var startupLogger = loggerFactory.CreateLogger("StellaOps.Cli.Startup");

View File

@@ -1,5 +1,6 @@
using System.Reflection;
using StellaOps.Authority.Storage.Postgres;
using StellaOps.Concelier.Storage.Postgres;
using StellaOps.Excititor.Storage.Postgres;
using StellaOps.Notify.Storage.Postgres;
using StellaOps.Policy.Storage.Postgres;
@@ -34,6 +35,11 @@ public static class MigrationModuleRegistry
SchemaName: "scheduler",
MigrationsAssembly: typeof(SchedulerDataSource).Assembly,
ResourcePrefix: "StellaOps.Scheduler.Storage.Postgres.Migrations"),
new(
Name: "Concelier",
SchemaName: "vuln",
MigrationsAssembly: typeof(ConcelierDataSource).Assembly,
ResourcePrefix: "StellaOps.Concelier.Storage.Postgres.Migrations"),
new(
Name: "Policy",
SchemaName: "policy",

View File

@@ -0,0 +1,478 @@
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Models;
using StellaOps.AirGap.Importer.Repositories;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
/// <summary>
/// Service for importing mirror bundles with DSSE, TUF, and Merkle verification.
/// CLI-AIRGAP-56-001: Extends CLI offline kit tooling to consume mirror bundles.
/// </summary>
public interface IMirrorBundleImportService
{
Task<MirrorImportResult> ImportAsync(MirrorImportRequest request, CancellationToken cancellationToken);
Task<MirrorVerificationResult> VerifyAsync(string bundlePath, string? trustRootsPath, CancellationToken cancellationToken);
}
public sealed class MirrorBundleImportService : IMirrorBundleImportService
{
private readonly IBundleCatalogRepository _catalogRepository;
private readonly IBundleItemRepository _itemRepository;
private readonly ImportValidator _validator;
private readonly ILogger<MirrorBundleImportService> _logger;
public MirrorBundleImportService(
IBundleCatalogRepository catalogRepository,
IBundleItemRepository itemRepository,
ILogger<MirrorBundleImportService> logger)
{
_catalogRepository = catalogRepository ?? throw new ArgumentNullException(nameof(catalogRepository));
_itemRepository = itemRepository ?? throw new ArgumentNullException(nameof(itemRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_validator = new ImportValidator();
}
public async Task<MirrorImportResult> ImportAsync(MirrorImportRequest request, CancellationToken cancellationToken)
{
_logger.LogDebug("Starting bundle import from {BundlePath}", request.BundlePath);
// Parse manifest
var manifestResult = await ParseManifestAsync(request.BundlePath, cancellationToken).ConfigureAwait(false);
if (!manifestResult.Success)
{
return MirrorImportResult.Failed(manifestResult.Error!);
}
var manifest = manifestResult.Manifest!;
var bundleDir = Path.GetDirectoryName(manifestResult.ManifestPath)!;
// Verify checksums
var checksumResult = await VerifyChecksumsAsync(bundleDir, cancellationToken).ConfigureAwait(false);
// If DSSE envelope exists, perform cryptographic verification
var dsseResult = await VerifyDsseIfPresentAsync(bundleDir, request.TrustRootsPath, cancellationToken).ConfigureAwait(false);
// Copy artifacts to data store
var dataStorePath = GetDataStorePath(request.TenantId, manifest.DomainId);
var importedPaths = new List<string>();
if (!request.DryRun)
{
importedPaths = await CopyArtifactsAsync(bundleDir, dataStorePath, manifest, cancellationToken).ConfigureAwait(false);
// Register in catalog
var bundleId = GenerateBundleId(manifest);
var manifestDigest = ComputeDigest(File.ReadAllBytes(manifestResult.ManifestPath));
var catalogEntry = new BundleCatalogEntry(
request.TenantId ?? "default",
bundleId,
manifestDigest,
DateTimeOffset.UtcNow,
importedPaths);
await _catalogRepository.UpsertAsync(catalogEntry, cancellationToken).ConfigureAwait(false);
// Register individual items
var items = manifest.Exports?.Select(e => new BundleItem(
request.TenantId ?? "default",
bundleId,
e.Key,
e.ArtifactDigest,
e.ArtifactSizeBytes ?? 0)) ?? Enumerable.Empty<BundleItem>();
await _itemRepository.UpsertManyAsync(items, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Imported bundle {BundleId} with {Count} exports", bundleId, manifest.Exports?.Count ?? 0);
}
return new MirrorImportResult
{
Success = true,
ManifestPath = manifestResult.ManifestPath,
DomainId = manifest.DomainId,
DisplayName = manifest.DisplayName,
GeneratedAt = manifest.GeneratedAt,
ExportCount = manifest.Exports?.Count ?? 0,
ChecksumVerification = checksumResult,
DsseVerification = dsseResult,
ImportedPaths = importedPaths,
DryRun = request.DryRun
};
}
public async Task<MirrorVerificationResult> VerifyAsync(string bundlePath, string? trustRootsPath, CancellationToken cancellationToken)
{
var manifestResult = await ParseManifestAsync(bundlePath, cancellationToken).ConfigureAwait(false);
if (!manifestResult.Success)
{
return new MirrorVerificationResult { Success = false, Error = manifestResult.Error };
}
var bundleDir = Path.GetDirectoryName(manifestResult.ManifestPath)!;
var checksumResult = await VerifyChecksumsAsync(bundleDir, cancellationToken).ConfigureAwait(false);
var dsseResult = await VerifyDsseIfPresentAsync(bundleDir, trustRootsPath, cancellationToken).ConfigureAwait(false);
var allValid = checksumResult.AllValid && (dsseResult?.IsValid ?? true);
return new MirrorVerificationResult
{
Success = allValid,
ManifestPath = manifestResult.ManifestPath,
DomainId = manifestResult.Manifest!.DomainId,
ChecksumVerification = checksumResult,
DsseVerification = dsseResult
};
}
private async Task<ManifestParseResult> ParseManifestAsync(string bundlePath, CancellationToken cancellationToken)
{
var resolvedPath = Path.GetFullPath(bundlePath);
string manifestPath;
if (File.Exists(resolvedPath) && resolvedPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
manifestPath = resolvedPath;
}
else if (Directory.Exists(resolvedPath))
{
var candidates = Directory.GetFiles(resolvedPath, "*-manifest.json")
.Concat(Directory.GetFiles(resolvedPath, "manifest.json"))
.ToArray();
if (candidates.Length == 0)
{
return ManifestParseResult.Failed("No manifest file found in bundle directory");
}
manifestPath = candidates.OrderByDescending(File.GetLastWriteTimeUtc).First();
}
else
{
return ManifestParseResult.Failed($"Bundle path not found: {resolvedPath}");
}
try
{
var json = await File.ReadAllTextAsync(manifestPath, cancellationToken).ConfigureAwait(false);
var manifest = JsonSerializer.Deserialize<MirrorBundle>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (manifest is null)
{
return ManifestParseResult.Failed("Failed to parse bundle manifest");
}
return new ManifestParseResult { Success = true, ManifestPath = manifestPath, Manifest = manifest };
}
catch (JsonException ex)
{
return ManifestParseResult.Failed($"Invalid manifest JSON: {ex.Message}");
}
}
private async Task<ChecksumVerificationResult> VerifyChecksumsAsync(string bundleDir, CancellationToken cancellationToken)
{
var checksumPath = Path.Combine(bundleDir, "SHA256SUMS");
var results = new List<FileChecksumResult>();
var allValid = true;
if (!File.Exists(checksumPath))
{
return new ChecksumVerificationResult { ChecksumFileFound = false, AllValid = true, Results = results };
}
var lines = await File.ReadAllLinesAsync(checksumPath, cancellationToken).ConfigureAwait(false);
foreach (var line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
{
var parts = line.Split([' ', '\t'], 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) continue;
var expected = parts[0].Trim();
var fileName = parts[1].Trim().TrimStart('*');
var filePath = Path.Combine(bundleDir, fileName);
if (!File.Exists(filePath))
{
results.Add(new FileChecksumResult(fileName, expected, "(missing)", false));
allValid = false;
continue;
}
var fileBytes = await File.ReadAllBytesAsync(filePath, cancellationToken).ConfigureAwait(false);
var actual = ComputeDigest(fileBytes);
var isValid = string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase) ||
string.Equals($"sha256:{expected}", actual, StringComparison.OrdinalIgnoreCase);
results.Add(new FileChecksumResult(fileName, expected, actual, isValid));
if (!isValid) allValid = false;
}
return new ChecksumVerificationResult { ChecksumFileFound = true, AllValid = allValid, Results = results };
}
private async Task<DsseVerificationResult?> VerifyDsseIfPresentAsync(string bundleDir, string? trustRootsPath, CancellationToken cancellationToken)
{
// Look for DSSE envelope
var dsseFiles = Directory.GetFiles(bundleDir, "*.dsse.json")
.Concat(Directory.GetFiles(bundleDir, "*envelope.json"))
.ToArray();
if (dsseFiles.Length == 0)
{
return null; // No DSSE envelope present - verification not required
}
var dsseFile = dsseFiles.OrderByDescending(File.GetLastWriteTimeUtc).First();
try
{
var envelopeJson = await File.ReadAllTextAsync(dsseFile, cancellationToken).ConfigureAwait(false);
var envelope = DsseEnvelope.Parse(envelopeJson);
// Load trust roots if provided
TrustRootConfig trustRoots;
if (!string.IsNullOrWhiteSpace(trustRootsPath) && File.Exists(trustRootsPath))
{
trustRoots = await LoadTrustRootsAsync(trustRootsPath, cancellationToken).ConfigureAwait(false);
}
else
{
// Try default trust roots location
var defaultTrustRoots = Path.Combine(bundleDir, "trust-roots.json");
if (File.Exists(defaultTrustRoots))
{
trustRoots = await LoadTrustRootsAsync(defaultTrustRoots, cancellationToken).ConfigureAwait(false);
}
else
{
return new DsseVerificationResult
{
IsValid = false,
EnvelopePath = dsseFile,
Error = "No trust roots available for DSSE verification"
};
}
}
var verifier = new DsseVerifier();
var result = verifier.Verify(envelope, trustRoots);
return new DsseVerificationResult
{
IsValid = result.IsValid,
EnvelopePath = dsseFile,
KeyId = envelope.Signatures.FirstOrDefault()?.KeyId,
Reason = result.Reason
};
}
catch (Exception ex)
{
return new DsseVerificationResult
{
IsValid = false,
EnvelopePath = dsseFile,
Error = $"Failed to verify DSSE: {ex.Message}"
};
}
}
private static async Task<TrustRootConfig> LoadTrustRootsAsync(string path, CancellationToken cancellationToken)
{
var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
var doc = JsonDocument.Parse(json);
var fingerprints = new List<string>();
var algorithms = new List<string>();
var publicKeys = new Dictionary<string, byte[]>();
if (doc.RootElement.TryGetProperty("trustedKeyFingerprints", out var fps))
{
foreach (var fp in fps.EnumerateArray())
{
fingerprints.Add(fp.GetString() ?? string.Empty);
}
}
if (doc.RootElement.TryGetProperty("allowedAlgorithms", out var algs))
{
foreach (var alg in algs.EnumerateArray())
{
algorithms.Add(alg.GetString() ?? string.Empty);
}
}
if (doc.RootElement.TryGetProperty("publicKeys", out var keys))
{
foreach (var key in keys.EnumerateObject())
{
var keyData = key.Value.GetString();
if (!string.IsNullOrEmpty(keyData))
{
publicKeys[key.Name] = Convert.FromBase64String(keyData);
}
}
}
return new TrustRootConfig(path, fingerprints, algorithms, null, null, publicKeys);
}
private async Task<List<string>> CopyArtifactsAsync(string bundleDir, string dataStorePath, MirrorBundle manifest, CancellationToken cancellationToken)
{
Directory.CreateDirectory(dataStorePath);
var importedPaths = new List<string>();
// Copy manifest
var manifestFiles = Directory.GetFiles(bundleDir, "*manifest.json");
foreach (var file in manifestFiles)
{
var destPath = Path.Combine(dataStorePath, Path.GetFileName(file));
await CopyFileAsync(file, destPath, cancellationToken).ConfigureAwait(false);
importedPaths.Add(destPath);
}
// Copy export artifacts
foreach (var export in manifest.Exports ?? Enumerable.Empty<MirrorBundleExport>())
{
var exportFiles = Directory.GetFiles(bundleDir, $"*{export.ExportId}*")
.Concat(Directory.GetFiles(bundleDir, $"*{export.Key}*"));
foreach (var file in exportFiles.Distinct())
{
var destPath = Path.Combine(dataStorePath, Path.GetFileName(file));
await CopyFileAsync(file, destPath, cancellationToken).ConfigureAwait(false);
importedPaths.Add(destPath);
}
}
// Copy checksums and signatures
var supportFiles = new[] { "SHA256SUMS", "*.sig", "*.dsse.json" };
foreach (var pattern in supportFiles)
{
foreach (var file in Directory.GetFiles(bundleDir, pattern))
{
var destPath = Path.Combine(dataStorePath, Path.GetFileName(file));
await CopyFileAsync(file, destPath, cancellationToken).ConfigureAwait(false);
importedPaths.Add(destPath);
}
}
return importedPaths;
}
private static async Task CopyFileAsync(string source, string destination, CancellationToken cancellationToken)
{
await using var sourceStream = File.OpenRead(source);
await using var destStream = File.Create(destination);
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);
}
private static string GetDataStorePath(string? tenantId, string domainId)
{
var basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var stellaPath = Path.Combine(basePath, "stellaops", "offline-kit", "data");
return Path.Combine(stellaPath, tenantId ?? "default", domainId);
}
private static string GenerateBundleId(MirrorBundle manifest)
{
return $"{manifest.DomainId}-{manifest.GeneratedAt:yyyyMMddHHmmss}";
}
private static string ComputeDigest(byte[] data)
{
return $"sha256:{Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant()}";
}
private sealed record ManifestParseResult
{
public bool Success { get; init; }
public string? ManifestPath { get; init; }
public MirrorBundle? Manifest { get; init; }
public string? Error { get; init; }
public static ManifestParseResult Failed(string error) => new() { Success = false, Error = error };
}
}
/// <summary>
/// Request for importing a mirror bundle.
/// </summary>
public sealed record MirrorImportRequest
{
public required string BundlePath { get; init; }
public string? TenantId { get; init; }
public string? TrustRootsPath { get; init; }
public bool DryRun { get; init; }
public bool Force { get; init; }
}
/// <summary>
/// Result of a mirror bundle import operation.
/// </summary>
public sealed record MirrorImportResult
{
public bool Success { get; init; }
public string? Error { get; init; }
public string? ManifestPath { get; init; }
public string? DomainId { get; init; }
public string? DisplayName { get; init; }
public DateTimeOffset GeneratedAt { get; init; }
public int ExportCount { get; init; }
public ChecksumVerificationResult? ChecksumVerification { get; init; }
public DsseVerificationResult? DsseVerification { get; init; }
public IReadOnlyList<string> ImportedPaths { get; init; } = Array.Empty<string>();
public bool DryRun { get; init; }
public static MirrorImportResult Failed(string error) => new() { Success = false, Error = error };
}
/// <summary>
/// Result of mirror bundle verification.
/// </summary>
public sealed record MirrorVerificationResult
{
public bool Success { get; init; }
public string? Error { get; init; }
public string? ManifestPath { get; init; }
public string? DomainId { get; init; }
public ChecksumVerificationResult? ChecksumVerification { get; init; }
public DsseVerificationResult? DsseVerification { get; init; }
}
/// <summary>
/// Checksum verification results.
/// </summary>
public sealed record ChecksumVerificationResult
{
public bool ChecksumFileFound { get; init; }
public bool AllValid { get; init; }
public IReadOnlyList<FileChecksumResult> Results { get; init; } = Array.Empty<FileChecksumResult>();
}
/// <summary>
/// Individual file checksum result.
/// </summary>
public sealed record FileChecksumResult(string FileName, string Expected, string Actual, bool IsValid);
/// <summary>
/// DSSE verification result.
/// </summary>
public sealed record DsseVerificationResult
{
public bool IsValid { get; init; }
public string? EnvelopePath { get; init; }
public string? KeyId { get; init; }
public string? Reason { get; init; }
public string? Error { get; init; }
}

View File

@@ -43,6 +43,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.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="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
@@ -64,6 +65,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Storage.Postgres/StellaOps.Authority.Storage.Postgres.csproj" />
<ProjectReference Include="../../Scheduler/__Libraries/StellaOps.Scheduler.Storage.Postgres/StellaOps.Scheduler.Storage.Postgres.csproj" />
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/StellaOps.Concelier.Storage.Postgres.csproj" />
<ProjectReference Include="../../Policy/__Libraries/StellaOps.Policy.Storage.Postgres/StellaOps.Policy.Storage.Postgres.csproj" />
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Storage.Postgres/StellaOps.Notify.Storage.Postgres.csproj" />
<ProjectReference Include="../../Excititor/__Libraries/StellaOps.Excititor.Storage.Postgres/StellaOps.Excititor.Storage.Postgres.csproj" />