Centralize Postgres connection string policy across all modules

Extract connection string building into PostgresConnectionStringPolicy so all
services use consistent pooling, application_name, and timeout settings.
Adopt the new policy in 20+ module DataSource/ServiceCollectionExtensions classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-06 08:51:04 +03:00
parent 517fa0a92d
commit ccdfd41e4f
64 changed files with 625 additions and 178 deletions

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Rekor checkpoint PostgreSQL store and aligned it with reusable runtime data-source conventions. |
| AUDIT-0049-M | DONE | Revalidated maintainability for StellaOps.Attestor.Core. | | AUDIT-0049-M | DONE | Revalidated maintainability for StellaOps.Attestor.Core. |
| AUDIT-0049-T | DONE | Revalidated test coverage for StellaOps.Attestor.Core. | | AUDIT-0049-T | DONE | Revalidated test coverage for StellaOps.Attestor.Core. |
| AUDIT-0049-A | TODO | Reopened on revalidation; address canonicalization, time/ID determinism, and Ed25519 gaps. | | AUDIT-0049-A | TODO | Reopened on revalidation; address canonicalization, time/ID determinism, and Ed25519 gaps. |

View File

@@ -180,7 +180,9 @@ public static class ServiceCollectionExtensions
throw new InvalidOperationException("Redis connection string is required when redis dedupe is enabled."); throw new InvalidOperationException("Redis connection string is required when redis dedupe is enabled.");
} }
return ConnectionMultiplexer.Connect(options.Redis.Url); var redisOptions = ConfigurationOptions.Parse(options.Redis.Url);
redisOptions.ClientName ??= "stellaops-attestor-dedupe";
return ConnectionMultiplexer.Connect(redisOptions);
}); });
services.AddSingleton<IAttestorArchiveStore>(sp => services.AddSingleton<IAttestorArchiveStore>(sp =>

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Attestor Redis dedupe client construction path. |
| AUDIT-0055-M | DONE | Revalidated 2026-01-06. | | AUDIT-0055-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-T | DONE | Revalidated 2026-01-06. | | AUDIT-0055-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-A | DONE | Applied determinism, backend resolver, and Rekor client fixes 2026-01-08. | | AUDIT-0055-A | DONE | Applied determinism, backend resolver, and Rekor client fixes 2026-01-08. |

View File

@@ -19,9 +19,9 @@ namespace StellaOps.Attestor.Core.Rekor;
/// <summary> /// <summary>
/// PostgreSQL implementation of the Rekor checkpoint store. /// PostgreSQL implementation of the Rekor checkpoint store.
/// </summary> /// </summary>
public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly PostgresCheckpointStoreOptions _options; private readonly PostgresCheckpointStoreOptions _options;
private readonly ILogger<PostgresRekorCheckpointStore> _logger; private readonly ILogger<PostgresRekorCheckpointStore> _logger;
@@ -30,8 +30,9 @@ public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore
ILogger<PostgresRekorCheckpointStore> logger) ILogger<PostgresRekorCheckpointStore> logger)
{ {
_options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_connectionString = _options.ConnectionString var connectionString = _options.ConnectionString
?? throw new InvalidOperationException("ConnectionString is required"); ?? throw new InvalidOperationException("ConnectionString is required");
_dataSource = CreateDataSource(connectionString);
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -285,11 +286,23 @@ public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore
private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken) private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
{ {
var conn = new NpgsqlConnection(_connectionString); return await _dataSource.OpenConnectionAsync(cancellationToken);
await conn.OpenAsync(cancellationToken);
return conn;
} }
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-rekor-checkpoint"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static StoredCheckpoint MapCheckpoint(NpgsqlDataReader reader) private static StoredCheckpoint MapCheckpoint(NpgsqlDataReader reader)
{ {
return new StoredCheckpoint return new StoredCheckpoint

View File

@@ -15,9 +15,9 @@ namespace StellaOps.Attestor.Persistence.Repositories;
/// EF Core implementation of the predicate type registry repository. /// EF Core implementation of the predicate type registry repository.
/// Preserves idempotent registration via ON CONFLICT DO NOTHING semantics. /// Preserves idempotent registration via ON CONFLICT DO NOTHING semantics.
/// </summary> /// </summary>
public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegistryRepository public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegistryRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly string _schemaName; private readonly string _schemaName;
private const int DefaultCommandTimeoutSeconds = 30; private const int DefaultCommandTimeoutSeconds = 30;
@@ -26,7 +26,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
/// </summary> /// </summary>
public PostgresPredicateTypeRegistryRepository(string connectionString, string? schemaName = null) public PostgresPredicateTypeRegistryRepository(string connectionString, string? schemaName = null)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _dataSource = CreateDataSource(connectionString);
_schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName; _schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName;
} }
@@ -38,8 +38,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
int limit = 100, int limit = 100,
CancellationToken ct = default) CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
var query = dbContext.PredicateTypeRegistry.AsNoTracking().AsQueryable(); var query = dbContext.PredicateTypeRegistry.AsNoTracking().AsQueryable();
@@ -67,8 +66,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
string predicateTypeUri, string predicateTypeUri,
CancellationToken ct = default) CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.PredicateTypeRegistry return await dbContext.PredicateTypeRegistry
@@ -85,8 +83,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
{ {
ArgumentNullException.ThrowIfNull(entry); ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
dbContext.PredicateTypeRegistry.Add(entry); dbContext.PredicateTypeRegistry.Add(entry);
@@ -115,4 +112,18 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
} }
return false; return false;
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-predicate-registry"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -16,9 +16,9 @@ namespace StellaOps.Attestor.Persistence.Repositories;
/// EF Core implementation of the verdict ledger repository. /// EF Core implementation of the verdict ledger repository.
/// Enforces append-only semantics with hash chain validation. /// Enforces append-only semantics with hash chain validation.
/// </summary> /// </summary>
public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly string _schemaName; private readonly string _schemaName;
private const int DefaultCommandTimeoutSeconds = 30; private const int DefaultCommandTimeoutSeconds = 30;
@@ -27,7 +27,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// </summary> /// </summary>
public PostgresVerdictLedgerRepository(string connectionString, string? schemaName = null) public PostgresVerdictLedgerRepository(string connectionString, string? schemaName = null)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _dataSource = CreateDataSource(connectionString);
_schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName; _schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName;
} }
@@ -47,8 +47,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
// Use raw SQL for the INSERT with RETURNING and enum cast, which is cleaner // Use raw SQL for the INSERT with RETURNING and enum cast, which is cleaner
// for the verdict_decision custom enum type // for the verdict_decision custom enum type
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
dbContext.VerdictLedger.Add(entry); dbContext.VerdictLedger.Add(entry);
@@ -74,8 +73,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetByHashAsync(string verdictHash, CancellationToken ct = default) public async Task<VerdictLedgerEntry?> GetByHashAsync(string verdictHash, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger return await dbContext.VerdictLedger
@@ -89,8 +87,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
Guid tenantId, Guid tenantId,
CancellationToken ct = default) CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger return await dbContext.VerdictLedger
@@ -103,8 +100,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetLatestAsync(Guid tenantId, CancellationToken ct = default) public async Task<VerdictLedgerEntry?> GetLatestAsync(Guid tenantId, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger return await dbContext.VerdictLedger
@@ -151,8 +147,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<long> CountAsync(Guid tenantId, CancellationToken ct = default) public async Task<long> CountAsync(Guid tenantId, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName); await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger return await dbContext.VerdictLedger
@@ -172,4 +167,18 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
} }
return false; return false;
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-verdict-ledger"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -5,6 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20260222_092_Attestor_dal_to_efcore.md`.
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named reusable PostgreSQL data sources for verdict-ledger and predicate-registry runtime repositories. |
| AUDIT-0060-M | DONE | Revalidated 2026-01-06. | | AUDIT-0060-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-T | DONE | Revalidated 2026-01-06. | | AUDIT-0060-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-A | TODO | Reopened after revalidation 2026-01-06. | | AUDIT-0060-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -13,8 +13,7 @@ public sealed partial class PostgresAlertDedupRepository
int dedupWindowMinutes, int dedupWindowMinutes,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var now = _timeProvider.GetUtcNow(); var now = _timeProvider.GetUtcNow();
var windowStart = now.AddMinutes(-dedupWindowMinutes); var windowStart = now.AddMinutes(-dedupWindowMinutes);

