Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.5.9",
"version": "5.5.11",
"commands": [
"reportgenerator"
],
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,4 @@ The Obsidian vault `H:\Obsidian\SmoothOperator` is the **canonical** knowledge b
- **Frontend nginx has two config files** — `frontend/nginx-main.conf` is the main context (loads the compiled `ngx_brotli` modules + worker tuning; the `/tmp` temp paths are mandatory under `read_only: true`), and `frontend/nginx.conf` is the server block. The brotli module is compiled from source in a dedicated Dockerfile build stage.
- **Trigram audit-log search** — the `AddTrigramSearchIndexes` migration adds `pg_trgm` expression GIN indexes on `lower(col)`. Keep `GetAuditLogsQuery` text filters as `EF.Functions.Like(col.ToLower(), pattern, "\\")`: this emits `LOWER(col) LIKE …` (uses the index on Postgres) and stays translatable on the SQLite test provider — `EF.Functions.ILike` would break the SQLite integration tests.
- **`SmoothOperator.Benchmarks` is intentionally excluded from `smooth-operator.sln`** so `dotnet test`/CI don't build it. Run it manually with `dotnet run -c Release`.
- **Two Sonar rules are `#pragma`-suppressed in `SmoothOperator.Api/Extensions/` as confirmed false positives** — same class as the other analyzer false positives in this list (the trigram/EF-query `CA1862` case, the Tailwind `css:S8776` case): the rule is right in general but wrong here, so it is suppressed with a reason comment rather than "fixed". `dotnet format` reports **no available code fix** for either, so `--verify-no-changes` cannot converge without the suppression. (1) `S2092` ("Set the `Secure` flag on this cookie") ×2 in `AuthCookieExtensions.cs` — `Secure` is set dynamically via `Secure = response.HttpContext.Request.IsHttps`; hardcoding `true` as Sonar wants makes the browser silently drop the cookie over plain HTTP, breaking login on local dev. (2) `S1313` ("Do not hardcode IP address") ×3 in `RateLimitingExtensions.cs` — the RFC1918 ranges `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` are the `KnownIPNetworks` trusted-proxy allowlist that lets ASP.NET Core honour `X-Forwarded-*` from the nginx sidecar; they are well-known constants, not environment-specific addresses. Both surfaced with the SonarAnalyzer.CSharp 10.25→10.31 bump. Keep each pragma scoped to its individual statement(s), never the whole file.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

<!-- SonarAnalyzer applied to all projects for code-smell detection at build time. -->
<ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.25.0.139117">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.31.0.145097">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public async Task<IActionResult> OidcCallback([FromQuery] string? code, [FromQue
{
await _audit.WriteAsync(AuditLoginFailed, ResourceTypeSso, "",
new { stage = "callback", reason = "idp_error", error });
return Redirect(_urls.FinalizeErrorUrl(Request, error!));
return Redirect(_urls.FinalizeErrorUrl(Request, error));
}

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static class AuthCookieExtensions
public static void SetAuthCookie(this HttpResponse response, string token)
{
var expiry = GetTokenExpiry(token);
#pragma warning disable S2092 // Secure is set dynamically from Request.IsHttps; hardcoding true drops the cookie over plain-HTTP local dev
response.Cookies.Append(CookieName, token, new CookieOptions
{
HttpOnly = true,
Expand All @@ -20,17 +21,20 @@ public static void SetAuthCookie(this HttpResponse response, string token)
Path = "/api/",
Expires = expiry,
});
#pragma warning restore S2092
}

public static void ClearAuthCookie(this HttpResponse response)
{
#pragma warning disable S2092 // Secure is set dynamically from Request.IsHttps; hardcoding true drops the cookie over plain-HTTP local dev
response.Cookies.Delete(CookieName, new CookieOptions
{
HttpOnly = true,
Secure = response.HttpContext.Request.IsHttps,
SameSite = SameSiteMode.Strict,
Path = "/api/",
});
#pragma warning restore S2092
}

private static DateTimeOffset? GetTokenExpiry(string token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private static Task WriteHealthCheckJson(HttpContext context, HealthReport repor
durationMs = e.Value.Duration.TotalMilliseconds,
}),
};
return context.Response.WriteAsync(JsonSerializer.Serialize(payload));
return context.Response.WriteAsync(JsonSerializer.Serialize(payload), context.RequestAborted);
}

