Harden runtime HTTP transport lifecycles

This commit is contained in:
master
2026-04-05 23:52:14 +03:00
parent 1151c30e3a
commit 751546084e
44 changed files with 1173 additions and 136 deletions

View File

@@ -16,8 +16,7 @@ public sealed partial class ArtifactController
{
_logger.LogDebug("Fetching from HTTP: {Uri}", uri);
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(30);
var httpClient = _httpClientFactory?.CreateClient(HttpFetchClientName) ?? SharedHttpFetchClient;
try
{

View File

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.Artifact.Core;
using System.Net.Http;
namespace StellaOps.Artifact.Api;
@@ -22,15 +23,23 @@ namespace StellaOps.Artifact.Api;
public sealed partial class ArtifactController : ControllerBase
{
private const string GetArtifactActionName = "GetArtifact";
private const string HttpFetchClientName = "ArtifactController.FetchHttp";
private static readonly HttpClient SharedHttpFetchClient = new()
{
Timeout = TimeSpan.FromSeconds(30)
};
private readonly IArtifactStore _artifactStore;
private readonly IHttpClientFactory? _httpClientFactory;
private readonly ILogger<ArtifactController> _logger;
public ArtifactController(
IArtifactStore artifactStore,
ILogger<ArtifactController> logger)
ILogger<ArtifactController> logger,
IHttpClientFactory? httpClientFactory = null)
{
_artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClientFactory = httpClientFactory;
}
}

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: artifact HTTP fetch path now prefers factory-backed clients and uses a shared fallback instead of allocating per request. |
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.Artifact.Core/StellaOps.Artifact.Core.md. Tests: `dotnet test src/__Libraries/StellaOps.Artifact.Core.Tests/StellaOps.Artifact.Core.Tests.csproj` (23 tests, MTP0001 warning). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Plugin.Hosting;
using System;
using System.Collections.Generic;
@@ -120,6 +120,12 @@ public static class PluginLoader
{
public static IReadOnlyList<TPlugin> LoadPlugins<TPlugin>(IEnumerable<Assembly> assemblies)
where TPlugin : class
=> LoadPlugins<TPlugin>(assemblies, serviceProvider: null);
public static IReadOnlyList<TPlugin> LoadPlugins<TPlugin>(
IEnumerable<Assembly> assemblies,
IServiceProvider? serviceProvider)
where TPlugin : class
{
if (assemblies == null) throw new ArgumentNullException(nameof(assemblies));
@@ -140,7 +146,7 @@ public static class PluginLoader
continue;
}
if (candidate.GetConstructor(Type.EmptyTypes) is null)
if (serviceProvider is null && candidate.GetConstructor(Type.EmptyTypes) is null)
{
continue;
}
@@ -148,11 +154,13 @@ public static class PluginLoader
TPlugin? plugin;
try
{
plugin = Activator.CreateInstance(candidate) as TPlugin;
plugin = serviceProvider is null
? Activator.CreateInstance(candidate) as TPlugin
: ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, candidate) as TPlugin;
}
catch
{
// Skip plugins that cannot be created via default constructor.
// Skip plugins that cannot be created via the available DI/default constructor path.
continue;
}

View File

@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0095-T | DONE | Revalidated 2026-01-08; test coverage audit for StellaOps.Plugin. |
| AUDIT-0095-A | TODO | Revalidated 2026-01-08 (open findings). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: PluginLoader now supports service-provider-backed runtime activation so host-owned plugins can consume shared transport clients. |

View File

@@ -122,7 +122,7 @@ public sealed class OciAttestationPublisher : IOciAttestationPublisher
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient = httpClient ?? new HttpClient();
_httpClient = httpClient ?? OciRuntimeHttpClient.SharedClient;
_timeProvider = timeProvider ?? TimeProvider.System;
}

View File

@@ -0,0 +1,19 @@
using System.Net.Http.Headers;
namespace StellaOps.Verdict.Oci;
internal static class OciRuntimeHttpClient
{
public static HttpClient SharedClient { get; } = CreateClient();
private static HttpClient CreateClient()
{
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(100)
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
return client;
}
}

View File