View File

@@ -8,15 +8,15 @@ namespace StellaOps.Attestor.Watchlist.Storage;
/// <summary> /// <summary>
/// PostgreSQL implementation of the alert deduplication repository. /// PostgreSQL implementation of the alert deduplication repository.
/// </summary> /// </summary>
public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresAlertDedupRepository> _logger; private readonly ILogger<PostgresAlertDedupRepository> _logger;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
public PostgresAlertDedupRepository(string connectionString, ILogger<PostgresAlertDedupRepository> logger, TimeProvider? timeProvider = null) public PostgresAlertDedupRepository(string connectionString, ILogger<PostgresAlertDedupRepository> logger, TimeProvider? timeProvider = null)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _dataSource = CreateDataSource(connectionString);
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
} }
@@ -27,8 +27,7 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
string identityHash, string identityHash,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
const string sql = @" const string sql = @"
SELECT alert_count FROM attestor.identity_alert_dedup SELECT alert_count FROM attestor.identity_alert_dedup
@@ -45,8 +44,7 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> CleanupExpiredAsync(CancellationToken cancellationToken = default) public async Task<int> CleanupExpiredAsync(CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var cutoff = _timeProvider.GetUtcNow().AddDays(-7); var cutoff = _timeProvider.GetUtcNow().AddDays(-7);
const string sql = @" const string sql = @"
@@ -57,4 +55,18 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
cmd.Parameters.AddWithValue("cutoff", cutoff); cmd.Parameters.AddWithValue("cutoff", cutoff);
return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-alert-dedup"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -14,8 +14,7 @@ public sealed partial class PostgresWatchlistRepository
bool includeGlobal = true, bool includeGlobal = true,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var sql = includeGlobal var sql = includeGlobal
? SqlQueries.SelectByTenantIncludingGlobal ? SqlQueries.SelectByTenantIncludingGlobal
@@ -46,8 +45,7 @@ public sealed partial class PostgresWatchlistRepository
return cached; return cached;
} }
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.SelectActiveByTenant, conn); await using var cmd = new NpgsqlCommand(SqlQueries.SelectActiveByTenant, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId); cmd.Parameters.AddWithValue("tenant_id", tenantId);

View File