private static bool HasValidMetricsBearerToken(HttpContext context, string? expectedToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ public static IServiceCollection AddApplicationForwardedHeaders(this IServiceCol
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownProxies.Clear();
#pragma warning disable S1313 // Deliberate RFC1918 private ranges: the trusted-proxy allowlist for the nginx sidecar, not environment-specific addresses
options.KnownIPNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
options.KnownIPNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
options.KnownIPNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
#pragma warning restore S1313
});
return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception
context.Response.ContentType = "application/json";

var body = JsonSerializer.Serialize(new { message });
await context.Response.WriteAsync(body);
await context.Response.WriteAsync(body, context.RequestAborted);
}
}
}
12 changes: 6 additions & 6 deletions backend/src/SmoothOperator.Api/SmoothOperator.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Keys" Version="1.2.4" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Keys" Version="1.6.3" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private void ApplyLocalSecret(Credential credential, CreateCredentialDto dto)
if (string.IsNullOrWhiteSpace(dto.Secret))
throw new BadRequestException("Secret is required for local credentials.");

credential.EncryptedSecret = _encryptionService.Encrypt(dto.Secret!);
credential.EncryptedSecret = _encryptionService.Encrypt(dto.Secret);
}

private async Task PersistAndAuditAsync(Credential credential, CancellationToken cancellationToken)
Expand Down Expand Up @@ -108,7 +108,7 @@ private async Task HandleExternalCredentialAsync(Credential credential, CreateCr

var secretProvider = _secretProviderFactory.Create(provider);
var secretName = $"smoothoperator-{SanitizeName(dto.Name)}-{Guid.NewGuid():N}";
await secretProvider.SetSecretAsync(secretName, dto.Secret!, cancellationToken);
await secretProvider.SetSecretAsync(secretName, dto.Secret, cancellationToken);

credential.ExternalSecretName = secretName;
credential.ExternalSecretVersion = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task<bool> Handle(UpdateCredentialCommand request, CancellationToke
var secretProvider = _secretProviderFactory.Create(provider);
var secretName = credential.ExternalSecretName
?? $"smoothoperator-{Sanitize(dto.Name)}-{Guid.NewGuid():N}";
await secretProvider.SetSecretAsync(secretName, dto.Secret!, cancellationToken);
await secretProvider.SetSecretAsync(secretName, dto.Secret, cancellationToken);
credential.ExternalSecretName = secretName;
credential.ExternalSecretVersion = null;
secretRotated = true;
Expand All @@ -93,7 +93,7 @@ await _audit.WriteAsync("credential.linked", "Credential", credential.Id.ToStrin

if (!string.IsNullOrEmpty(dto.Secret))
{
credential.EncryptedSecret = _encryptionService.Encrypt(dto.Secret!);
credential.EncryptedSecret = _encryptionService.Encrypt(dto.Secret);
secretRotated = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@

<!-- Options classes use DataAnnotations for validation -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.10" />
</ItemGroup>

<!-- CQRS / Validation / Mapping -->
<ItemGroup>
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="Mapster" Version="10.0.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="10.0.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="FluentValidation" Version="11.12.0" />
<PackageReference Include="Mapster" Version="10.0.11" />
<PackageReference Include="Mapster.DependencyInjection" Version="10.0.11" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Otp.NET" Version="1.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public async Task<string> IssueTicketAsync(
TicketPayload? payload;
try
{
payload = JsonSerializer.Deserialize<TicketPayload>((string)raw!);
payload = JsonSerializer.Deserialize<TicketPayload>(raw.ToString());
}
catch
{
Expand Down Expand Up @@ -963,7 +963,7 @@ private async Task RunBidirectionalProxyAsync(
private void ObserveRelayFault(Task relayTask, Action<string> onGuacError)
{
if (!relayTask.IsFaulted) return;
var ex = relayTask.Exception!.Flatten().InnerException ?? relayTask.Exception;
var ex = relayTask.Exception.Flatten().InnerException ?? relayTask.Exception;
_logger.LogError(ex, "Guacamole relay loop faulted unexpectedly");
onGuacError($"Internal relay error: {ex.Message}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private async Task SweepOnceAsync(CancellationToken ct)
&& r.Connection != null
&& r.Connection.ConnectionGroup != null
&& (r.Connection.ConnectionGroup.RecordingRetentionDays ?? defaultRetention) > 0
&& r.EndedAt!.Value.AddDays(
&& r.EndedAt.Value.AddDays(
r.Connection.ConnectionGroup.RecordingRetentionDays ?? defaultRetention) < now)
.Include(r => r.Connection!).ThenInclude(c => c.ConnectionGroup)
.ToListAsync(ct);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task<SsoProvisioningResult> ProvisionOrLinkAsync(SsoProviderType pr
{
Id = Guid.NewGuid(),
Email = email,
Name = string.IsNullOrWhiteSpace(displayName) ? email : displayName!,
Name = string.IsNullOrWhiteSpace(displayName) ? email : displayName,
ExternalId = externalId,
SsoProviderType = providerType,
IsActive = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,54 +19,63 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="FluentValidation" Version="11.12.0" />
<PackageReference Include="Mapster" Version="10.0.11" />
<PackageReference Include="Mapster.DependencyInjection" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Otp.NET" Version="1.4.1" />
<PackageReference Include="Duende.IdentityModel" Version="8.1.0" />
<PackageReference Include="ITfoxtec.Identity.Saml2" Version="4.18.0" />
<!-- ITfoxtec.Identity.Saml2 pulls Microsoft.IdentityModel.Tokens 8.15.0 transitively
while Microsoft.AspNetCore.Authentication.JwtBearer floors JsonWebTokens / Protocols
at 8.0.1. The version split causes JsonWebTokens 8.0.1 to call a Base64UrlEncoder
overload that doesn't exist in Tokens 8.15.0 (MissingMethodException -> every token
is rejected as "invalid_token"). Pin the whole IdentityModel set to 8.15.0. -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.18.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.18.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
<PackageReference Include="ITfoxtec.Identity.Saml2" Version="4.20.1" />
<!-- ITfoxtec.Identity.Saml2 pulls Microsoft.IdentityModel.Tokens transitively while
Microsoft.AspNetCore.Authentication.JwtBearer floors JsonWebTokens / Protocols at a
different version. Any version split causes JsonWebTokens to call a Base64UrlEncoder
overload that doesn't exist in the resolved Tokens assembly (MissingMethodException ->
every token is rejected as "invalid_token"). Pin the whole IdentityModel set — all four
packages below — to the SAME version (currently 8.22.0); never bump one in isolation.
NOTE: the `ignore` rule for Microsoft.IdentityModel.* in .github/dependabot.yml only
suppresses routine version updates, NOT security-advisory-driven ones, and it never
matched System.IdentityModel.Tokens.Jwt (different prefix). A partial bump like the one
this pin exists to prevent can therefore recur — re-align all four by hand when it does. -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.22.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.22.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.22.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0" />
<!-- Pin EFCore.Relational to 10.0.7 to align with EFCore.InMemory used by tests
and avoid MSB3277 version conflict against Npgsql.EntityFrameworkCore.PostgreSQL. -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.12.14" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />

<!-- Structured logging + Loki sink -->
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.2" />

<!-- Prometheus metrics -->
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />

<!-- OpenTelemetry distributed tracing -->
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
<!-- Azure Key Vault integration -->
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.11.0" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />

<!-- Session recording storage backends (v1.0.2) -->
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.29.1" />
<!-- AWSSDK.S3 4.0.23+ pulls AWSSDK.Core >= 4.0.0.32, which patches GHSA-9cvc-h2w8-phrp -->
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
<PackageReference Include="AWSSDK.S3" Version="4.0.101.7" />
</ItemGroup>

</Project>
Loading
Loading