Stabilize web test lane warning cleanup
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
## Dependencies & Concurrency
|
||||
- Depends on `docs/implplan/SPRINT_20260405_008_Integrations_consul_pg_router_runtime_tuning.md` for the PostgreSQL runtime logging baseline.
|
||||
- Depends on `docs/implplan/SPRINT_20260405_010_AdvisoryAI_pg_pooling_and_gitea_spike_followup.md` for the proven AdvisoryAI regression pattern and remediation baseline.
|
||||
- Cross-module edits allowed for `src/AdvisoryAI/**`, `src/AirGap/**`, `src/Attestor/**`, `src/Authority/**`, `src/BinaryIndex/**`, `src/Concelier/**`, `src/Doctor/**`, `src/EvidenceLocker/**`, `src/Findings/**`, `src/Graph/**`, `src/Integrations/**`, `src/JobEngine/**`, `src/Notify/**`, `src/Platform/**`, `src/Policy/**`, `src/ReachGraph/**`, `src/ReleaseOrchestrator/**`, `src/Scanner/**`, `src/Signals/**`, `src/Timeline/**`, `src/Router/**`, `src/Plugin/**`, `src/Workflow/**`, `docs/**`, and `devops/**` when they consume the shared transport conventions.
|
||||
- Cross-module edits allowed for `src/AdvisoryAI/**`, `src/AirGap/**`, `src/Attestor/**`, `src/Authority/**`, `src/BinaryIndex/**`, `src/Cli/**`, `src/Concelier/**`, `src/Doctor/**`, `src/EvidenceLocker/**`, `src/Findings/**`, `src/Graph/**`, `src/Integrations/**`, `src/JobEngine/**`, `src/Notify/**`, `src/Platform/**`, `src/Policy/**`, `src/ReachGraph/**`, `src/ReleaseOrchestrator/**`, `src/Scanner/**`, `src/Signals/**`, `src/Timeline/**`, `src/Router/**`, `src/Plugin/**`, `src/Workflow/**`, `docs/**`, and `devops/**` when they consume the shared transport conventions.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
|
||||
@@ -25,6 +25,8 @@
|
||||
- `src/__Tests/AGENTS.md`
|
||||
- `src/AirGap/StellaOps.AirGap.Policy/AGENTS.md`
|
||||
- `src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AGENTS.md`
|
||||
- `src/Cli/AGENTS.md`
|
||||
- `src/Cli/StellaOps.Cli/AGENTS.md`
|
||||
- `src/ReleaseOrchestrator/AGENTS.md`
|
||||
- `src/Workflow/AGENTS.md`
|
||||
|
||||
@@ -148,6 +150,19 @@ Completion criteria:
|
||||
- [x] The remaining raw IntegrationHub connector `HttpClient` constructions route through `ConnectorHttpClients.CreateClient(...)` instead of the default handler path.
|
||||
- [x] The shared convention suite and targeted IntegrationHub tests cover the broadened ReleaseOrchestrator connector hotspot set.
|
||||
|
||||
### XPORT-HTTP-010 - Finish CLI fallback hardening and convert the HTTP guardrail to an allowlist
|
||||
Status: DONE
|
||||
Dependency: XPORT-HTTP-009
|
||||
Owners: Developer
|
||||
Task description:
|
||||
- Replace the remaining CLI command/setup default-handler `HttpClient` fallbacks with a shared compatibility helper so CLI runtime paths no longer allocate independent transport pools when named DI clients are unavailable.
|
||||
- Tighten the shared HTTP convention test from a hotspot list into an explicit allowlist covering only the remaining documented compatibility wrappers and diagnostics/local-socket transports.
|
||||
|
||||
Completion criteria:
|
||||
- [x] CLI runtime command/setup fallbacks use a shared compatibility helper instead of raw default-handler `new HttpClient()` construction.
|
||||
- [x] The shared convention suite fails any new runtime `HttpClient` construction outside the explicit allowlist.
|
||||
- [x] CLI task boards, shared transport docs, and sprint notes reflect the narrowed set of intentional HTTP exceptions.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
@@ -168,6 +183,8 @@ Completion criteria:
|
||||
| 2026-04-05 | Validation: `dotnet build src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj` and `dotnet test src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj` passed. | Developer |
|
||||
| 2026-04-06 | Added `src/ReleaseOrchestrator/AGENTS.md`, routed the remaining IntegrationHub SCM, settings-store, and registry connectors through `ConnectorHttpClients.CreateClient(...)`, and added focused helper coverage for isolated shared-handler client creation. | Developer |
|
||||
| 2026-04-06 | Validation: `dotnet build src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/StellaOps.ReleaseOrchestrator.IntegrationHub.csproj`, `dotnet test src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests.csproj`, and `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed. | Developer |
|
||||
| 2026-04-06 | Added `CliHttpClients`, moved the remaining CLI command/setup fallback call sites onto the shared compatibility helper, and replaced the narrow HTTP hotspot regression check with a repo-wide allowlisted runtime `HttpClient` guardrail. | Developer |
|
||||
| 2026-04-06 | Validation: `dotnet build src/Cli/StellaOps.Cli/StellaOps.Cli.csproj` passed; `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed `82/82`; `src/Cli/__Tests/StellaOps.Cli.Tests/bin/Debug/net10.0/StellaOps.Cli.Tests.exe -class StellaOps.Cli.Tests.Services.CliHttpClientsTests` passed `3/3`. A full `dotnet test src/Cli/__Tests/StellaOps.Cli.Tests/StellaOps.Cli.Tests.csproj --filter CliHttpClientsTests` attempt showed that Microsoft Testing Platform ignored the VSTest filter and ran the full assembly, which still has seven unrelated existing failures. | Developer |
|
||||
|
||||
## Decisions & Risks
|
||||
- The first implementation wave standardizes PostgreSQL fully and applies the same lifecycle/attribution rule to other transports only where the existing runtime code already exposes a shared construction seam.
|
||||
@@ -175,8 +192,9 @@ Completion criteria:
|
||||
- Cross-module service patches will be kept minimal and tied back to the shared standard rather than introducing per-service bespoke option models where the shared library can carry the behavior.
|
||||
- The static guardrail now enforces anonymous `NpgsqlDataSource.Create(...)`, unnamed `NpgsqlDataSourceBuilder`, and raw runtime `NpgsqlConnection` usage outside an explicit allowlist.
|
||||
- The Valkey convention guardrail now also fails unnamed runtime `ConnectionMultiplexer.Connect(...)` / `ConnectAsync(...)` call sites outside explicit CLI/tooling/test exceptions.
|
||||
- The first shared HTTP guardrail is intentionally narrow: it covers the known host-owned hotspot files patched in this sprint, while broader repo-wide HTTP enforcement remains a follow-up because several legacy connectors and tools still create transport-specific temporary clients.
|
||||
- The shared HTTP guardrail is now repo-wide for runtime code: only the documented compatibility wrappers and explicit diagnostics/local-socket transports remain allowlisted for direct `new HttpClient(...)` construction.
|
||||
- AirGap's fallback egress wrapper now uses a shared handler while still returning isolated `HttpClient` instances per call, preserving caller-specific header/base-address configuration without paying the raw default-handler churn cost.
|
||||
- xUnit v3 CLI tests currently need direct runner filters such as `StellaOps.Cli.Tests.exe -class ...` for targeted validation because Microsoft Testing Platform ignores legacy VSTest `--filter` arguments in this project.
|
||||
- Integrations now activates connector plugins through DI when a service provider is available, which lets built-in runtime plugins consume named factory-backed clients without breaking reflection-only callers that still rely on default construction.
|
||||
- ReleaseOrchestrator IntegrationHub connectors still do not use `IHttpClientFactory`; this sprint broadens the shared-handler compatibility path across SCM, settings-store, and registry connectors so they stop allocating default-handler clients while preserving per-connector client isolation.
|
||||
- ReleaseOrchestrator's compatibility wrapper is still not safe to client-cache broadly because many connectors mutate `DefaultRequestHeaders` with per-connector auth state; a future refactor needs request-scoped headers or typed/factory clients before shared client instances can be introduced there.
|
||||
@@ -184,10 +202,9 @@ Completion criteria:
|
||||
- The remaining explicit raw-connection allowlist is intentionally narrow: CLI/setup, migrations, diagnostics, and `PlatformMigrationAdminService`.
|
||||
- Shared Valkey factories that do not receive a service-specific name now apply a module-level fallback `ClientName`; this restores baseline attribution, but Router transport callers may still want a future option for per-service Valkey identity.
|
||||
- Shared transport rules are documented in `docs/technical/runtime-transport-client-rules.md`.
|
||||
- HTTP compatibility fallbacks now live behind module-specific wrappers (`Integrations` shared defaults, `ReleaseOrchestrator` shared-handler connector clients, OCI helper shared clients) so hotspot files no longer construct raw clients directly; broader HTTP sweeps should continue to replace the remaining wrappers with true host-managed factories where possible.
|
||||
- HTTP compatibility fallbacks now live behind module-specific wrappers (`Integrations` shared defaults, `ReleaseOrchestrator` shared-handler connector clients, CLI shared compatibility clients, AirGap egress fallback, and OCI helper shared clients) so runtime hotspot files no longer construct raw clients directly.
|
||||
- The remaining runtime `HttpClient` allowlist is explicit: AirGap compatibility fallback, CLI compatibility fallback, ReleaseOrchestrator compatibility wrapper, Doctor environment TLS probe, and Zastava Docker local-socket transport.
|
||||
|
||||
## Next Checkpoints
|
||||
- Continue the broader HTTP/SCM/Vault-style lifecycle sweep (ReleaseOrchestrator SCM/cloud connectors, any remaining tool-specific temporary clients, and factory adoption for the compatibility wrappers added here) with the same guardrail approach.
|
||||
- Continue the broader HTTP/SCM/Vault-style lifecycle sweep with special focus on connector stacks that still mutate `DefaultRequestHeaders` on shared compatibility clients, because those need request-scoped auth/header refactors before client caching is safe.
|
||||
- Continue the connector HTTP sweep with request-scoped auth/header refactors for ReleaseOrchestrator and the remaining CLI fallbacks, because those are now the main sources of duplicated runtime client setup after the shared-handler migration.
|
||||
- Evaluate whether Workflow should move from normalized raw `NpgsqlConnection` usage to a module-scoped `NpgsqlDataSource` wrapper in a future storage refactor, but it is no longer a blocker for the shared convention suite.
|
||||
- Optional future refinement: convert the remaining documented HTTP compatibility wrappers to true typed/factory-managed clients where host DI seams already exist.
|
||||
- Optional future refinement: evaluate whether Workflow should move from normalized raw `NpgsqlConnection` usage to a module-scoped `NpgsqlDataSource` wrapper, though it is no longer a blocker for the shared convention suite.
|
||||
|
||||
@@ -19,10 +19,11 @@ This document defines the minimum lifecycle and attribution rules for long-lived
|
||||
- When DI-backed wiring is not available yet, compatibility fallbacks must still avoid per-request or per-call `new HttpClient()` churn.
|
||||
- Compatibility wrappers may still return per-call `HttpClient` instances when callers need isolated headers or base addresses, but those wrappers should share the underlying handler/pool rather than constructing default-handler clients repeatedly.
|
||||
- Plugin loaders that activate runtime components should use service-provider-backed construction when available so named clients and other shared transports can flow into plugins.
|
||||
- Existing analyzer-based guardrails remain in place for specialized modules, and the shared convention suite now covers the scoped host-owned HTTP hotspot waves across Integrations, ReleaseOrchestrator connector helpers plus the broadened SCM/settings-store/registry connector set, and OCI fallback publishers.
|
||||
- Existing analyzer-based guardrails remain in place for specialized modules, and the shared convention suite now enforces a repo-wide runtime `HttpClient` allowlist.
|
||||
- The current explicit runtime HTTP exceptions are: AirGap fallback wrapper, CLI fallback wrapper, ReleaseOrchestrator connector compatibility wrapper, Doctor's per-probe TLS capture client, and Zastava's Docker local-socket transport client.
|
||||
|
||||
## Static Enforcement
|
||||
- `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs` enforces the shared PostgreSQL and Valkey runtime construction rules plus the scoped HTTP hotspot regression checks.
|
||||
- `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs` enforces the shared PostgreSQL and Valkey runtime construction rules plus the repo-wide runtime HTTP allowlist.
|
||||
|
||||
## Operational Goal
|
||||
- Every long-lived runtime transport should be attributable in production diagnostics without relying on IP-only correlation.
|
||||
|
||||
@@ -1111,8 +1111,7 @@ internal static class AdviseChatCommandGroup
|
||||
}
|
||||
|
||||
// Create manually with HttpClient
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory?.CreateClient("ChatClient") ?? new HttpClient();
|
||||
var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "ChatClient");
|
||||
|
||||
return new ChatClient(httpClient, options);
|
||||
}
|
||||
|
||||
@@ -1348,7 +1348,7 @@ public static class BundleVerifyCommand
|
||||
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
|
||||
using var http = StellaOps.Cli.Services.CliHttpClients.CreateClient(timeout: TimeSpan.FromSeconds(60));
|
||||
var url = $"{blobSource.TrimEnd('/')}/v2/_blobs/{digest}";
|
||||
var response = await http.GetAsync(url, ct);
|
||||
if (response.IsSuccessStatusCode)
|
||||
|
||||
@@ -425,7 +425,8 @@ public static class ChainCommandGroup
|
||||
}
|
||||
|
||||
// Call the chain API
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var endpoint = direction switch
|
||||
{
|
||||
ChainDirection.Upstream => $"api/v1/chains/{Uri.EscapeDataString(attestationId)}/upstream?maxDepth={maxDepth}",
|
||||
@@ -502,7 +503,8 @@ public static class ChainCommandGroup
|
||||
}
|
||||
|
||||
// Call the chain API
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var targetId = attestationId;
|
||||
if (string.IsNullOrWhiteSpace(targetId) && !string.IsNullOrWhiteSpace(artifact))
|
||||
{
|
||||
@@ -585,7 +587,8 @@ public static class ChainCommandGroup
|
||||
}
|
||||
|
||||
// Call the chain graph API
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var formatParam = graphFormat.ToString().ToLowerInvariant();
|
||||
var endpoint = $"api/v1/chains/{Uri.EscapeDataString(attestationId)}/graph?format={formatParam}&maxDepth={maxDepth}";
|
||||
|
||||
@@ -652,7 +655,8 @@ public static class ChainCommandGroup
|
||||
}
|
||||
|
||||
// Call the layer attestation API
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var endpoint = $"api/v1/attestor/layers/{Uri.EscapeDataString(image)}";
|
||||
|
||||
var response = await httpClient.GetAsync(endpoint, ct);
|
||||
@@ -708,7 +712,8 @@ public static class ChainCommandGroup
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var endpoint = $"api/v1/attestor/layers/{Uri.EscapeDataString(image)}/{layerIndex}";
|
||||
|
||||
var response = await httpClient.GetAsync(endpoint, ct);
|
||||
@@ -775,7 +780,8 @@ public static class ChainCommandGroup
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(backendUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(backendUrl));
|
||||
var endpoint = $"api/v1/attestor/layers/{Uri.EscapeDataString(image)}/create?sign={sign}";
|
||||
|
||||
var response = await httpClient.PostAsync(endpoint, null, ct);
|
||||
|
||||
@@ -193,8 +193,8 @@ public static class CheckpointCommands
|
||||
Console.WriteLine($"Exporting checkpoint from {instance}...");
|
||||
Console.WriteLine();
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.BaseAddress = new Uri(instance.TrimEnd('/') + "/");
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(instance.TrimEnd('/') + "/"));
|
||||
|
||||
// Fetch current checkpoint
|
||||
Console.Write("Fetching checkpoint...");
|
||||
|
||||
@@ -115,8 +115,7 @@ public static class DbCommandGroup
|
||||
}
|
||||
|
||||
// Make API request
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory?.CreateClient("Api") ?? new HttpClient();
|
||||
var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "Api");
|
||||
|
||||
DbStatusResponse? response = null;
|
||||
try
|
||||
|
||||
@@ -1898,7 +1898,8 @@ public static class EvidenceCommandGroup
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(serverUrl));
|
||||
|
||||
// Get reindex impact assessment
|
||||
var assessmentUrl = $"/api/v1/evidence/reindex/assess?since={since?.ToString("O") ?? ""}&batchSize={batchSize}";
|
||||
@@ -2057,7 +2058,8 @@ public static class EvidenceCommandGroup
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(serverUrl));
|
||||
|
||||
// Request continuity verification
|
||||
var verifyUrl = $"/api/v1/evidence/continuity/verify?oldRoot={Uri.EscapeDataString(oldRoot)}&newRoot={Uri.EscapeDataString(newRoot)}";
|
||||
@@ -2232,7 +2234,8 @@ public static class EvidenceCommandGroup
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(serverUrl));
|
||||
|
||||
if (rollback)
|
||||
{
|
||||
|
||||
@@ -810,8 +810,7 @@ public static class ExceptionCommandGroup
|
||||
|
||||
private static HttpClient CreateHttpClient(IServiceProvider services, StellaOpsCliOptions options)
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var client = httpClientFactory?.CreateClient("PolicyGateway") ?? new HttpClient();
|
||||
var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "PolicyGateway");
|
||||
|
||||
if (client.BaseAddress is null)
|
||||
{
|
||||
|
||||
@@ -234,8 +234,7 @@ public static class ExplainCommandGroup
|
||||
var options = services.GetService<StellaOpsCliOptions>();
|
||||
|
||||
// Get HTTP client
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
using var httpClient = httpClientFactory?.CreateClient("PolicyGateway") ?? new HttpClient();
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "PolicyGateway");
|
||||
|
||||
var baseUrl = options?.BackendUrl?.TrimEnd('/')
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
|
||||
@@ -271,9 +271,7 @@ public static class GateCommandGroup
|
||||
};
|
||||
|
||||
// Call API
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
using var client = httpClientFactory?.CreateClient("PolicyGateway")
|
||||
?? new HttpClient();
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "PolicyGateway");
|
||||
|
||||
// Configure base address if not set
|
||||
if (client.BaseAddress is null)
|
||||
|
||||
@@ -398,8 +398,7 @@ public static class GuardCommandGroup
|
||||
IAnsiConsole console,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
using var client = httpClientFactory?.CreateClient("Scanner") ?? new HttpClient();
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "Scanner");
|
||||
|
||||
var baseUrl = serverUrl
|
||||
?? Environment.GetEnvironmentVariable("STELLA_SCANNER_URL")
|
||||
|
||||
@@ -574,8 +574,7 @@ public static class LayerSbomCommandGroup
|
||||
|
||||
private static HttpClient CreateHttpClient(IServiceProvider services, StellaOpsCliOptions options)
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var client = httpClientFactory?.CreateClient("ScannerService") ?? new HttpClient();
|
||||
var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "ScannerService");
|
||||
|
||||
if (client.BaseAddress is null)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,8 @@ public static class ReachGraphCommandHandlers
|
||||
string output,
|
||||
string apiUrl)
|
||||
{
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl) };
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(apiUrl));
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-ID", "cli-user");
|
||||
|
||||
// Build query string
|
||||
@@ -93,7 +94,8 @@ public static class ReachGraphCommandHandlers
|
||||
bool verbose,
|
||||
string apiUrl)
|
||||
{
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl) };
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(apiUrl));
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-ID", "cli-user");
|
||||
|
||||
// Parse input files
|
||||
@@ -214,7 +216,8 @@ public static class ReachGraphCommandHandlers
|
||||
string digest,
|
||||
string apiUrl)
|
||||
{
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl) };
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(apiUrl));
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-ID", "cli-user");
|
||||
|
||||
try
|
||||
|
||||
@@ -657,8 +657,7 @@ public static class ReplayCommandGroup
|
||||
}
|
||||
|
||||
var options = services.GetService<StellaOpsCliOptions>();
|
||||
var clientFactory = services.GetService<IHttpClientFactory>();
|
||||
var httpClient = clientFactory?.CreateClient() ?? new HttpClient();
|
||||
var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(services);
|
||||
var baseUrl = options?.BackendUrl
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
?? "http://localhost:10011";
|
||||
|
||||
@@ -4468,7 +4468,7 @@ public static class SbomCommandGroup
|
||||
// In production, this would use HttpOciRegistryClient with auth.
|
||||
// For now, use the CLI's configured registry client.
|
||||
return new StellaOps.Cli.Services.OciAttestationRegistryClient(
|
||||
new HttpClient(),
|
||||
StellaOps.Cli.Services.CliHttpClients.CreateClient(),
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger<StellaOps.Cli.Services.OciAttestationRegistryClient>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -1534,9 +1534,7 @@ public static class ScoreCommandGroup
|
||||
StellaOpsCliOptions options,
|
||||
int timeout)
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var client = httpClientFactory?.CreateClient("Platform")
|
||||
?? new HttpClient();
|
||||
var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "Platform");
|
||||
|
||||
if (client.BaseAddress is null)
|
||||
{
|
||||
|
||||
@@ -1206,9 +1206,7 @@ public static class ScoreGateCommandGroup
|
||||
StellaOpsCliOptions options,
|
||||
int timeout)
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var client = httpClientFactory?.CreateClient("PolicyGateway")
|
||||
?? new HttpClient();
|
||||
var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "PolicyGateway");
|
||||
|
||||
if (client.BaseAddress is null)
|
||||
{
|
||||
|
||||
@@ -141,10 +141,7 @@ public static class ScoreReplayCommandGroup
|
||||
}
|
||||
|
||||
// Make API request
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
var httpClient = TryCreateClient(httpClientFactory, "Platform")
|
||||
?? TryCreateClient(httpClientFactory, "PlatformApi")
|
||||
?? new HttpClient();
|
||||
var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "Platform");
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
@@ -351,23 +348,6 @@ public static class ScoreReplayCommandGroup
|
||||
return $"{parts[0].ToLowerInvariant()}:{parts[1].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static HttpClient? TryCreateClient(IHttpClientFactory? factory, string name)
|
||||
{
|
||||
if (factory is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return factory.CreateClient(name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static T? TryDeserialize<T>(string payload)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
|
||||
@@ -347,8 +347,8 @@ public sealed class LlmSetupStep : SetupStepBase
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||
|
||||
var response = await client.GetAsync("https://api.openai.com/v1/models", ct);
|
||||
@@ -369,8 +369,8 @@ public sealed class LlmSetupStep : SetupStepBase
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
client.DefaultRequestHeaders.Add("x-api-key", apiKey);
|
||||
client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
@@ -395,8 +395,8 @@ public sealed class LlmSetupStep : SetupStepBase
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
|
||||
var response = await client.GetAsync(
|
||||
$"https://generativelanguage.googleapis.com/v1beta/models?key={apiKey}",
|
||||
|
||||
@@ -184,9 +184,10 @@ public sealed class RegistrySetupStep : SetupStepBase
|
||||
handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
|
||||
}
|
||||
|
||||
using var client = new HttpClient(handler);
|
||||
client.BaseAddress = new Uri(url.TrimEnd('/'));
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
handler,
|
||||
baseAddress: new Uri(url.TrimEnd('/')),
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
|
||||
// Add basic auth if credentials provided
|
||||
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
|
||||
|
||||
@@ -291,7 +291,8 @@ public sealed class ScmSetupStep : SetupStepBase
|
||||
Dictionary<string, string> config,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
|
||||
var baseUrl = config.TryGetValue("scm.url", out var url) ? url.TrimEnd('/') : "";
|
||||
|
||||
|
||||
@@ -381,9 +381,9 @@ public sealed class SettingsStoreSetupStep : SetupStepBase
|
||||
string? token,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.BaseAddress = new Uri(address.TrimEnd('/'));
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(address.TrimEnd('/')),
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
@@ -399,9 +399,9 @@ public sealed class SettingsStoreSetupStep : SetupStepBase
|
||||
|
||||
private static async Task<string> TestEtcdConnectionAsync(string endpoint, CancellationToken ct)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.BaseAddress = new Uri(endpoint.TrimEnd('/'));
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(endpoint.TrimEnd('/')),
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
|
||||
var response = await client.GetAsync("/version", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -177,8 +177,8 @@ public sealed class TelemetrySetupStep : SetupStepBase
|
||||
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||||
{
|
||||
// HTTP/gRPC endpoint - try to connect
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
timeout: TimeSpan.FromSeconds(5));
|
||||
|
||||
// OTLP HTTP uses different paths for different signals
|
||||
// Try the root or a health check path
|
||||
|
||||
@@ -305,8 +305,8 @@ public sealed class VaultSetupStep : SetupStepBase
|
||||
string? ns,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.BaseAddress = new Uri(address.TrimEnd('/'));
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri(address.TrimEnd('/')));
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
|
||||
@@ -229,8 +229,7 @@ public static class TimelineCommandGroup
|
||||
}
|
||||
|
||||
var options = services.GetService<StellaOpsCliOptions>();
|
||||
var factory = services.GetService<IHttpClientFactory>();
|
||||
var client = factory?.CreateClient() ?? new HttpClient();
|
||||
var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services);
|
||||
var baseUrl = options?.BackendUrl
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
?? "http://localhost:10011";
|
||||
|
||||
@@ -83,7 +83,9 @@ internal static class TrustCommandHandlers
|
||||
// Fetch initial TUF metadata
|
||||
Console.WriteLine($"Fetching TUF metadata from {tufUrl}...");
|
||||
|
||||
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
services,
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
|
||||
// Fetch root.json
|
||||
var rootResponse = await httpClient.GetAsync($"{tufUrl.TrimEnd('/')}/root.json", cancellationToken);
|
||||
@@ -168,7 +170,9 @@ internal static class TrustCommandHandlers
|
||||
|
||||
Console.WriteLine($"Syncing TUF metadata from {config.TufUrl}...");
|
||||
|
||||
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||
using var httpClient = StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
services,
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
var tufUrl = config.TufUrl.TrimEnd('/');
|
||||
|
||||
// Fetch timestamp first (freshness indicator)
|
||||
|
||||
@@ -169,9 +169,7 @@ public static class VexGateScanCommandGroup
|
||||
}
|
||||
|
||||
// Call API
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
using var client = httpClientFactory?.CreateClient("ScannerService")
|
||||
?? new HttpClient();
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "ScannerService");
|
||||
|
||||
// Configure base address if not set
|
||||
if (client.BaseAddress is null)
|
||||
@@ -286,9 +284,7 @@ public static class VexGateScanCommandGroup
|
||||
}
|
||||
|
||||
// Call API
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
using var client = httpClientFactory?.CreateClient("ScannerService")
|
||||
?? new HttpClient();
|
||||
using var client = StellaOps.Cli.Services.CliHttpClients.CreateClient(services, "ScannerService");
|
||||
|
||||
// Configure base address if not set
|
||||
if (client.BaseAddress is null)
|
||||
|
||||
@@ -572,11 +572,10 @@ internal static class WatchlistCommandHandlers
|
||||
|
||||
private static HttpClient GetHttpClient(IServiceProvider services)
|
||||
{
|
||||
var factory = services.GetService(typeof(IHttpClientFactory)) as IHttpClientFactory;
|
||||
return factory?.CreateClient("AttestorApi") ?? new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost:5200")
|
||||
};
|
||||
return StellaOps.Cli.Services.CliHttpClients.CreateClient(
|
||||
services,
|
||||
"AttestorApi",
|
||||
baseAddress: new Uri("http://localhost:5200"));
|
||||
}
|
||||
|
||||
private static string BuildDisplayName(string? issuer, string? san, string? keyId)
|
||||
|
||||
@@ -186,6 +186,9 @@ internal static class Program
|
||||
})
|
||||
.AddEgressPolicyGuard("stellaops-cli", "sources-ingest");
|
||||
|
||||
services.AddHttpClient("stellaops-cli.compat")
|
||||
.AddEgressPolicyGuard("stellaops-cli", "compat");
|
||||
|
||||
services.AddSingleton<IScannerExecutor, ScannerExecutor>();
|
||||
services.AddSingleton<IScannerInstaller, ScannerInstaller>();
|
||||
services.AddSingleton<MigrationCommandService>();
|
||||
|
||||
73
src/Cli/StellaOps.Cli/Services/CliHttpClients.cs
Normal file
73
src/Cli/StellaOps.Cli/Services/CliHttpClients.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
internal static class CliHttpClients
|
||||
{
|
||||
private const string CompatibilityClientName = "stellaops-cli.compat";
|
||||
private static readonly SocketsHttpHandler SharedHandler = new()
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
|
||||
};
|
||||
|
||||
public static HttpClient CreateClient(
|
||||
IServiceProvider services,
|
||||
string? name = null,
|
||||
Uri? baseAddress = null,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
var factory = services.GetService<IHttpClientFactory>();
|
||||
return CreateClient(factory, name, baseAddress, timeout);
|
||||
}
|
||||
|
||||
public static HttpClient CreateClient(
|
||||
IHttpClientFactory? factory = null,
|
||||
string? name = null,
|
||||
Uri? baseAddress = null,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
var client = factory is null
|
||||
? new HttpClient(SharedHandler, disposeHandler: false)
|
||||
: factory.CreateClient(string.IsNullOrWhiteSpace(name) ? CompatibilityClientName : name);
|
||||
|
||||
Configure(client, baseAddress, timeout);
|
||||
return client;
|
||||
}
|
||||
|
||||
public static HttpClient CreateClient(
|
||||
HttpMessageHandler handler,
|
||||
Uri? baseAddress = null,
|
||||
TimeSpan? timeout = null,
|
||||
bool disposeHandler = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler);
|
||||
Configure(client, baseAddress, timeout);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static void Configure(HttpClient client, Uri? baseAddress, TimeSpan? timeout)
|
||||
{
|
||||
if (baseAddress is not null)
|
||||
{
|
||||
client.BaseAddress = baseAddress;
|
||||
}
|
||||
|
||||
if (timeout.HasValue)
|
||||
{
|
||||
client.Timeout = timeout.Value;
|
||||
}
|
||||
|
||||
if (!client.DefaultRequestHeaders.UserAgent.Any())
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps.Cli", "1.0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| SPRINT_20260208_030-CORE | DONE | Added `stella advise ask --file` batch processing and `stella advise export` conversation history command surfaces (2026-02-08). |
|
||||
| SPRINT_20260208_033-CORE | DONE | Unknowns export schema/versioning envelope and CLI option integration completed (2026-02-08). |
|
||||
| STS-004 | DONE | SPRINT_20260210_004 - Added `stella verify release` command that maps to promotion bundle verification flow. |
|
||||
| SPRINT_20260406_011-XPORT-HTTP | DONE | Replaced remaining CLI command/setup default-handler `HttpClient` fallbacks with `CliHttpClients` and aligned the module with the repo-wide runtime transport guardrail. |
|
||||
|
||||
| SPRINT_20260208_031-CORE | DONE | Compare verification overlay options, builder, and output/model integration completed (2026-02-08).
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Cli.Services;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Services;
|
||||
|
||||
public sealed class CliHttpClientsTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateClient_without_factory_returns_isolated_clients_with_defaults()
|
||||
{
|
||||
using var first = CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri("https://api.example/"),
|
||||
timeout: TimeSpan.FromSeconds(5));
|
||||
using var second = CliHttpClients.CreateClient(
|
||||
baseAddress: new Uri("https://api.example/"));
|
||||
|
||||
Assert.NotSame(first, second);
|
||||
Assert.Equal(new Uri("https://api.example/"), first.BaseAddress);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), first.Timeout);
|
||||
Assert.NotEmpty(first.DefaultRequestHeaders.UserAgent);
|
||||
Assert.NotEmpty(second.DefaultRequestHeaders.UserAgent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateClient_with_factory_uses_requested_name()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var factory = new RecordingHttpClientFactory();
|
||||
services.AddSingleton<IHttpClientFactory>(factory);
|
||||
|
||||
using var serviceProvider = services.BuildServiceProvider();
|
||||
using var client = CliHttpClients.CreateClient(
|
||||
serviceProvider,
|
||||
"PolicyGateway",
|
||||
baseAddress: new Uri("https://policy.example/"),
|
||||
timeout: TimeSpan.FromSeconds(9));
|
||||
|
||||
Assert.Equal("PolicyGateway", factory.LastName);
|
||||
Assert.Equal(new Uri("https://policy.example/"), client.BaseAddress);
|
||||
Assert.Equal(TimeSpan.FromSeconds(9), client.Timeout);
|
||||
Assert.NotEmpty(client.DefaultRequestHeaders.UserAgent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateClient_with_handler_applies_requested_configuration()
|
||||
{
|
||||
using var client = CliHttpClients.CreateClient(
|
||||
new HttpClientHandler(),
|
||||
baseAddress: new Uri("https://registry.example/"),
|
||||
timeout: TimeSpan.FromSeconds(11));
|
||||
|
||||
Assert.Equal(new Uri("https://registry.example/"), client.BaseAddress);
|
||||
Assert.Equal(TimeSpan.FromSeconds(11), client.Timeout);
|
||||
Assert.NotEmpty(client.DefaultRequestHeaders.UserAgent);
|
||||
}
|
||||
|
||||
private sealed class RecordingHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public string? LastName { get; private set; }
|
||||
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
LastName = name;
|
||||
return new HttpClient(new HttpClientHandler(), disposeHandler: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| SPRINT_20260208_030-TESTS | DONE | Added isolated advise parity validation in StellaOps.Cli.AdviseParity.Tests; command passed (2 tests, 2026-02-08).
|
||||
| SPRINT_20260208_033-TESTS | DONE | Added isolated Unknowns export deterministic validation in StellaOps.Cli.UnknownsExport.Tests; command passed (3 tests, 2026-02-08).
|
||||
| STS-005 | DONE | SPRINT_20260210_004 - Updated command structure coverage for `verify release` and verification consolidation list (execution blocked by pre-existing Policy.Determinization compile errors). |
|
||||
| SPRINT_20260406_011-XPORT-HTTP-T | DONE | Added `CliHttpClients` compatibility-helper coverage and validated the repo-wide runtime HTTP allowlist against the CLI fallback cleanup. |
|
||||
|
||||
| SPRINT_20260208_031-TESTS | DONE | Isolated compare overlay deterministic validation added in StellaOps.Cli.CompareOverlay.Tests; command passed (3 tests, 2026-02-08).
|
||||
|
||||
|
||||
@@ -27,37 +27,14 @@ public sealed class RuntimePostgresConstructionConventionTests
|
||||
"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/GenericOciConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.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/Scm/AzureDevOpsConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.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",
|
||||
];
|
||||
private static readonly HashSet<string> AllowedRuntimeHttpClientFiles = new(StringComparer.Ordinal)
|
||||
{
|
||||
"src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs",
|
||||
"src/Cli/StellaOps.Cli/Services/CliHttpClients.cs",
|
||||
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Environment/Checks/EnvironmentConnectivityCheck.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/ConnectorHttpClients.cs",
|
||||
"src/Zastava/StellaOps.Zastava.Agent/Docker/DockerSocketClient.cs",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Runtime_code_does_not_use_anonymous_NpgsqlDataSource_Create()
|
||||
@@ -129,16 +106,16 @@ public sealed class RuntimePostgresConstructionConventionTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Known_runtime_http_hotspots_do_not_allocate_ad_hoc_HttpClient()
|
||||
public void Runtime_http_client_construction_is_restricted_to_explicit_wrappers_or_diagnostics()
|
||||
{
|
||||
var offenders = EnumerateRuntimeSourceFiles()
|
||||
.Where(file => KnownHttpLifecycleHotspots.Contains(ToRelativePath(file)))
|
||||
.Where(file => File.ReadAllText(file).Contains("new HttpClient(", StringComparison.Ordinal))
|
||||
.Select(ToRelativePath)
|
||||
.Where(file => !AllowedRuntimeHttpClientFiles.Contains(file))
|
||||
.ToList();
|
||||
|
||||
offenders.Should().BeEmpty(
|
||||
"the scoped HTTP hardening waves removed raw runtime HttpClient allocation from the known host-owned hotspots");
|
||||
"runtime HttpClient construction should stay behind explicit wrappers or documented diagnostics/local-socket exceptions");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateRuntimeSourceFiles()
|
||||
@@ -147,6 +124,7 @@ public sealed class RuntimePostgresConstructionConventionTests
|
||||
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 => !PathSegments(file).Any(segment => segment.Contains("Analyzers", 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))
|
||||
|
||||
Reference in New Issue
Block a user