@@ -13,8 +13,7 @@ public sealed partial class PostgresWatchlistRepository
{ {
ArgumentNullException.ThrowIfNull(entry); ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.Upsert, conn); await using var cmd = new NpgsqlCommand(SqlQueries.Upsert, conn);
AddUpsertParameters(cmd, entry); AddUpsertParameters(cmd, entry);
@@ -38,8 +37,7 @@ public sealed partial class PostgresWatchlistRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> DeleteAsync(Guid id, string tenantId, CancellationToken cancellationToken = default) public async Task<bool> DeleteAsync(Guid id, string tenantId, CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.Delete, conn); await using var cmd = new NpgsqlCommand(SqlQueries.Delete, conn);
cmd.Parameters.AddWithValue("id", id); cmd.Parameters.AddWithValue("id", id);

View File

@@ -11,9 +11,9 @@ namespace StellaOps.Attestor.Watchlist.Storage;
/// <summary> /// <summary>
/// PostgreSQL implementation of the watchlist repository with caching. /// PostgreSQL implementation of the watchlist repository with caching.
/// </summary> /// </summary>
public sealed partial class PostgresWatchlistRepository : IWatchlistRepository public sealed partial class PostgresWatchlistRepository : IWatchlistRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly ILogger<PostgresWatchlistRepository> _logger; private readonly ILogger<PostgresWatchlistRepository> _logger;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(5); private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(5);
@@ -28,7 +28,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
IMemoryCache cache, IMemoryCache cache,
ILogger<PostgresWatchlistRepository> logger) ILogger<PostgresWatchlistRepository> logger)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _dataSource = CreateDataSource(connectionString);
_cache = cache ?? throw new ArgumentNullException(nameof(cache)); _cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -36,8 +36,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<WatchedIdentity?> GetAsync(Guid id, CancellationToken cancellationToken = default) public async Task<WatchedIdentity?> GetAsync(Guid id, CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.SelectById, conn); await using var cmd = new NpgsqlCommand(SqlQueries.SelectById, conn);
cmd.Parameters.AddWithValue("id", id); cmd.Parameters.AddWithValue("id", id);
@@ -54,8 +53,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> GetCountAsync(string tenantId, CancellationToken cancellationToken = default) public async Task<int> GetCountAsync(string tenantId, CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
const string sql = "SELECT COUNT(*) FROM attestor.identity_watchlist WHERE tenant_id = @tenant_id"; const string sql = "SELECT COUNT(*) FROM attestor.identity_watchlist WHERE tenant_id = @tenant_id";
@@ -65,4 +63,18 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return Convert.ToInt32(result); return Convert.ToInt32(result);
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-watchlist"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: replaced raw PostgreSQL connection construction with named reusable data sources for watchlist and alert-dedup runtime paths. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/StellaOps.Attestor.Watchlist.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/StellaOps.Attestor.Watchlist.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -176,7 +176,11 @@ builder.Services.TryAddSingleton<IDpopProofValidator, DpopProofValidator>();
if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase)) if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase))
{ {
builder.Services.TryAddSingleton<IConnectionMultiplexer>(_ => builder.Services.TryAddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect(senderConstraints.Dpop.Nonce.RedisConnectionString!)); {
var redisOptions = ConfigurationOptions.Parse(senderConstraints.Dpop.Nonce.RedisConnectionString!);
redisOptions.ClientName ??= "stellaops-authority-dpop-nonce";
return ConnectionMultiplexer.Connect(redisOptions);
});
builder.Services.TryAddSingleton<IDpopNonceStore>(provider => builder.Services.TryAddSingleton<IDpopNonceStore>(provider =>
{ {

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Authority DPoP nonce-store Valkey client construction path. |
| AUDIT-0085-M | DONE | Revalidated 2026-01-06. | | AUDIT-0085-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0085-T | DONE | Revalidated 2026-01-06 (coverage reviewed). | | AUDIT-0085-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0085-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid/DateTimeOffset.UtcNow, fix branding error messages, and modularize Program.cs. | | AUDIT-0085-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid/DateTimeOffset.UtcNow, fix branding error messages, and modularize Program.cs. |

View File

@@ -123,6 +123,7 @@ static IResolutionCacheService CreateResolutionCacheService(IServiceProvider ser
redisOptions.ConnectTimeout = 500; redisOptions.ConnectTimeout = 500;
redisOptions.SyncTimeout = 500; redisOptions.SyncTimeout = 500;
redisOptions.AsyncTimeout = 500; redisOptions.AsyncTimeout = 500;
redisOptions.ClientName ??= "stellaops-binaryindex-resolution-cache";
var multiplexer = ConnectionMultiplexer.Connect(redisOptions); var multiplexer = ConnectionMultiplexer.Connect(redisOptions);
_ = multiplexer.GetDatabase().Ping(); _ = multiplexer.GetDatabase().Ping();

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the BinaryIndex resolution-cache Valkey client. |
| QA-BINARYINDEX-VERIFY-029 | DONE | SPRINT_20260211_033 run-001: `patch-coverage-tracking` passed Tier 0/1/2 with patch-coverage controller behavioral evidence (including post-create coverage updates) and was moved to `docs/features/checked/binaryindex/patch-coverage-tracking.md`. | | QA-BINARYINDEX-VERIFY-029 | DONE | SPRINT_20260211_033 run-001: `patch-coverage-tracking` passed Tier 0/1/2 with patch-coverage controller behavioral evidence (including post-create coverage updates) and was moved to `docs/features/checked/binaryindex/patch-coverage-tracking.md`. |
| QA-BINARYINDEX-VERIFY-028 | BLOCKED | SPRINT_20260211_033: `ml-function-embedding-service` is actively owned by another lane (`run-001` in progress); this lane terminalized collision as `skipped` (`owned_by_other_agent`) per FLOW 0.1. | | QA-BINARYINDEX-VERIFY-028 | BLOCKED | SPRINT_20260211_033: `ml-function-embedding-service` is actively owned by another lane (`run-001` in progress); this lane terminalized collision as `skipped` (`owned_by_other_agent`) per FLOW 0.1. |
| QA-BINARYINDEX-VERIFY-026 | DONE | SPRINT_20260211_033 run-002: `known-build-binary-catalog` passed Tier 0/1/2 with Build-ID/SHA256/assertion/cache/method-mapping behavioral evidence; dossier verified in `docs/features/checked/binaryindex/known-build-binary-catalog.md`. | | QA-BINARYINDEX-VERIFY-026 | DONE | SPRINT_20260211_033 run-002: `known-build-binary-catalog` passed Tier 0/1/2 with Build-ID/SHA256/assertion/cache/method-mapping behavioral evidence; dossier verified in `docs/features/checked/binaryindex/known-build-binary-catalog.md`. |

View File

@@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence;
/// <summary> /// <summary>
/// PostgreSQL repository for match results. /// PostgreSQL repository for match results.
/// </summary> /// </summary>
public sealed class MatchResultRepository : IMatchResultRepository public sealed class MatchResultRepository : IMatchResultRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
public MatchResultRepository(string connectionString) public MatchResultRepository(string connectionString)
{ {
_connectionString = connectionString; _dataSource = CreateDataSource(connectionString);
_jsonOptions = new JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
@@ -30,8 +30,7 @@ public sealed class MatchResultRepository : IMatchResultRepository
{ {
if (results.Count == 0) return; if (results.Count == 0) return;
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var transaction = await conn.BeginTransactionAsync(ct); await using var transaction = await conn.BeginTransactionAsync(ct);
@@ -105,8 +104,7 @@ public sealed class MatchResultRepository : IMatchResultRepository
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IReadOnlyList<MatchResult>> GetForRunAsync(Guid runId, CancellationToken ct = default) public async Task<IReadOnlyList<MatchResult>> GetForRunAsync(Guid runId, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
const string query = """ const string query = """
SELECT SELECT
@@ -215,4 +213,18 @@ public sealed class MatchResultRepository : IMatchResultRepository
string ExpectedName, string? ExpectedDemangledName, long ExpectedAddress, long? ExpectedSize, string ExpectedBuildId, string ExpectedBinaryName, string ExpectedName, string? ExpectedDemangledName, long ExpectedAddress, long? ExpectedSize, string ExpectedBuildId, string ExpectedBinaryName,
string? ActualName, string? ActualDemangledName, long? ActualAddress, long? ActualSize, string? ActualBuildId, string? ActualBinaryName, string? ActualName, string? ActualDemangledName, long? ActualAddress, long? ActualSize, string? ActualBuildId, string? ActualBinaryName,
string Outcome, double? MatchScore, string? Confidence, string? InferredCause, string? MismatchDetail, double? MatchDurationMs); string Outcome, double? MatchScore, string? Confidence, string? InferredCause, string? MismatchDetail, double? MatchDurationMs);
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-binaryindex-match-results"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence;
/// <summary> /// <summary>
/// PostgreSQL repository for validation runs. /// PostgreSQL repository for validation runs.
/// </summary> /// </summary>
public sealed class ValidationRunRepository : IValidationRunRepository public sealed class ValidationRunRepository : IValidationRunRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
public ValidationRunRepository(string connectionString) public ValidationRunRepository(string connectionString)
{ {
_connectionString = connectionString; _dataSource = CreateDataSource(connectionString);
_jsonOptions = new JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
@@ -28,8 +28,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
/// <inheritdoc/> /// <inheritdoc/>
public async Task SaveAsync(ValidationRun run, CancellationToken ct = default) public async Task SaveAsync(ValidationRun run, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
const string upsert = """ const string upsert = """
INSERT INTO groundtruth.validation_runs ( INSERT INTO groundtruth.validation_runs (
@@ -107,8 +106,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
/// <inheritdoc/> /// <inheritdoc/>
public async Task<ValidationRun?> GetAsync(Guid runId, CancellationToken ct = default) public async Task<ValidationRun?> GetAsync(Guid runId, CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
const string query = """ const string query = """
SELECT SELECT
@@ -132,8 +130,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
ValidationRunFilter? filter, ValidationRunFilter? filter,
CancellationToken ct = default) CancellationToken ct = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await _dataSource.OpenConnectionAsync(ct);
await conn.OpenAsync(ct);
var sql = new System.Text.StringBuilder(""" var sql = new System.Text.StringBuilder("""
SELECT id, name, status, created_at, completed_at, SELECT id, name, status, created_at, completed_at,
@@ -264,4 +261,18 @@ public sealed class ValidationRunRepository : IValidationRunRepository
private sealed record ValidationRunSummaryRow( private sealed record ValidationRunSummaryRow(
Guid Id, string Name, string Status, DateTimeOffset CreatedAt, DateTimeOffset? CompletedAt, Guid Id, string Name, string Status, DateTimeOffset CreatedAt, DateTimeOffset? CompletedAt,
double? MatchRate, double? F1Score, int? PairCount, int? FunctionCount, string[]? Tags); double? MatchRate, double? F1Score, int? PairCount, int? FunctionCount, string[]? Tags);
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-binaryindex-validation-runs"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named reusable PostgreSQL data sources for validation-run and match-result persistence. |
| QA-BINARYINDEX-VERIFY-023 | BLOCKED | SPRINT_20260211_033 run-001: blocked because `AGENTS.md` is missing in this module and related validation modules/tests (repo AGENTS rule 5). | | QA-BINARYINDEX-VERIFY-023 | BLOCKED | SPRINT_20260211_033 run-001: blocked because `AGENTS.md` is missing in this module and related validation modules/tests (repo AGENTS rule 5). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/StellaOps.BinaryIndex.Validation.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/StellaOps.BinaryIndex.Validation.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -11,16 +11,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of distro advisory repository. /// PostgreSQL implementation of distro advisory repository.
/// Queries the vuln.distro_advisories table for CVE + package evidence. /// Queries the vuln.distro_advisories table for CVE + package evidence.
/// </summary> /// </summary>
public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresDistroAdvisoryRepository> _logger; private readonly ILogger<PostgresDistroAdvisoryRepository> _logger;
public PostgresDistroAdvisoryRepository( public PostgresDistroAdvisoryRepository(
string connectionString, string connectionString,
ILogger<PostgresDistroAdvisoryRepository> logger) ILogger<PostgresDistroAdvisoryRepository> logger)
{ {
_connectionString = connectionString; _dataSource = CreateDataSource(connectionString);
_logger = logger; _logger = logger;
} }
@@ -48,8 +48,7 @@ public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
var result = await connection.QuerySingleOrDefaultAsync<DistroAdvisoryDto>( var result = await connection.QuerySingleOrDefaultAsync<DistroAdvisoryDto>(
new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct)); new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct));
@@ -71,4 +70,18 @@ public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository
throw; throw;
} }
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-distro-advisories"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -11,16 +11,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of patch repository. /// PostgreSQL implementation of patch repository.
/// Queries vuln.patch_evidence and feedser.binary_fingerprints tables. /// Queries vuln.patch_evidence and feedser.binary_fingerprints tables.
/// </summary> /// </summary>
public sealed class PostgresPatchRepository : IPatchRepository public sealed class PostgresPatchRepository : IPatchRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresPatchRepository> _logger; private readonly ILogger<PostgresPatchRepository> _logger;
public PostgresPatchRepository( public PostgresPatchRepository(
string connectionString, string connectionString,
ILogger<PostgresPatchRepository> logger) ILogger<PostgresPatchRepository> logger)
{ {
_connectionString = connectionString; _dataSource = CreateDataSource(connectionString);
_logger = logger; _logger = logger;
} }
@@ -45,8 +45,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
var results = await connection.QueryAsync<PatchHeaderDto>( var results = await connection.QueryAsync<PatchHeaderDto>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct)); new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -89,8 +88,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
var results = await connection.QueryAsync<PatchSigDto>( var results = await connection.QueryAsync<PatchSigDto>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct)); new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -144,8 +142,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
var results = await connection.QueryAsync<BinaryFingerprintRow>( var results = await connection.QueryAsync<BinaryFingerprintRow>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct)); new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -206,4 +203,18 @@ public sealed class PostgresPatchRepository : IPatchRepository
public required DateTimeOffset ExtractedAt { get; init; } public required DateTimeOffset ExtractedAt { get; init; }
public required string ExtractorVersion { get; init; } public required string ExtractorVersion { get; init; }
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-patches"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -10,16 +10,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of source artifact repository. /// PostgreSQL implementation of source artifact repository.
/// Queries vuln.changelog_evidence for CVE mentions in changelogs. /// Queries vuln.changelog_evidence for CVE mentions in changelogs.
/// </summary> /// </summary>
public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository, IAsyncDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresSourceArtifactRepository> _logger; private readonly ILogger<PostgresSourceArtifactRepository> _logger;
public PostgresSourceArtifactRepository( public PostgresSourceArtifactRepository(
string connectionString, string connectionString,
ILogger<PostgresSourceArtifactRepository> logger) ILogger<PostgresSourceArtifactRepository> logger)
{ {
_connectionString = connectionString; _dataSource = CreateDataSource(connectionString);
_logger = logger; _logger = logger;
} }
@@ -46,8 +46,7 @@ public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
var results = await connection.QueryAsync<ChangelogDto>( var results = await connection.QueryAsync<ChangelogDto>(
new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct)); new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct));
@@ -68,4 +67,18 @@ public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository
throw; throw;
} }
} }
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-source-artifacts"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: replaced raw PostgreSQL connection construction with named reusable proof-service data sources. |
| AUDIT-0233-M | DONE | Revalidated 2026-01-07. | | AUDIT-0233-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0233-T | DONE | Revalidated 2026-01-07. | | AUDIT-0233-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0233-A | TODO | Revalidated 2026-01-07 (open findings). | | AUDIT-0233-A | TODO | Revalidated 2026-01-07 (open findings). |

View File

@@ -23,7 +23,7 @@ namespace StellaOps.Doctor.WebService.Services;
/// </summary> /// </summary>
public sealed class PostgresReportStorageService : IReportStorageService, IDisposable public sealed class PostgresReportStorageService : IReportStorageService, IDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
private readonly DoctorServiceOptions _options; private readonly DoctorServiceOptions _options;
private readonly ILogger<PostgresReportStorageService> _logger; private readonly ILogger<PostgresReportStorageService> _logger;
private readonly Timer? _cleanupTimer; private readonly Timer? _cleanupTimer;
@@ -37,9 +37,10 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
IOptions<DoctorServiceOptions> options, IOptions<DoctorServiceOptions> options,
ILogger<PostgresReportStorageService> logger) ILogger<PostgresReportStorageService> logger)
{ {
_connectionString = configuration.GetConnectionString("StellaOps") var connectionString = configuration.GetConnectionString("StellaOps")
?? configuration["Database:ConnectionString"] ?? configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Database connection string not configured"); ?? throw new InvalidOperationException("Database connection string not configured");
_dataSource = CreateDataSource(connectionString);
_options = options.Value; _options = options.Value;
_logger = logger; _logger = logger;
@@ -60,8 +61,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
var json = JsonSerializer.Serialize(report, JsonSerializerOptions.Default); var json = JsonSerializer.Serialize(report, JsonSerializerOptions.Default);
var compressed = CompressJson(json); var compressed = CompressJson(json);
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = """ const string sql = """
INSERT INTO doctor_reports (run_id, started_at, completed_at, overall_severity, INSERT INTO doctor_reports (run_id, started_at, completed_at, overall_severity,
@@ -104,8 +104,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc /> /// <inheritdoc />
public async Task<DoctorReport?> GetReportAsync(string runId, CancellationToken ct) public async Task<DoctorReport?> GetReportAsync(string runId, CancellationToken ct)
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = "SELECT report_json_compressed FROM doctor_reports WHERE run_id = @runId"; const string sql = "SELECT report_json_compressed FROM doctor_reports WHERE run_id = @runId";
@@ -126,8 +125,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ReportSummaryDto>> ListReportsAsync(int limit, int offset, CancellationToken ct) public async Task<IReadOnlyList<ReportSummaryDto>> ListReportsAsync(int limit, int offset, CancellationToken ct)
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = """ const string sql = """
SELECT run_id, started_at, completed_at, overall_severity, SELECT run_id, started_at, completed_at, overall_severity,
@@ -170,8 +168,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> DeleteReportAsync(string runId, CancellationToken ct) public async Task<bool> DeleteReportAsync(string runId, CancellationToken ct)
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = "DELETE FROM doctor_reports WHERE run_id = @runId"; const string sql = "DELETE FROM doctor_reports WHERE run_id = @runId";
@@ -185,8 +182,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> GetCountAsync(CancellationToken ct) public async Task<int> GetCountAsync(CancellationToken ct)
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = "SELECT COUNT(*) FROM doctor_reports"; const string sql = "SELECT COUNT(*) FROM doctor_reports";
@@ -207,8 +203,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
var cutoff = DateTimeOffset.UtcNow.AddDays(-_options.ReportRetentionDays); var cutoff = DateTimeOffset.UtcNow.AddDays(-_options.ReportRetentionDays);
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _dataSource.OpenConnectionAsync(ct);
await connection.OpenAsync(ct);
const string sql = "DELETE FROM doctor_reports WHERE created_at < @cutoff"; const string sql = "DELETE FROM doctor_reports WHERE created_at < @cutoff";
@@ -261,7 +256,20 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
if (!_disposed) if (!_disposed)
{ {
_cleanupTimer?.Dispose(); _cleanupTimer?.Dispose();
_dataSource.Dispose();
_disposed = true; _disposed = true;
} }
} }
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-doctor-report-storage"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -3,6 +3,7 @@
## Completed ## Completed
### 2026-01-12 - Sprint 001_007 Implementation ### 2026-01-12 - Sprint 001_007 Implementation
- [x] SPRINT_20260405_011-XPORT: named the PostgreSQL report-storage data source and aligned the runtime path with the shared transport attribution guardrail.
- [x] Created project structure following Platform WebService pattern - [x] Created project structure following Platform WebService pattern
- [x] Implemented DoctorServiceOptions with authority configuration - [x] Implemented DoctorServiceOptions with authority configuration
- [x] Defined DoctorPolicies and DoctorScopes for authorization - [x] Defined DoctorPolicies and DoctorScopes for authorization

View File

@@ -38,7 +38,12 @@ public sealed class EvidenceLockerDataSource : IAsyncDisposable
private static NpgsqlDataSource CreateDataSource(string connectionString) private static NpgsqlDataSource CreateDataSource(string connectionString)
{ {
var builder = new NpgsqlDataSourceBuilder(connectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-evidence-locker",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
builder.EnableDynamicJson(); builder.EnableDynamicJson();
return builder.Build(); return builder.Build();
} }

View File

@@ -83,10 +83,10 @@ public static class EvidenceLockerInfrastructureServiceCollectionExtensions
// Verdict attestation repository // Verdict attestation repository
services.AddScoped<StellaOps.EvidenceLocker.Storage.IVerdictRepository>(provider => services.AddScoped<StellaOps.EvidenceLocker.Storage.IVerdictRepository>(provider =>
{ {
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value; var dataSource = provider.GetRequiredService<EvidenceLockerDataSource>();
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository>>(); var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository>>();
return new StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository( return new StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository(
options.Database.ConnectionString, cancellationToken => dataSource.OpenConnectionAsync(cancellationToken),
logger); logger);
}); });
@@ -94,10 +94,10 @@ public static class EvidenceLockerInfrastructureServiceCollectionExtensions
// Sprint: SPRINT_20260219_009 (CID-04) // Sprint: SPRINT_20260219_009 (CID-04)
services.AddScoped<StellaOps.EvidenceLocker.Storage.IEvidenceThreadRepository>(provider => services.AddScoped<StellaOps.EvidenceLocker.Storage.IEvidenceThreadRepository>(provider =>
{ {
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value; var dataSource = provider.GetRequiredService<EvidenceLockerDataSource>();
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository>>(); var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository>>();
return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository( return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository(
options.Database.ConnectionString, cancellationToken => dataSource.OpenConnectionAsync(cancellationToken),
logger); logger);
}); });

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Evidence Locker PostgreSQL datasource and switched runtime repositories onto the shared open-connection path. |
| AUDIT-0289-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. | | AUDIT-0289-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
| AUDIT-0289-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. | | AUDIT-0289-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
| AUDIT-0289-A | TODO | Revalidated 2026-01-07 (open findings). | | AUDIT-0289-A | TODO | Revalidated 2026-01-07 (open findings). |

View File

@@ -11,14 +11,14 @@ namespace StellaOps.EvidenceLocker.Storage;
/// </summary> /// </summary>
public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
{ {
private readonly string _connectionString; private readonly Func<CancellationToken, Task<NpgsqlConnection>> _openConnectionAsync;
private readonly ILogger<PostgresEvidenceThreadRepository> _logger; private readonly ILogger<PostgresEvidenceThreadRepository> _logger;
public PostgresEvidenceThreadRepository( public PostgresEvidenceThreadRepository(
string connectionString, Func<CancellationToken, Task<NpgsqlConnection>> openConnectionAsync,
ILogger<PostgresEvidenceThreadRepository> logger) ILogger<PostgresEvidenceThreadRepository> logger)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _openConnectionAsync = openConnectionAsync ?? throw new ArgumentNullException(nameof(openConnectionAsync));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -45,8 +45,7 @@ public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var record = await connection.QuerySingleOrDefaultAsync<ArtifactCanonicalRecord>( var record = await connection.QuerySingleOrDefaultAsync<ArtifactCanonicalRecord>(
new CommandDefinition( new CommandDefinition(
@@ -88,8 +87,7 @@ public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var results = await connection.QueryAsync<ArtifactCanonicalRecord>( var results = await connection.QueryAsync<ArtifactCanonicalRecord>(
new CommandDefinition( new CommandDefinition(

View File

@@ -9,16 +9,16 @@ namespace StellaOps.EvidenceLocker.Storage;
/// </summary> /// </summary>
public sealed class PostgresVerdictRepository : IVerdictRepository public sealed class PostgresVerdictRepository : IVerdictRepository
{ {
private readonly string _connectionString; private readonly Func<CancellationToken, Task<NpgsqlConnection>> _openConnectionAsync;
private readonly ILogger<PostgresVerdictRepository> _logger; private readonly ILogger<PostgresVerdictRepository> _logger;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
public PostgresVerdictRepository( public PostgresVerdictRepository(
string connectionString, Func<CancellationToken, Task<NpgsqlConnection>> openConnectionAsync,
ILogger<PostgresVerdictRepository> logger, ILogger<PostgresVerdictRepository> logger,
TimeProvider? timeProvider = null) TimeProvider? timeProvider = null)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _openConnectionAsync = openConnectionAsync ?? throw new ArgumentNullException(nameof(openConnectionAsync));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
} }
@@ -74,8 +74,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var now = _timeProvider.GetUtcNow(); var now = _timeProvider.GetUtcNow();
var verdictId = await connection.ExecuteScalarAsync<string>( var verdictId = await connection.ExecuteScalarAsync<string>(
@@ -153,8 +152,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var record = await connection.QuerySingleOrDefaultAsync<VerdictAttestationRecord>( var record = await connection.QuerySingleOrDefaultAsync<VerdictAttestationRecord>(
new CommandDefinition( new CommandDefinition(
@@ -231,8 +229,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var results = await connection.QueryAsync<VerdictAttestationSummary>( var results = await connection.QueryAsync<VerdictAttestationSummary>(
new CommandDefinition( new CommandDefinition(
@@ -309,8 +306,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var results = await connection.QueryAsync<VerdictAttestationSummary>( var results = await connection.QueryAsync<VerdictAttestationSummary>(
new CommandDefinition( new CommandDefinition(
@@ -365,8 +361,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try try
{ {
await using var connection = new NpgsqlConnection(_connectionString); await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
await connection.OpenAsync(cancellationToken);
var count = await connection.ExecuteScalarAsync<int>( var count = await connection.ExecuteScalarAsync<int>(
new CommandDefinition( new CommandDefinition(

View File

@@ -40,7 +40,12 @@ public sealed class ExportCenterDataSource : IAsyncDisposable
private static NpgsqlDataSource CreateDataSource(string connectionString) private static NpgsqlDataSource CreateDataSource(string connectionString)
{ {
var builder = new NpgsqlDataSourceBuilder(connectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-export-center",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
builder.EnableDynamicJson(); builder.EnableDynamicJson();
return builder.Build(); return builder.Build();
} }

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Export Center PostgreSQL datasource sessions for runtime attribution. |
| QA-EXPORTCENTER-VERIFY-001 | DONE | `cli-ui-surfacing-of-hidden-backend-capabilities` verified in run-002 (Tier 0/1/2 pass; Policy blocker remediated; client 62/62 and service 920/920). | | QA-EXPORTCENTER-VERIFY-001 | DONE | `cli-ui-surfacing-of-hidden-backend-capabilities` verified in run-002 (Tier 0/1/2 pass; Policy blocker remediated; client 62/62 and service 920/920). |
| QA-EXPORTCENTER-VERIFY-002 | DONE | `export-center-risk-bundle-builder` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). | | QA-EXPORTCENTER-VERIFY-002 | DONE | `export-center-risk-bundle-builder` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). |
| QA-EXPORTCENTER-VERIFY-003 | DONE | `export-telemetry-and-worker` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). | | QA-EXPORTCENTER-VERIFY-003 | DONE | `export-telemetry-and-worker` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). |

View File

@@ -22,7 +22,12 @@ public sealed class LedgerDataSource : IAsyncDisposable
_options = options.Value.Database; _options = options.Value.Database;
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
var builder = new NpgsqlDataSourceBuilder(_options.ConnectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_options.ConnectionString)
{
ApplicationName = "stellaops-findings-ledger",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
_dataSource = builder.Build(); _dataSource = builder.Build();
} }

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Findings Ledger PostgreSQL datasource sessions for runtime attribution. |
| AUDIT-0342-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings Ledger. | | AUDIT-0342-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings Ledger. |
| AUDIT-0342-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings Ledger. | | AUDIT-0342-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings Ledger. |
| AUDIT-0342-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). | | AUDIT-0342-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). |

View File

@@ -20,7 +20,13 @@ public sealed class PostgresRiskScoreResultStore : IRiskScoreResultStore, IAsync
public PostgresRiskScoreResultStore(string connectionString) public PostgresRiskScoreResultStore(string connectionString)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = NpgsqlDataSource.Create(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-riskengine",
};
_dataSource = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
} }
public async Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken) public async Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken)

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the risk-engine PostgreSQL result-store datasource. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Infrastructure/StellaOps.RiskEngine.Infrastructure.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Infrastructure/StellaOps.RiskEngine.Infrastructure.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-312-005 | DONE | Added `PostgresRiskScoreResultStore` with schema/bootstrap and deterministic upsert/read behavior. | | SPRINT-312-005 | DONE | Added `PostgresRiskScoreResultStore` with schema/bootstrap and deterministic upsert/read behavior. |

View File

@@ -32,10 +32,7 @@ public sealed class AnalyticsIngestionDataSource : IAsyncDisposable
return null; return null;
} }
_dataSource ??= new NpgsqlDataSourceBuilder(_connectionString!) _dataSource ??= CreateDataSource(_connectionString!);
{
Name = "StellaOps.Platform.Analytics.Ingestion"
}.Build();
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false); await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false);
@@ -62,4 +59,17 @@ public sealed class AnalyticsIngestionDataSource : IAsyncDisposable
_logger.LogDebug("Configured analytics ingestion session for PostgreSQL connection."); _logger.LogDebug("Configured analytics ingestion session for PostgreSQL connection.");
} }
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-platform-analytics-ingestion",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
{
Name = "StellaOps.Platform.Analytics.Ingestion"
}.Build();
}
} }

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named analytics ingestion PostgreSQL datasource construction for runtime attribution. |
| QA-PLATFORM-VERIFY-001 | DONE | run-002 verification captured analytics rollup/materialized-view behavior evidence; feature terminalized as `not_implemented` due missing advisory lock/LISTEN-NOTIFY parity. | | QA-PLATFORM-VERIFY-001 | DONE | run-002 verification captured analytics rollup/materialized-view behavior evidence; feature terminalized as `not_implemented` due missing advisory lock/LISTEN-NOTIFY parity. |
| QA-PLATFORM-VERIFY-002 | DONE | run-001 verification passed with maintenance, endpoint paths, analytics service behavior, and Docker schema integration (`38/38` scoped tests). | | QA-PLATFORM-VERIFY-002 | DONE | run-001 verification passed with maintenance, endpoint paths, analytics service behavior, and Docker schema integration (`38/38` scoped tests). |
| QA-PLATFORM-VERIFY-003 | DONE | `platform-service-aggregation-layer` verified with run-001 Tier 0/1/2 endpoint evidence and moved to `docs/features/checked/platform/`. | | QA-PLATFORM-VERIFY-003 | DONE | `platform-service-aggregation-layer` verified with run-001 Tier 0/1/2 endpoint evidence and moved to `docs/features/checked/platform/`. |

View File

@@ -32,10 +32,7 @@ public sealed class PlatformAnalyticsDataSource : IAsyncDisposable
return null; return null;
} }
_dataSource ??= new NpgsqlDataSourceBuilder(_connectionString!) _dataSource ??= CreateDataSource(_connectionString!);
{
Name = "StellaOps.Platform.Analytics"
}.Build();
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false); await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false);
@@ -64,4 +61,17 @@ public sealed class PlatformAnalyticsDataSource : IAsyncDisposable
_logger.LogDebug("Configured analytics session for PostgreSQL connection."); _logger.LogDebug("Configured analytics session for PostgreSQL connection.");
} }
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-platform-analytics",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
{
Name = "StellaOps.Platform.Analytics"
}.Build();
}
} }

View File

@@ -30,7 +30,12 @@ public static class ServiceCollectionExtensions
services.AddSingleton<NpgsqlDataSource>(sp => services.AddSingleton<NpgsqlDataSource>(sp =>
{ {
var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value; var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value;
var dataSourceBuilder = new NpgsqlDataSourceBuilder(options.ConnectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-plugin-registry",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
return dataSourceBuilder.Build(); return dataSourceBuilder.Build();
}); });
@@ -62,7 +67,12 @@ public static class ServiceCollectionExtensions
services.AddSingleton<NpgsqlDataSource>(sp => services.AddSingleton<NpgsqlDataSource>(sp =>
{ {
var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value; var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value;
var dataSourceBuilder = new NpgsqlDataSourceBuilder(options.ConnectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-plugin-registry",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
return dataSourceBuilder.Build(); return dataSourceBuilder.Build();
}); });

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named plugin registry PostgreSQL datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Plugin/StellaOps.Plugin.Registry/StellaOps.Plugin.Registry.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Plugin/StellaOps.Plugin.Registry/StellaOps.Plugin.Registry.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -272,7 +272,11 @@ public static class PolicyEngineServiceCollectionExtensions
string connectionString) string connectionString)
{ {
services.TryAddSingleton<IConnectionMultiplexer>(sp => services.TryAddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(connectionString)); {
var redisOptions = ConfigurationOptions.Parse(connectionString);
redisOptions.ClientName ??= "stellaops-policy-engine";
return ConnectionMultiplexer.Connect(redisOptions);
});
return services; return services;
} }

View File

@@ -5,6 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20260119_021_Policy_license_compliance.md
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Policy Engine Valkey client construction. |
| AUDIT-0440-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Engine. | | AUDIT-0440-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Engine. |
| AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. | | AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. |
| AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). | | AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). |

View File

@@ -35,7 +35,13 @@ builder.Services.AddSingleton<NpgsqlDataSource>(sp =>
var config = sp.GetRequiredService<IConfiguration>(); var config = sp.GetRequiredService<IConfiguration>();
var connStr = config.GetConnectionString("PostgreSQL") var connStr = config.GetConnectionString("PostgreSQL")
?? throw new InvalidOperationException("PostgreSQL connection string not configured"); ?? throw new InvalidOperationException("PostgreSQL connection string not configured");
return new NpgsqlDataSourceBuilder(connStr).Build();
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connStr)
{
ApplicationName = "stellaops-reachgraph",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}); });
// Redis/Valkey (lazy so integration tests can replace before first resolve) // Redis/Valkey (lazy so integration tests can replace before first resolve)
@@ -43,7 +49,9 @@ builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{ {
var config = sp.GetRequiredService<IConfiguration>(); var config = sp.GetRequiredService<IConfiguration>();
var redisConnStr = config.GetConnectionString("Redis") ?? "localhost:6379"; var redisConnStr = config.GetConnectionString("Redis") ?? "localhost:6379";
return ConnectionMultiplexer.Connect(redisConnStr); var redisOptions = ConfigurationOptions.Parse(redisConnStr);
redisOptions.ClientName ??= "stellaops-reachgraph";
return ConnectionMultiplexer.Connect(redisOptions);
}); });
// Core services // Core services