@@ -9,5 +9,6 @@ Source of truth: `docs/implplan/SPRINT_20260222_080_Verdict_persistence_dal_to_e
| VERDICT-EF-03 | DONE | PostgresVerdictStore converted to use VerdictDataSource + VerdictDbContextFactory pattern; inline VerdictDbContext removed. |
| VERDICT-EF-04 | DONE | Compiled model stubs generated; assembly attributes excluded from compilation; VerdictDbContextFactory uses compiled model for default schema. |
| VERDICT-EF-05 | DONE | Sequential builds pass (0 warnings, 0 errors); module docs and AGENTS.md updated; sprint tracker updated. |
| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: OCI attestation publisher fallback HTTP path now reuses a shared runtime client instead of allocating ad hoc transport instances. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.Verdict/StellaOps.Verdict.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,167 @@
using FluentAssertions;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Infrastructure.Postgres.Tests;
[Trait("Category", TestCategories.Unit)]
public sealed class RuntimePostgresConstructionConventionTests
{
private static readonly string RepoRoot = FindRepoRoot();
private static readonly HashSet<string> AllowedRuntimeRawConnectionFiles = new(StringComparer.Ordinal)
{
"src/Cli/__Libraries/StellaOps.Cli.Plugins.Aoc/AocVerificationService.cs",
"src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/DatabaseSetupStep.cs",
"src/Cli/StellaOps.Cli/Services/MigrationCommandService.cs",
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresConnectionPoolCheck.cs",
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresConnectivityCheck.cs",
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresMigrationStatusCheck.cs",
"src/Platform/StellaOps.Platform.WebService/Services/PlatformMigrationAdminService.cs",
"src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs",
"src/__Libraries/StellaOps.Doctor.Plugins.Database/Checks/DatabaseCheckBase.cs",
"src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs",
"src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs",
};
private static readonly HashSet<string> AllowedRuntimeValkeyConnectionFiles = new(StringComparer.Ordinal)
{
"src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/CacheSetupStep.cs",
"src/Tools/NotifySmokeCheck/NotifySmokeCheckRunner.cs",
"src/__Libraries/StellaOps.TestKit/Fixtures/ValkeyFixture.cs",
};
private static readonly string[] KnownHttpLifecycleHotspots =
[
"src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs",
"src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs",
"src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs",
"src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs",
"src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs",
"src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs",
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs",
"src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs",
"src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs",
];
[Fact]
public void Runtime_code_does_not_use_anonymous_NpgsqlDataSource_Create()
{
var offenders = EnumerateRuntimeSourceFiles()
.Where(file => File.ReadAllText(file).Contains("NpgsqlDataSource.Create(", StringComparison.Ordinal))
.Select(ToRelativePath)
.ToList();
offenders.Should().BeEmpty(
"runtime PostgreSQL callers should use a named data source or the shared PostgreSQL policy path");
}
[Fact]
public void Runtime_data_source_builders_must_apply_runtime_attribution()
{
var offenders = EnumerateRuntimeSourceFiles()
.Where(file =>
{
var content = File.ReadAllText(file);
return content.Contains("new NpgsqlDataSourceBuilder(", StringComparison.Ordinal)
&& !content.Contains("ApplicationName", StringComparison.Ordinal)
&& !content.Contains("PostgresConnectionStringPolicy", StringComparison.Ordinal);
})
.Select(ToRelativePath)
.ToList();
offenders.Should().BeEmpty(
"runtime NpgsqlDataSourceBuilder call sites must stamp ApplicationName explicitly or flow through PostgresConnectionStringPolicy");
}
[Fact]
public void Runtime_code_does_not_use_raw_NpgsqlConnection_outside_allowlist()
{
var offenders = EnumerateRuntimeSourceFiles()
.Where(file => File.ReadAllText(file).Contains("new NpgsqlConnection(", StringComparison.Ordinal))
.Select(ToRelativePath)
.Where(file => !AllowedRuntimeRawConnectionFiles.Contains(file))
.ToList();
offenders.Should().BeEmpty(
"runtime PostgreSQL callers should use named data sources unless they are an explicit CLI/setup/migration/diagnostic exception");
}
[Fact]
public void Runtime_valkey_connections_must_stamp_client_name()
{
var offenders = EnumerateRuntimeSourceFiles()
.Where(file =>
{
var content = File.ReadAllText(file);
var relativePath = ToRelativePath(file);
var usesValkeyMultiplexer =
content.Contains("ConnectionMultiplexer.Connect(", StringComparison.Ordinal)
|| content.Contains("ConnectionMultiplexer.ConnectAsync(", StringComparison.Ordinal);
if (!usesValkeyMultiplexer || AllowedRuntimeValkeyConnectionFiles.Contains(relativePath))
{
return false;
}
return !content.Contains("ClientName", StringComparison.Ordinal);
})
.Select(ToRelativePath)
.ToList();
offenders.Should().BeEmpty(
"runtime Valkey callers should stamp a stable ClientName unless they are an explicit CLI/tooling exception");
}
[Fact]
public void Known_runtime_http_hotspots_do_not_allocate_ad_hoc_HttpClient()
{
var offenders = EnumerateRuntimeSourceFiles()
.Where(file => KnownHttpLifecycleHotspots.Contains(ToRelativePath(file)))
.Where(file => File.ReadAllText(file).Contains("new HttpClient(", StringComparison.Ordinal))
.Select(ToRelativePath)
.ToList();
offenders.Should().BeEmpty(
"the scoped HTTP hardening waves removed raw runtime HttpClient allocation from the known host-owned hotspots");
}
private static IEnumerable<string> EnumerateRuntimeSourceFiles()
{
var srcRoot = Path.Combine(RepoRoot, "src");
return Directory.EnumerateFiles(srcRoot, "*.cs", SearchOption.AllDirectories)
.Where(file => !PathSegments(file).Any(segment => segment.EndsWith(".Tests", StringComparison.Ordinal)))
.Where(file => !PathSegments(file).Any(segment => segment.EndsWith(".Benchmarks", StringComparison.Ordinal)))
.Where(file => !file.Contains($"{Path.DirectorySeparatorChar}Testing{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.Where(file => !file.Contains($"{Path.DirectorySeparatorChar}__Tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.Where(file => !file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.Where(file => !file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal));
}
private static string FindRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var srcPath = Path.Combine(current.FullName, "src");
var docsPath = Path.Combine(current.FullName, "docs");
if (Directory.Exists(srcPath) && Directory.Exists(docsPath))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Failed to locate the Stella Ops repository root from the test output directory.");
}
private static string ToRelativePath(string fullPath)
=> Path.GetRelativePath(RepoRoot, fullPath).Replace('\\', '/');
private static IEnumerable<string> PathSegments(string fullPath)
=> fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-GUARD | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: shared PostgreSQL/Valkey transport policy tests and static guardrails for runtime transport construction plus the scoped second-wave HTTP hotspot regressions. |
| AUDIT-0028-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0028-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0028-A | DONE | Waived (test project; revalidated 2026-01-08). |