View File

@@ -4,5 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the ReachGraph PostgreSQL datasource for runtime attribution. |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the ReachGraph Valkey client construction path. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReachGraph/StellaOps.ReachGraph.WebService/StellaOps.ReachGraph.WebService.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReachGraph/StellaOps.ReachGraph.WebService/StellaOps.ReachGraph.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -29,7 +29,11 @@ public static class ServiceCollectionExtensions
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
// Create NpgsqlDataSource from connection string // Create NpgsqlDataSource from connection string
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-release-orchestrator-policygate",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
var dataSource = dataSourceBuilder.Build(); var dataSource = dataSourceBuilder.Build();
// Register stores // Register stores

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Policy Gate PostgreSQL datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/StellaOps.ReleaseOrchestrator.PolicyGate.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/StellaOps.ReleaseOrchestrator.PolicyGate.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -14,7 +14,13 @@ public sealed class PostgresFeedSnapshotIndexStore : IFeedSnapshotIndexStore, IA
public PostgresFeedSnapshotIndexStore(string connectionString) public PostgresFeedSnapshotIndexStore(string connectionString)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = NpgsqlDataSource.Create(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-replay",
};
_dataSource = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
} }
public async Task IndexSnapshotAsync(FeedSnapshotIndexEntry entry, CancellationToken ct = default) public async Task IndexSnapshotAsync(FeedSnapshotIndexEntry entry, CancellationToken ct = default)

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Replay PostgreSQL snapshot-index datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Replay/StellaOps.Replay.WebService/StellaOps.Replay.WebService.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Replay/StellaOps.Replay.WebService/StellaOps.Replay.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-312-006 | DONE | Added Postgres snapshot index + seed-fs snapshot blob stores and wired storage-driver registration in webservice startup. | | SPRINT-312-006 | DONE | Added Postgres snapshot index + seed-fs snapshot blob stores and wired storage-driver registration in webservice startup. |

View File

@@ -20,7 +20,7 @@ namespace StellaOps.Scanner.Reachability.Cache;
/// </summary> /// </summary>
public sealed class PostgresReachabilityCache : IReachabilityCache public sealed class PostgresReachabilityCache : IReachabilityCache
{ {
private readonly string _connectionString; private readonly Services.PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresReachabilityCache> _logger; private readonly ILogger<PostgresReachabilityCache> _logger;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
@@ -28,8 +28,16 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string connectionString, string connectionString,
ILogger<PostgresReachabilityCache> logger, ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null) TimeProvider? timeProvider = null)
: this(new Services.PostgresReachabilityDataSourceProvider(connectionString), logger, timeProvider)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); }
internal PostgresReachabilityCache(
Services.PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
} }
@@ -40,8 +48,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string graphHash, string graphHash,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken);
// Get cache entry // Get cache entry
const string entrySql = """ const string entrySql = """
@@ -120,8 +127,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
{ {
ArgumentNullException.ThrowIfNull(entry); ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken);
await using var tx = await conn.BeginTransactionAsync(cancellationToken); await using var tx = await conn.BeginTransactionAsync(cancellationToken);
try try
@@ -202,8 +208,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string sinkMethodKey, string sinkMethodKey,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken);
const string sql = """ const string sql = """
SELECT p.is_reachable, p.path_length, p.confidence, p.computed_at SELECT p.is_reachable, p.path_length, p.confidence, p.computed_at
@@ -246,8 +251,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
IEnumerable<string> affectedMethodKeys, IEnumerable<string> affectedMethodKeys,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken);
// For now, invalidate entire cache for service // For now, invalidate entire cache for service
// More granular invalidation would require additional indices // More granular invalidation would require additional indices
@@ -283,8 +287,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string serviceId, string serviceId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await using var conn = new NpgsqlConnection(_connectionString); await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.OpenAsync(cancellationToken);
const string sql = """ const string sql = """
SELECT total_hits, total_misses, full_recomputes, incremental_computes, SELECT total_hits, total_misses, full_recomputes, incremental_computes,
@@ -392,4 +395,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
await cmd.ExecuteNonQueryAsync(cancellationToken); await cmd.ExecuteNonQueryAsync(cancellationToken);
} }
private ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSourceProvider.OpenConnectionAsync(cancellationToken);
} }

View File

@@ -34,10 +34,12 @@ public static class ServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
services.TryAddSingleton(_ => new PostgresReachabilityDataSourceProvider(connectionString));
// CVE-Symbol Mapping Service // CVE-Symbol Mapping Service
services.TryAddSingleton<ICveSymbolMappingService>(sp => services.TryAddSingleton<ICveSymbolMappingService>(sp =>
new PostgresCveSymbolMappingRepository( new PostgresCveSymbolMappingRepository(
connectionString, sp.GetRequiredService<PostgresReachabilityDataSourceProvider>(),
sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresCveSymbolMappingRepository>>())); sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresCveSymbolMappingRepository>>()));
// Stack Evaluator (already exists, ensure registered) // Stack Evaluator (already exists, ensure registered)

View File

@@ -15,14 +15,21 @@ namespace StellaOps.Scanner.Reachability.Services;
/// </summary> /// </summary>
public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingService public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingService
{ {
private readonly string _connectionString; private readonly PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresCveSymbolMappingRepository> _logger; private readonly ILogger<PostgresCveSymbolMappingRepository> _logger;
public PostgresCveSymbolMappingRepository( public PostgresCveSymbolMappingRepository(
string connectionString, string connectionString,
ILogger<PostgresCveSymbolMappingRepository> logger) ILogger<PostgresCveSymbolMappingRepository> logger)
: this(new PostgresReachabilityDataSourceProvider(connectionString), logger)
{ {
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); }
internal PostgresCveSymbolMappingRepository(
PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresCveSymbolMappingRepository> logger)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -206,9 +213,7 @@ public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingServic
private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken ct) private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken ct)
{ {
var conn = new NpgsqlConnection(_connectionString); return await _dataSourceProvider.OpenConnectionAsync(ct).ConfigureAwait(false);
await conn.OpenAsync(ct);
return conn;
} }
private static CveSinkMapping MapFromReader(NpgsqlDataReader reader) private static CveSinkMapping MapFromReader(NpgsqlDataReader reader)

View File

@@ -0,0 +1,37 @@
using Npgsql;
namespace StellaOps.Scanner.Reachability.Services;
internal sealed class PostgresReachabilityDataSourceProvider : IAsyncDisposable
{
private readonly Lazy<NpgsqlDataSource> _dataSource;
public PostgresReachabilityDataSourceProvider(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = new Lazy<NpgsqlDataSource>(
() => CreateDataSource(connectionString),
isThreadSafe: true);
}
public ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSource.Value.OpenConnectionAsync(cancellationToken);
public async ValueTask DisposeAsync()
{
if (_dataSource.IsValueCreated)
{
await _dataSource.Value.DisposeAsync().ConfigureAwait(false);
}
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-scanner-reachability",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added a named reachability datasource provider and routed PostgreSQL-backed reachability services through it. |
| QA-SCANNER-VERIFY-005 | DONE | SPRINT_20260212_002 run-001: `api-gateway-boundary-extractor` passed Tier 0/1/2 and moved to `docs/features/checked/scanner/`. | | QA-SCANNER-VERIFY-005 | DONE | SPRINT_20260212_002 run-001: `api-gateway-boundary-extractor` passed Tier 0/1/2 and moved to `docs/features/checked/scanner/`. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -236,6 +236,7 @@ public static class VexLensServiceCollectionExtensions
{ {
var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString) var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{ {
ApplicationName = "stellaops-vexlens",
CommandTimeout = options.CommandTimeoutSeconds CommandTimeout = options.CommandTimeoutSeconds
}; };
var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString); var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString);
@@ -274,6 +275,7 @@ public static class VexLensServiceCollectionExtensions
{ {
var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString) var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{ {
ApplicationName = "stellaops-vexlens",
CommandTimeout = options.CommandTimeoutSeconds CommandTimeout = options.CommandTimeoutSeconds
}; };
var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString); var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString);

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named VexLens PostgreSQL consensus-store data sources. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/VexLens/StellaOps.VexLens/StellaOps.VexLens.md. | | REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/VexLens/StellaOps.VexLens/StellaOps.VexLens.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. | | REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -62,7 +62,7 @@ public static class ServiceCollectionExtensions
"Eventing:ConnectionString must be configured when Eventing:UseInMemoryStore is false."); "Eventing:ConnectionString must be configured when Eventing:UseInMemoryStore is false.");
} }
return NpgsqlDataSource.Create(configuredOptions.ConnectionString); return CreateNamedDataSource(configuredOptions.ConnectionString, configuredOptions.ServiceName);
}); });
services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>(); services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>();
@@ -104,7 +104,7 @@ public static class ServiceCollectionExtensions
TryAddHybridLogicalClock(services, serviceName); TryAddHybridLogicalClock(services, serviceName);
// Register NpgsqlDataSource // Register NpgsqlDataSource
services.TryAddSingleton(_ => NpgsqlDataSource.Create(connectionString)); services.TryAddSingleton(_ => CreateNamedDataSource(connectionString, serviceName));
services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>(); services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>();
services.TryAddSingleton<ITimelineEventEmitter, TimelineEventEmitter>(); services.TryAddSingleton<ITimelineEventEmitter, TimelineEventEmitter>();
@@ -186,4 +186,15 @@ public static class ServiceCollectionExtensions
return nodeId.Trim().Replace(' ', '-').ToLowerInvariant(); return nodeId.Trim().Replace(' ', '-').ToLowerInvariant();
} }
private static NpgsqlDataSource CreateNamedDataSource(string connectionString, string? serviceName)
{
var normalizedServiceName = ResolveNodeId(serviceName);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = $"stellaops-eventing-{normalizedServiceName}",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
} }

View File

@@ -19,6 +19,8 @@
## Determinism & Guardrails ## Determinism & Guardrails
- Target runtime: .NET 10, Npgsql 9.x; keep options defaults deterministic (UTC timezone, statement timeout, stable pagination ordering). - Target runtime: .NET 10, Npgsql 9.x; keep options defaults deterministic (UTC timezone, statement timeout, stable pagination ordering).
- Tenant context must be set via `set_config('app.current_tenant', ...)` on every connection before use; never bypass DataSourceBase. - Tenant context must be set via `set_config('app.current_tenant', ...)` on every connection before use; never bypass DataSourceBase.
- Runtime `NpgsqlDataSource` creation must carry a stable `application_name`; prefer `PostgresConnectionStringPolicy` or an explicit `NpgsqlConnectionStringBuilder.ApplicationName`.
- Anonymous `NpgsqlDataSource.Create(...)` is forbidden in steady-state runtime code. Allow exceptions only for tests, migrations, CLI/setup paths, or sprint-documented one-shot diagnostics.
- Migrations ship as embedded resources; MigrationRunner uses SHA256 checksums and `RunFromAssemblyAsync`???do not execute ad-hoc SQL outside tracked migrations. - Migrations ship as embedded resources; MigrationRunner uses SHA256 checksums and `RunFromAssemblyAsync`???do not execute ad-hoc SQL outside tracked migrations.
- Respect air-gap posture: no external downloads at runtime; pin Postgres/Testcontainers images (`postgres:16-alpine` or later) in tests. - Respect air-gap posture: no external downloads at runtime; pin Postgres/Testcontainers images (`postgres:16-alpine` or later) in tests.
@@ -30,4 +32,3 @@
## Handoff Notes ## Handoff Notes
- Align configuration defaults with the provisioning values under `devops/database/postgres` (ports, pool sizes, SSL/TLS). - Align configuration defaults with the provisioning values under `devops/database/postgres` (ports, pool sizes, SSL/TLS).
- Update this AGENTS file whenever connection/session rules or provisioning defaults change; record updates in the sprint Execution Log. - Update this AGENTS file whenever connection/session rules or provisioning defaults change; record updates in the sprint Execution Log.

View File

@@ -241,15 +241,8 @@ public abstract class DataSourceBase : IAsyncDisposable
return connection; return connection;
} }
private static string BuildConnectionString(PostgresOptions options) private string BuildConnectionString(PostgresOptions options)
{ => PostgresConnectionStringPolicy.Build(
var builder = new NpgsqlConnectionStringBuilder(options.ConnectionString) options,
{ PostgresConnectionStringPolicy.BuildDefaultApplicationName(ModuleName));
Pooling = options.Pooling,
MaxPoolSize = options.MaxPoolSize,
MinPoolSize = options.MinPoolSize
};
return builder.ToString();
}
} }

View File

@@ -0,0 +1,103 @@
using Npgsql;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.Infrastructure.Postgres.Connections;
/// <summary>
/// Applies StellaOps runtime defaults to PostgreSQL connection strings.
/// </summary>
public static class PostgresConnectionStringPolicy
{
public static string Build(PostgresOptions options, string defaultApplicationName)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(defaultApplicationName);
return Build(
options.ConnectionString,
string.IsNullOrWhiteSpace(options.ApplicationName)
? defaultApplicationName.Trim()
: options.ApplicationName.Trim(),
options.Pooling,
options.MinPoolSize,
options.MaxPoolSize,
options.ConnectionIdleLifetimeSeconds);
}
public static string Build(
string connectionString,
string applicationName,
bool pooling = true,
int minPoolSize = 1,
int maxPoolSize = 100,
int? connectionIdleLifetimeSeconds = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(applicationName);
var normalizedMinPoolSize = Math.Max(minPoolSize, 0);
var normalizedMaxPoolSize = Math.Max(maxPoolSize, normalizedMinPoolSize);
var builder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = applicationName.Trim(),
Pooling = pooling,
MinPoolSize = normalizedMinPoolSize,
MaxPoolSize = normalizedMaxPoolSize,
};
if (connectionIdleLifetimeSeconds.HasValue && connectionIdleLifetimeSeconds.Value > 0)
{
builder.ConnectionIdleLifetime = connectionIdleLifetimeSeconds.Value;
}
return builder.ToString();
}
public static string BuildDefaultApplicationName(string moduleName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(moduleName);
return $"stellaops-{ToKebabCase(moduleName.Trim())}";
}
private static string ToKebabCase(string value)
{
var chars = new List<char>(value.Length + 8);
for (var i = 0; i < value.Length; i++)
{
var current = value[i];
if (char.IsWhiteSpace(current) || current is '_' or '-')
{
if (chars.Count > 0 && chars[^1] != '-')
{
chars.Add('-');
}
continue;
}
if (char.IsUpper(current))
{
var hasPrevious = chars.Count > 0;
var previous = hasPrevious ? value[i - 1] : '\0';
var hasNext = i + 1 < value.Length;
var next = hasNext ? value[i + 1] : '\0';
if (hasPrevious
&& chars[^1] != '-'
&& (!char.IsUpper(previous) || (hasNext && char.IsLower(next))))
{
chars.Add('-');
}
chars.Add(char.ToLowerInvariant(current));
continue;
}
chars.Add(char.ToLowerInvariant(current));
}
return new string(chars.ToArray()).Trim('-');
}
}

View File

@@ -20,6 +20,11 @@ public sealed class PostgresOptions
/// </summary> /// </summary>
public int MaxPoolSize { get; set; } = 100; public int MaxPoolSize { get; set; } = 100;
/// <summary>
/// Stable PostgreSQL application name used for runtime attribution.
/// </summary>
public string? ApplicationName { get; set; }
/// <summary> /// <summary>
/// Minimum number of connections in the pool. Default is 1. /// Minimum number of connections in the pool. Default is 1.
/// </summary> /// </summary>

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes | | Task ID | Status | Notes |
| --- | --- | --- | | --- | --- | --- |
| SPRINT_20260405_011-XPORT-STD | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: shared transport hardening, PostgreSQL application-name policy, runtime caller remediation, and static guardrails. |
| AUDIT-0089-M | DONE | Revalidated 2026-01-08; maintainability audit for Infrastructure.Postgres. | | AUDIT-0089-M | DONE | Revalidated 2026-01-08; maintainability audit for Infrastructure.Postgres. |
| AUDIT-0089-T | DONE | Revalidated 2026-01-08; test coverage audit for Infrastructure.Postgres. | | AUDIT-0089-T | DONE | Revalidated 2026-01-08; test coverage audit for Infrastructure.Postgres. |
| AUDIT-0089-A | TODO | Pending approval (non-test project; revalidated 2026-01-08). | | AUDIT-0089-A | TODO | Pending approval (non-test project; revalidated 2026-01-08). |

View File

@@ -0,0 +1,66 @@
using FluentAssertions;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Connections;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Infrastructure.Postgres.Tests;
[Trait("Category", TestCategories.Unit)]
public sealed class PostgresConnectionStringPolicyTests
{
[Fact]
public void Build_applies_runtime_attribution_and_pooling_policy()
{
var options = new PostgresOptions
{
ConnectionString = "Host=localhost;Database=stellaops;Username=stellaops;Password=stellaops",
ApplicationName = "custom-ledger-app",
Pooling = false,
MinPoolSize = 3,
MaxPoolSize = 12,
ConnectionIdleLifetimeSeconds = 900,
};
var connectionString = PostgresConnectionStringPolicy.Build(options, "stellaops-findings-ledger");
var builder = new NpgsqlConnectionStringBuilder(connectionString);
builder.ApplicationName.Should().Be("custom-ledger-app");
builder.Pooling.Should().BeFalse();
builder.MinPoolSize.Should().Be(3);
builder.MaxPoolSize.Should().Be(12);
builder.ConnectionIdleLifetime.Should().Be(900);
}
[Fact]
public void Build_uses_default_application_name_when_none_is_configured()
{
var options = new PostgresOptions
{
ConnectionString = "Host=localhost;Database=stellaops;Username=stellaops;Password=stellaops",
MinPoolSize = 0,
MaxPoolSize = 1,
};
var connectionString = PostgresConnectionStringPolicy.Build(options, "stellaops-policy");
var builder = new NpgsqlConnectionStringBuilder(connectionString);
builder.ApplicationName.Should().Be("stellaops-policy");
builder.MinPoolSize.Should().Be(0);
builder.MaxPoolSize.Should().Be(1);
builder.ConnectionIdleLifetime.Should().Be(300);
}
[Theory]
[InlineData("Policy", "stellaops-policy")]
[InlineData("PacksRegistry", "stellaops-packs-registry")]
[InlineData("IssuerDirectory", "stellaops-issuer-directory")]
[InlineData("ReachGraph", "stellaops-reach-graph")]
public void BuildDefaultApplicationName_normalizes_module_names(string moduleName, string expected)
{
var result = PostgresConnectionStringPolicy.BuildDefaultApplicationName(moduleName);
result.Should().Be(expected);
}
}