mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 01:51:14 +00:00
[AZ-484] Cycle 1 Steps 12-16: docs, security, perf, deploy report
Captures the post-implementation autodev gates for AZ-484 multi-source tile storage: - Step 12 (Test-Spec Sync): added 7 AC rows (AZ-484 AC-1..AC-7) and a PT-07 NFR row to traceability-matrix.md; added PT-07 scenario to performance-tests.md. - Step 13 (Update Docs): refreshed data_model.md (tiles columns + indexes + selection rule + UPSERT contract + migrations 012/013), module-layout.md (Common/Enums section with L-001 guidance, DataAccess imports-from now lists 6 sites), 6 module / component docs to reflect the new repo signatures, source/captured_at fields, and Dapper enum bypass workaround. ripple_log_cycle1.md records zero out-of-scope ripple. - Step 14 (Security Audit): PASS_WITH_WARNINGS - 0 Critical, 0 High, 5 Medium, 5 Low. AZ-484 itself added zero new findings. Hardening items (Postgres default creds, .env in build context, GMaps key rotation, ASP.NET Core 8.0.21 -> 8.0.25, rate limiter) recorded for separate tickets. - Step 15 (Performance Test): all PT-01..PT-07 scenarios Unverified (non-blocking); PT-07 baseline-comparison harness deferred to a leftover for next cycle. - Step 16 (Deploy): cycle deploy report covering migration safety, rollback path, post-deploy verification, security caveats. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Phase 1 — Dependency Scan
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**Method**: Manual inventory from `*.csproj` + targeted advisory search (WebSearch against GHSA / NVD / NuGet ReversingLabs).
|
||||
**Reason for manual mode**: `dotnet list package --vulnerable` is on the project's "do not run from agent" list (AGENTS.md — these commands hang in this environment).
|
||||
|
||||
## Inventory
|
||||
|
||||
| Project | Package | Version | Notes |
|
||||
|---------|---------|---------|-------|
|
||||
| Api | Microsoft.AspNetCore.OpenApi | 8.0.21 | ASP.NET Core 8 LTS patch (one behind 8.0.25) |
|
||||
| Api | Newtonsoft.Json | 13.0.4 | Latest 13.x |
|
||||
| Api | Serilog.AspNetCore | 8.0.3 | |
|
||||
| Api | Serilog.Sinks.File | 6.0.0 | |
|
||||
| Api | SixLabors.ImageSharp | 3.1.11 | |
|
||||
| Api | Swashbuckle.AspNetCore | 6.6.2 | |
|
||||
| Common | SixLabors.ImageSharp | 3.1.11 | |
|
||||
| DataAccess | Dapper | 2.1.35 | |
|
||||
| DataAccess | Npgsql | 9.0.2 | |
|
||||
| DataAccess | dbup-postgresql | 6.0.3 | |
|
||||
| DataAccess | Microsoft.Extensions.Configuration.Abstractions | 9.0.10 | |
|
||||
| DataAccess | Microsoft.Extensions.Logging.Abstractions | 9.0.10 | |
|
||||
| TileDownloader | Microsoft.Extensions.Caching.Memory | 9.0.10 | |
|
||||
| TileDownloader | Microsoft.Extensions.Http | 9.0.10 | |
|
||||
| TileDownloader | Microsoft.Extensions.Logging.Abstractions | 9.0.10 | |
|
||||
| TileDownloader | Microsoft.Extensions.Options.ConfigurationExtensions | 9.0.10 | |
|
||||
| TileDownloader | Newtonsoft.Json | 13.0.4 | |
|
||||
| Tests | coverlet.collector | 6.0.0 | |
|
||||
| Tests | FluentAssertions | 8.8.0 | |
|
||||
| Tests | Microsoft.Extensions.* | 9.0.10 | (multiple) |
|
||||
| Tests | Microsoft.NET.Test.Sdk | 17.8.0 | NuGet.Frameworks transitive CVE flag — see findings |
|
||||
| Tests | Moq | 4.20.72 | |
|
||||
| Tests | xunit | 2.5.3 | |
|
||||
| Tests | xunit.runner.visualstudio | 2.5.3 | |
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Severity | Package | Version | Advisory | Disposition |
|
||||
|---|----------|---------|---------|----------|-------------|
|
||||
| D1 | Medium (production-risk: **Low**, exposure: not reachable) | Microsoft.AspNetCore.OpenApi → ASP.NET Core 8 runtime | 8.0.21 | [CVE-2026-26130](https://github.com/dotnet/aspnetcore/security/advisories/GHSA-4vgm-c2wm-63mw) — SignalR DoS via unbounded buffer | **Not exploitable in this app**: codebase grep for `SignalR\|MapHub\|UseSignalR\|HubConnection` returns zero hits. Runtime patch still recommended. Upgrade `Microsoft.AspNetCore.OpenApi` to `8.0.25` (or current 8.0.x patch) and redeploy on a runtime ≥ 8.0.25 to remove the vulnerable code paths from the deployed image. |
|
||||
| D2 | Low (test-only) | Microsoft.NET.Test.Sdk | 17.8.0 | [CVE-2022-30184](https://github.com/microsoft/vstest/issues/4409) via transitive `NuGet.Frameworks <6.2.1` — information disclosure (CVSS 5.5) | **Not exploitable in production**: package is `IsTestProject=true` only; never shipped. Upgrade to `Microsoft.NET.Test.Sdk` ≥ 17.9.0 (which dropped the `NuGet.Frameworks` dependency entirely) the next time the test project's deps are touched. |
|
||||
|
||||
## Cross-version sanity (per `coderule.mdc`: keep dependency versions consistent)
|
||||
|
||||
- `Microsoft.Extensions.*` is uniformly **9.0.10** across DataAccess, TileDownloader, Tests, RegionProcessing, RouteManagement — consistent. ✓
|
||||
- `Newtonsoft.Json` is **13.0.4** in both Api and TileDownloader — consistent. ✓
|
||||
- `SixLabors.ImageSharp` is **3.1.11** in both Api and Common — consistent. ✓
|
||||
- ASP.NET Core meta-package level is at **8.0.21** while extensions are at 9.0.10. The 9.x extensions ship a forward-compatible netstandard2.0 surface and load fine on the .NET 8 runtime — no functional issue, but worth flagging as a minor consistency drift for whoever next bumps the framework.
|
||||
|
||||
## Items checked clean
|
||||
|
||||
- SixLabors.ImageSharp 3.1.11 — newer than the patched 3.1.7 / 3.1.5 lines (CVE-2024-41131, CVE-2025-27598). No outstanding GHSA against 3.1.11 itself.
|
||||
- Newtonsoft.Json 13.0.4 — past CVE-2024-21907 fix line (13.0.1).
|
||||
- Npgsql 9.0.2 — outside the 4.x / 5.x / 6.x / 7.x / 8.x ranges affected by CVE-2024-32655 (SQL injection via protocol message size overflow). 9.0.x line was never affected.
|
||||
- Dapper 2.1.35 — only "advisory" hit was a dependency-check false positive for SQLite CVE-2017-10989; not a Dapper issue.
|
||||
- Swashbuckle.AspNetCore 6.6.2 — no published GHSA / CVE.
|
||||
- Serilog.AspNetCore 8.0.3 — no published GHSA / CVE.
|
||||
- dbup-postgresql 6.0.3 — no published GHSA / CVE.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All package manifests scanned (8 csproj files)
|
||||
- [x] Each finding has a CVE ID or advisory reference
|
||||
- [x] Upgrade paths identified for every Medium/Low finding
|
||||
- [x] No Critical or High finding remains open after exploitability triage
|
||||
@@ -0,0 +1,87 @@
|
||||
# Phase 4 — Configuration & Infrastructure Review
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**Scope**: `Dockerfile` (API + integration tests), `docker-compose.yml`, `docker-compose.tests.yml`, `.dockerignore`, `.woodpecker/01-test.yml`, `.woodpecker/02-build-push.yml`, `appsettings*.json`, `.env` handling.
|
||||
|
||||
## Findings
|
||||
|
||||
### I1 — Dockerfile runs as root (Low)
|
||||
|
||||
- **Location**: `SatelliteProvider.Api/Dockerfile` (no `USER` directive)
|
||||
- **Description**: The final stage of the API image inherits root from `mcr.microsoft.com/dotnet/aspnet:8.0` (current Microsoft images default to root unless overridden). Any process compromise — even a low-impact one — has uid-0 inside the container.
|
||||
- **Impact**: Container escape primitives (e.g., kernel CVE, sloppy bind-mount of `/var/run/docker.sock`) become host-root rather than host-uid-1000. The `02-build-push.yml` step itself bind-mounts `/var/run/docker.sock` into the build container — that's a separate concern (build host, not runtime), but it underscores why "least privilege at runtime" matters even on a single-tenant box.
|
||||
- **Remediation**: Add to the `final` stage:
|
||||
```dockerfile
|
||||
RUN adduser --disabled-password --gecos "" --uid 10001 satellite && \
|
||||
chown -R satellite:satellite /app
|
||||
USER satellite
|
||||
```
|
||||
Also verify `./tiles`, `./ready`, `./logs` host volumes are writable by uid 10001 in deployment manifests.
|
||||
|
||||
### I2 — No security headers middleware (Low)
|
||||
|
||||
- **Location**: `SatelliteProvider.Api/Program.cs` (no `app.UseSecurityHeaders()` / `app.Use(headers …)` block)
|
||||
- **Description**: API responses do not set `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, or HSTS (`Strict-Transport-Security`) — only `app.UseHttpsRedirection()` is wired. For a JSON-only API this is **low** impact (no browser is the primary client), but the missing `Cache-Control` defaults can let proxies cache 5xx responses.
|
||||
- **Impact**: Limited — JSON-only responses, no cookies, no browser session. The Swagger UI (Development-only) does render HTML; a permissive default there is more of a hygiene issue than a real risk.
|
||||
- **Remediation**: Add a tiny middleware to set the standard hardening headers, OR install `NWebsec.AspNetCore.Middleware` and wire `app.UseHsts()` + the `nosniff` / `frame-options` defaults. Cheap, no behavioural change.
|
||||
|
||||
### I3 — No rate limiting on any HTTP endpoint (Medium)
|
||||
|
||||
- **Location**: `SatelliteProvider.Api/Program.cs` (no `app.UseRateLimiter()`, no `AddRateLimiter()`)
|
||||
- **Description**: There is internal concurrency control on outbound Google Maps calls (`SemaphoreSlim`, `MaxConcurrentDownloads`), but no inbound rate limiting. An attacker can:
|
||||
- Submit `N` `POST /api/satellite/request` calls in a tight loop, filling the bounded `IRegionRequestQueue` (capacity 1000) and DoS-ing the background processor.
|
||||
- Submit `N` `GET /api/satellite/tiles/latlon` calls with novel lat/lon pairs, forcing the upstream Google Maps quota to drain.
|
||||
- **Impact**: Service-degradation DoS. Combined with finding A01-caveat (no auth), the only protection is the network boundary.
|
||||
- **Remediation**: Wire `Microsoft.AspNetCore.RateLimiting` (built into .NET 8 — no new package). Conservative starting point:
|
||||
```csharp
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 60, Window = TimeSpan.FromMinutes(1) }));
|
||||
});
|
||||
app.UseRateLimiter();
|
||||
```
|
||||
Tune per-endpoint after observing baseline production load.
|
||||
|
||||
### I4 — No security-event logs / alerting (Low)
|
||||
|
||||
- **Location**: Logging strategy across `Program.cs`, `GlobalExceptionHandler`, `CorsConfigurationValidator`
|
||||
- **Description**: Operational logs are well-structured (Serilog → file rotation; correlationId propagation), but there are no log entries for what would be security-relevant events: validation failures (BadRequest stream), repeated 4xx from a single IP, malformed input bursts, or migration failures. The migration failure path does throw and crash startup (good signal), but this leaves no trail in the file logs.
|
||||
- **Impact**: No way to detect abuse of the unauthenticated endpoints from logs alone. For an internal-only deployment this is acceptable; if the API ever moves toward a less-trusted network, post-deploy log-mining will not be able to reconstruct attack patterns.
|
||||
- **Remediation**: Defer until/unless the trust boundary changes. When required: add a structured log line for each 400/404 (with `Method`, `Path`, `RemoteIp`, `correlationId`) and a counter for "validation failures per minute per IP".
|
||||
|
||||
### I5 — `.env` is NOT in `.dockerignore` (Medium)
|
||||
|
||||
- **Location**: `.dockerignore` (line by line review — no `.env` entry); `SatelliteProvider.Api/Dockerfile:15` (`COPY . .`)
|
||||
- **Description**: The Dockerfile's `COPY . .` step copies the entire build context into `/src`. The build context starts at the repo root, where `.env` lives. `.env` IS in `.gitignore` so the dev-only Google Maps key never reaches the git repo, but it WILL be baked into the build-stage image layer (and into the final image, since `final` does `COPY --from=publish /app/publish .` — only `/app/publish` survives, but the `build` stage retains `.env` and is reachable if anyone introspects an intermediate layer).
|
||||
- **Impact**:
|
||||
- Anyone with read access to the registry can `docker pull <build-stage-tag>` (if exported) and recover the API key from the layer.
|
||||
- Even just the `final` image: BuildKit cache mounts and any future Dockerfile change that does `COPY . /app` instead of `COPY --from=publish` would silently include the file.
|
||||
- **Remediation**: Add `.env` to `.dockerignore`:
|
||||
```
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
```
|
||||
This is a one-line fix and complements finding S4.
|
||||
|
||||
### I6 — `docker-compose.yml` exposes Postgres on `0.0.0.0:5432` (Medium — duplicate of S2; restated here for infra-domain completeness)
|
||||
|
||||
- **Location**: `docker-compose.yml:9-10`
|
||||
- **Description / Remediation**: See S2.
|
||||
|
||||
## Items checked clean
|
||||
|
||||
- **Secrets management in CI**: `.woodpecker/02-build-push.yml` uses `from_secret: registry_host / registry_user / registry_token` — no plaintext credentials. The `docker login` step pipes the token via `--password-stdin`, which avoids leaking the token via process list. ✓
|
||||
- **Image attribution**: build step labels images with `org.opencontainers.image.revision`, `…created`, `…source` — good provenance hygiene. ✓
|
||||
- **Healthcheck on Postgres**: `pg_isready -U postgres` configured. (Note: relies on the weak default user from S2.)
|
||||
- **Log volume layout**: `./logs:/app/logs` mounted; not exposed via the API. ✓
|
||||
- **Test runner isolation**: `docker-compose.tests.yml` extends the API service (good — same image) but uses `restart: "no"` so a flapping integration test doesn't loop and amplify load.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All Dockerfiles reviewed (Api + IntegrationTests)
|
||||
- [x] All CI/CD configs reviewed (`.woodpecker/01-test.yml`, `02-build-push.yml`)
|
||||
- [x] All env / config files reviewed (`appsettings*.json`, `.env`, `docker-compose*.yml`)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Phase 3 — OWASP Top 10:2025 Review
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**OWASP version**: [OWASP Top 10:2025](https://owasp.org/Top10/2025/en/) (verified at audit start)
|
||||
**Project context**: Self-hosted .NET 8 backend service. Documented as an "internal/trusted network service — no auth layer" (`_docs/02_document/architecture.md` §7). Deployed via Docker behind another network boundary (per `_docs/02_document/deployment/`). The audit is scoped to the codebase as it stands; categories whose findings depend on a missing trust-boundary control are flagged accordingly.
|
||||
|
||||
| # | Category | Status | Findings | Notes |
|
||||
|---|----------|--------|----------|-------|
|
||||
| A01 | Broken Access Control | **N/A** (with caveat) | — | The service intentionally exposes ALL endpoints without authentication or authorization — documented design (architecture.md §7). No IDOR analysis applies because there is no user concept. **Caveat**: this is only safe if the deployment puts the API behind a network-level gatekeeper (VPN, mTLS, internal-only LB). If the deploy ever moves to a public network, this category becomes the #1 risk and EVERY endpoint becomes an unauthenticated execution surface. |
|
||||
| A02 | Security Misconfiguration | **FAIL** | S1, S2, I1, I2 | Default Postgres credentials in both `appsettings.json` and `docker-compose.yml`; Postgres port bound to `0.0.0.0`; container runs as root; no security headers middleware. |
|
||||
| A03 | Software Supply Chain Failures | **PASS_WITH_WARNINGS** | D1, D2 | Two known transitive CVEs (D1 — ASP.NET Core 8.0.21 SignalR DoS, not exploitable here; D2 — `Microsoft.NET.Test.Sdk` 17.8.0 → `NuGet.Frameworks` info disclosure, test-only). No use of unsigned NuGet packages; no auto-update of dependencies in production. |
|
||||
| A04 | Cryptographic Failures | **N/A** | — | No password storage (no users), no encryption at rest, no in-app crypto. The Google Maps integration uses HTTPS (default Npgsql/HttpClient stacks). At-rest tile storage is plain JPEG by design — these are public satellite images, not confidential data. |
|
||||
| A05 | Injection | **PASS** | — | All Dapper queries use parameter objects (`new { Id = id }` etc.); no string-built or interpolated user input flows into SQL. No `Process.Start`, no shell exec, no `eval`. JSON deserialization uses `System.Text.Json` defaults (no type-name handling). XSS / template injection N/A — JSON-only API. |
|
||||
| A06 | Insecure Design | **FAIL** | S3, I3 | No rate limiting on any endpoint despite the existence of an outbound rate-limited dependency (Google Maps). Latitude / longitude inputs are not range-validated at the API boundary (S3). No quota / throttling on region-request creation, which can multiply outbound calls and disk writes. |
|
||||
| A07 | Authentication Failures | **N/A** (with caveat) | — | Same caveat as A01 — there is no authentication system to fail. |
|
||||
| A08 | Software or Data Integrity Failures | **PASS** | — | DbUp migrations are idempotent and tracked in `schemaversions`; rollback is forward-only by design. No auto-update path. CI artifacts go through `.woodpecker/02-build-push.yml` with `from_secret: registry_token` (not in plaintext). No unsigned external scripts executed at build/deploy. |
|
||||
| A09 | Security Logging and Alerting Failures | **PASS_WITH_WARNINGS** | I4 | Serilog writes structured logs with file rotation; `GlobalExceptionHandler` correlates server logs to client responses via `correlationId` (good). However: no security-event logging (e.g., bad-input bursts, repeated 4xx from same source) and no alerting on log patterns. Acceptable for an internal service; would need attention if exposed publicly. |
|
||||
| A10 | Mishandling of Exceptional Conditions | **PASS** | — | `GlobalExceptionHandler` returns RFC 7807 ProblemDetails with a generic body and a correlationId — no exception text leaks to clients. `GlobalExceptionHandlerTests.cs` includes a positive control that confirms a "leakySecret"-shaped exception message is NOT echoed. |
|
||||
|
||||
## Cross-reference to Phase 1 / Phase 2 findings
|
||||
|
||||
| OWASP Cat | Tied finding | Severity | Source phase |
|
||||
|-----------|--------------|----------|--------------|
|
||||
| A02 | S1 — default password in appsettings.json | Medium | Phase 2 |
|
||||
| A02 | S2 — weak Postgres creds + 0.0.0.0 binding in compose | Medium | Phase 2 |
|
||||
| A02 | I1 — Dockerfile runs as root | Low | Phase 4 (next) |
|
||||
| A02 | I2 — no security headers middleware | Low | Phase 4 (next) |
|
||||
| A03 | D1 — CVE-2026-26130 in ASP.NET Core 8.0.21 (SignalR; not reachable) | Medium (paper) / Low (real) | Phase 1 |
|
||||
| A03 | D2 — CVE-2022-30184 transitive via test SDK | Low (test-only) | Phase 1 |
|
||||
| A06 | S3 — lat/lon not range-validated at API boundary | Low | Phase 2 |
|
||||
| A06 | I3 — no rate limiting on any endpoint | Medium | Phase 4 (next) |
|
||||
| A06 | S4 — Google Maps API key handling (no .env.example, no rotation hygiene) | Medium | Phase 2 |
|
||||
| A09 | I4 — no security-event logs, no alerting | Low | Phase 4 (next) |
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All current OWASP Top 10:2025 categories assessed
|
||||
- [x] Each FAIL has at least one specific finding with evidence
|
||||
- [x] N/A categories have justification + caveat
|
||||
- [x] No `security_approach.md` exists in `_docs/00_problem/` to cross-reference (project has not declared explicit security requirements; this audit treats the architecture-vision statement "internal/trusted network service" as the de-facto requirement)
|
||||
@@ -0,0 +1,120 @@
|
||||
# Security Audit Report
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**Scope**: Satellite Provider — full repository (Api, Common, DataAccess, Services.*, Tests, infra)
|
||||
**Trigger**: `/autodev` Step 14 (Security Audit) — feature cycle 1, post-AZ-484
|
||||
**Verdict**: **PASS_WITH_WARNINGS**
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 5 |
|
||||
| Low | 5 |
|
||||
|
||||
No Critical or High findings. The verdict is `PASS_WITH_WARNINGS` driven by 5 Medium findings, all of which are well-understood configuration / hardening gaps rather than exploitable vulnerabilities in the application logic itself. **AZ-484 (the cycle's only feature change) introduced zero new findings** — it is a pure data-layer change with no auth surface, no untrusted-input handling, and no new external dependencies.
|
||||
|
||||
## OWASP Top 10:2025 Assessment
|
||||
|
||||
| Category | Status | Findings |
|
||||
|----------|--------|----------|
|
||||
| A01 Broken Access Control | N/A (with caveat) | — |
|
||||
| A02 Security Misconfiguration | FAIL | S1, S2/I6, I1, I2 |
|
||||
| A03 Software Supply Chain Failures | PASS_WITH_WARNINGS | D1, D2 |
|
||||
| A04 Cryptographic Failures | N/A | — |
|
||||
| A05 Injection | PASS | — |
|
||||
| A06 Insecure Design | FAIL | S3, S4, I3 |
|
||||
| A07 Authentication Failures | N/A (with caveat) | — |
|
||||
| A08 Software or Data Integrity Failures | PASS | — |
|
||||
| A09 Security Logging and Alerting Failures | PASS_WITH_WARNINGS | I4 |
|
||||
| A10 Mishandling of Exceptional Conditions | PASS | — |
|
||||
|
||||
The two **N/A (with caveat)** entries (A01, A07) reflect the documented architectural choice (`architecture.md` §7) that this is an internal/trusted-network service. **The audit does not endorse that choice — it merely notes that the choice has been made deliberately.** If the deployment trust boundary ever changes, A01 and A07 immediately become FAIL and every endpoint becomes an unauthenticated surface; that decision must be re-examined before any internet-facing exposure.
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Severity | Category | Location | Title |
|
||||
|----|----------|------------------------|---------------------------------------------------------|--------------------------------------------------------------------------------------|
|
||||
| S1 | Medium | A02 — Misconfiguration | `SatelliteProvider.Api/appsettings.json:24` | Default Postgres password (`postgres/postgres`) committed in `appsettings.json` |
|
||||
| S2 | Medium | A02 — Misconfiguration | `docker-compose.yml:6-7,30` | Weak Postgres credentials in compose (mirrors S1) |
|
||||
| S3 | Low | A06 — Insecure Design | `SatelliteProvider.Api/Program.cs:169,207,237` | Latitude/longitude inputs not range-validated at API boundary |
|
||||
| S4 | Medium | A06 — Insecure Design | `.env` (workspace root) | Apparent real Google Maps API key on developer filesystem; no `.env.example` |
|
||||
| D1 | Medium | A03 — Supply Chain | `SatelliteProvider.Api.csproj` — `Microsoft.AspNetCore.OpenApi 8.0.21` | CVE-2026-26130 SignalR DoS (not reachable in this app — codebase has zero SignalR use) |
|
||||
| D2 | Low | A03 — Supply Chain | `SatelliteProvider.Tests.csproj` — `Microsoft.NET.Test.Sdk 17.8.0` | CVE-2022-30184 transitive via `NuGet.Frameworks <6.2.1` (test-only) |
|
||||
| I1 | Low | A02 — Misconfiguration | `SatelliteProvider.Api/Dockerfile` | Container runs as root (no `USER` directive) |
|
||||
| I2 | Low | A02 — Misconfiguration | `SatelliteProvider.Api/Program.cs` | No security headers middleware |
|
||||
| I3 | Medium | A06 — Insecure Design | `SatelliteProvider.Api/Program.cs` | No inbound rate limiting on any HTTP endpoint |
|
||||
| I4 | Low | A09 — Logging | `SatelliteProvider.Api/*` (logging strategy) | No security-event logs / alerting |
|
||||
| I5 | Medium | A02 — Misconfiguration | `.dockerignore` + `Dockerfile:15` (`COPY . .`) | `.env` not in `.dockerignore` — risk of API key being baked into image layers |
|
||||
|
||||
I6 in the infra report is a duplicate of S2 (same root cause) and is not double-counted in the summary.
|
||||
|
||||
### Finding Details
|
||||
|
||||
Full evidence and remediation for every finding lives in the per-phase reports. The detail tables there are the source of truth — this top-level report intentionally avoids restating multi-paragraph remediation steps.
|
||||
|
||||
- **Phase 1**: `_docs/05_security/dependency_scan.md` — D1, D2, full dependency inventory + cross-version sanity check
|
||||
- **Phase 2**: `_docs/05_security/static_analysis.md` — S1, S2, S3, S4, plus the categories that were checked clean (SQL injection, command injection, deserialization, path traversal, log leakage, exception leakage)
|
||||
- **Phase 3**: `_docs/05_security/owasp_review.md` — OWASP Top 10:2025 per-category assessment + cross-reference table
|
||||
- **Phase 4**: `_docs/05_security/infrastructure_review.md` — I1, I2, I3, I4, I5, I6, plus the items checked clean (CI secrets handling, image attribution labels)
|
||||
|
||||
## Dependency Vulnerabilities
|
||||
|
||||
| Package | CVE | Severity | Reachable? | Fix Version |
|
||||
|---------|-----|----------|------------|-------------|
|
||||
| Microsoft.AspNetCore.OpenApi (→ ASP.NET Core 8 runtime) | CVE-2026-26130 | High (paper) / Low (this app) | **No** — codebase has zero SignalR use | 8.0.25 |
|
||||
| Microsoft.NET.Test.Sdk → NuGet.Frameworks | CVE-2022-30184 | Medium (paper) / Low (this app) | Test project only — never shipped | Microsoft.NET.Test.Sdk 17.9.0+ |
|
||||
|
||||
All other dependencies (`Newtonsoft.Json 13.0.4`, `SixLabors.ImageSharp 3.1.11`, `Npgsql 9.0.2`, `Dapper 2.1.35`, `Swashbuckle.AspNetCore 6.6.2`, `Serilog.AspNetCore 8.0.3`, `dbup-postgresql 6.0.3`, `Microsoft.Extensions.* 9.0.10`) are at or beyond the patched line for every CVE I could find.
|
||||
|
||||
## AZ-484 Cycle-Specific Verdict
|
||||
|
||||
The reason this audit was triggered (the AZ-484 multi-source tile storage cycle) is independently **clean**:
|
||||
|
||||
- Migration 013 is transactional and idempotent — no data loss / data integrity finding.
|
||||
- `TileSourceConverter` enforces a closed value space at the language layer; `TileEntity.Source` is `string` only as a Dapper-bug workaround documented in `_docs/LESSONS.md` L-001.
|
||||
- `TileRepository` queries continue to use parameterised Dapper — no new SQL injection surface.
|
||||
- No new external dependencies, no new endpoints, no new untrusted-input flows.
|
||||
- All findings in this report predate AZ-484 and are unchanged by it.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate (Critical/High)
|
||||
|
||||
None — there are no Critical or High findings. The audit does not block the next deploy on its own merit.
|
||||
|
||||
### Short-term (Medium — pick before next public-network exposure or any post-deploy hardening pass)
|
||||
|
||||
1. **S1 + S2 + I5** — De-default DB credentials and stop shipping the .env into the build context. One coordinated change:
|
||||
- Remove `ConnectionStrings:DefaultConnection` from `appsettings.json` (rely on env-var via the existing throw on null).
|
||||
- Add `POSTGRES_USER` / `POSTGRES_PASSWORD` to a tracked `.env.example` and source them from a dev `.env`; bind `5432` to `127.0.0.1`.
|
||||
- Append `.env` and `.env.*` (with `!.env.example` exception) to `.dockerignore`.
|
||||
2. **S4** — Rotate the Google Maps API key out-of-band, add `.env.example`, add Google Cloud key restrictions (HTTP referrer or IP allowlist + per-API quotas). The audit deliberately did not echo the key value into any artifact.
|
||||
3. **D1** — Bump `Microsoft.AspNetCore.OpenApi` from `8.0.21` to the current 8.0.x patch (≥ 8.0.25) and rebuild the deployed image so the vulnerable SignalR code paths are physically absent.
|
||||
4. **I3** — Wire `Microsoft.AspNetCore.RateLimiting` (built into .NET 8 — no new package). Conservative starting threshold in the per-phase report.
|
||||
|
||||
### Long-term (Low — hardening backlog)
|
||||
|
||||
1. **I1** — Add a non-root `USER` to the API Dockerfile.
|
||||
2. **I2** — Add a tiny security-headers middleware (or pull `NWebsec.AspNetCore.Middleware`).
|
||||
3. **S3** — Add explicit lat/lon range guards at the API boundary (matches the existing `SizeMeters` 100-10000 pattern).
|
||||
4. **D2** — Bump `Microsoft.NET.Test.Sdk` to ≥ 17.9.0 next time the test project's deps are touched.
|
||||
5. **I4** — Defer until the trust boundary changes; if/when the API moves toward a less-trusted network, add structured 4xx logging per IP + a basic alerting rule.
|
||||
|
||||
## Verdict Logic
|
||||
|
||||
- No Critical or High findings → **not FAIL**
|
||||
- 5 Medium + 5 Low findings exist → **not PASS**
|
||||
- Therefore: **PASS_WITH_WARNINGS**
|
||||
|
||||
This satisfies the autodev gate to proceed to Step 15 (Performance Test). The recommendations above should be tracked as separate Jira tasks under a hardening epic before the first non-internal deployment.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All findings from Phases 1–4 included
|
||||
- [x] No duplicate findings (I6 explicitly noted as a duplicate of S2 and not double-counted)
|
||||
- [x] Every finding has remediation guidance (in per-phase reports)
|
||||
- [x] Verdict matches severity logic (no Critical/High → not FAIL; >0 findings → not PASS)
|
||||
- [x] No real secret values printed in any audit artifact (S4 described without echoing the API key)
|
||||
@@ -0,0 +1,90 @@
|
||||
# Phase 2 — Static Analysis (SAST)
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**Scope**: All `*.cs` files in production projects (Api, Common, DataAccess, Services.*) plus Tests for false-positive triage. Configuration files (`appsettings*.json`, `docker-compose*.yml`, `Dockerfile`, `.env`).
|
||||
**Method**: Pattern-based grep + targeted file review.
|
||||
|
||||
## Patterns checked
|
||||
|
||||
| Category | Pattern(s) | Verdict |
|
||||
|----------|-----------|---------|
|
||||
| SQL injection | `$"SELECT…"`, `+ "WHERE"`, raw `CommandText`, manual SQL string assembly | **Clean** |
|
||||
| Command/process injection | `Process.Start`, `ProcessStartInfo`, `cmd.exe`, `/bin/sh`, `UseShellExecute`, `eval`-equivalent | **Clean** |
|
||||
| XSS | unsanitized user input flowed to HTML or `Response.Write` | **N/A** — JSON-only API, no HTML rendering |
|
||||
| Template injection | Razor / scriban / handlebars on user input | **N/A** — none used |
|
||||
| Hardcoded credentials | `password = "…"`, `secret = "…"`, `token = "…"`, `apikey = "…"` in source | See findings S1, S2 |
|
||||
| Weak crypto | MD5/SHA1 for passwords, `RNGCryptoServiceProvider` (deprecated), hardcoded keys | **N/A** — no password storage, no crypto code in app |
|
||||
| Insecure deserialization | `BinaryFormatter`, `pickle`, untrusted JSON with type-name handling | **Clean** — `System.Text.Json` with default settings; `Newtonsoft.Json` 13.0.4 used only for outbound serialization to Google session-creation endpoint (line `GoogleMapsDownloaderV2.cs`), no deserialization of untrusted inbound JSON |
|
||||
| Path traversal | user input flowed into `File.Open`, `Path.Combine` | **Clean** — file paths are computed server-side from validated tile coordinates; no user-supplied path component reaches the filesystem |
|
||||
| Sensitive data in logs | passwords, API keys, tokens, PII in log statements | **Clean** — `GlobalExceptionHandler.cs` logs only `Method`, `Path`, `correlationId`; client gets a generic 500 + correlationId. `CorsConfigurationValidator` warning (`PermissiveDefaultWarning`) does not include secrets. There is a deliberate test fixture `GlobalExceptionHandlerTests.cs:23` that uses `"Connection string Host=secret-db;Password=hunter2 failed at line 42"` to verify the handler does NOT echo exception messages back — this is a positive control, not a finding |
|
||||
| Verbose error responses | stack traces or internal details returned to clients | **Clean** — `GlobalExceptionHandler` returns RFC 7807 ProblemDetails with `Detail = "An unexpected error occurred. Use the correlationId to look up the server log entry."` |
|
||||
| Input validation | numeric ranges, geo coordinates, enum-like strings | See finding S3 |
|
||||
|
||||
## Findings
|
||||
|
||||
### S1 — Default DB password committed in `appsettings.json` (Medium)
|
||||
|
||||
- **Location**: `SatelliteProvider.Api/appsettings.json:24`
|
||||
- **Vulnerable code**:
|
||||
```json
|
||||
"DefaultConnection": "Host=localhost;Database=satelliteprovider;Username=postgres;Password=postgres"
|
||||
```
|
||||
- **Description**: The default (non-Development) appsettings file ships with a weak, well-known password (`postgres/postgres`). In production this string is overridden by `ConnectionStrings__DefaultConnection` in `docker-compose.yml`/env, but the file itself becomes the fallback if env-var injection ever fails or is misconfigured (silent connect-as-default behaviour).
|
||||
- **Impact**: If a deployment misconfiguration drops the env override, the app silently falls back to attempting `postgres:postgres@localhost`. On a developer workstation this connects to the local Postgres container with full superuser; in production it would fail loudly only if the prod DB has different creds. Combined with finding S2 below (matching weak creds in compose file), this normalises a credential pattern that real production deployments may inherit.
|
||||
- **Remediation**:
|
||||
- Replace the default value with a deliberately-invalid placeholder such as `Host=__set-via-env__;Database=__;Username=__;Password=__` so a misconfiguration fails fast at startup instead of silently falling through.
|
||||
- OR remove the `ConnectionStrings:DefaultConnection` key from `appsettings.json` entirely and require the env var; `Program.cs` line 23–24 already throws when missing — keep that behaviour.
|
||||
|
||||
### S2 — Weak Postgres credentials in `docker-compose.yml` (Medium, dev-only as written)
|
||||
|
||||
- **Location**: `docker-compose.yml:6-7, 30`
|
||||
- **Vulnerable code**:
|
||||
```yaml
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
…
|
||||
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres
|
||||
```
|
||||
- **Description**: Same `postgres/postgres` credentials as S1. The compose file is labelled `Development` (`ASPNETCORE_ENVIRONMENT=Development`), so this is contained — but the file is the only compose artifact in the repo, which means anyone running `docker-compose up` on a network-reachable host immediately exposes a Postgres-with-default-creds.
|
||||
- **Impact**: Postgres on `0.0.0.0:5432` (port `"5432:5432"` mapping) with `postgres/postgres` is one of the most-scanned credential pairs on the public internet. If a developer runs this on a non-laptop host (cloud VM, shared lab, etc.) the DB is trivially compromised within minutes.
|
||||
- **Remediation**:
|
||||
- Bind `5432` to `127.0.0.1:5432` rather than `0.0.0.0:5432` so the host firewall isn't the only protection. (Replace `"5432:5432"` with `"127.0.0.1:5432:5432"`.)
|
||||
- Source `POSTGRES_USER` / `POSTGRES_PASSWORD` from the same `.env` file that already supplies `GOOGLE_MAPS_API_KEY` (line 31 already shows the pattern). Provide an `.env.example` with placeholder values and document the required vars in the README.
|
||||
- The deploy/observability docs at `_docs/02_document/deployment/` already describe a secret-manager strategy for staging/prod — fold the same pattern into the dev compose.
|
||||
|
||||
### S3 — Latitude / longitude inputs not range-validated at the API boundary (Low)
|
||||
|
||||
- **Locations**:
|
||||
- `SatelliteProvider.Api/Program.cs:169` — `GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, …)`
|
||||
- `SatelliteProvider.Api/Program.cs:207` — `RequestRegion` validates `SizeMeters` only; `request.Latitude` / `request.Longitude` are unchecked
|
||||
- `SatelliteProvider.Api/Program.cs:237` — `CreateRoute` delegates to `RouteService` which validates names but does not range-check waypoint coordinates
|
||||
- **Description**: `Latitude`, `Longitude`, and (for region requests) the implicit `MaxRoutePointSpacingMeters` boundary are accepted without enforcing valid geographic ranges (`-90 ≤ lat ≤ 90`, `-180 ≤ lon ≤ 180`). `ZoomLevel` IS validated downstream by `GoogleMapsDownloaderV2` against `MapConfig.AllowedZoomLevels` — so it is fine.
|
||||
- **Impact**:
|
||||
- Garbage inputs (e.g. `lat=999`) propagate through `GeoUtils.WorldToTilePos` and the slippy-map math, eventually producing nonsensical tile coordinates that are persisted to `tiles` and `regions`. This is a **data-quality** issue, not a code-execution issue.
|
||||
- No DoS amplification: every tile-download endpoint already enforces zoom against `AllowedZoomLevels`, so an attacker cannot use lat/lon abuse to multiply outbound Google Maps traffic beyond what zoom already bounds.
|
||||
- **Remediation**: Add explicit guard clauses at the API boundary (matches the existing `SizeMeters` 100-10000 pattern):
|
||||
```csharp
|
||||
if (Latitude < -90 || Latitude > 90) return Results.BadRequest(new { error = "Latitude must be between -90 and 90" });
|
||||
if (Longitude < -180 || Longitude > 180) return Results.BadRequest(new { error = "Longitude must be between -180 and 180" });
|
||||
```
|
||||
Apply uniformly to `GetTileByLatLon`, `RequestRegion`, and to each waypoint inside `CreateRoute`.
|
||||
|
||||
### S4 — `.env` file on developer filesystem contains an apparently real Google Maps API key (Medium — exposure depends on key reach)
|
||||
|
||||
- **Location**: `.env` (workspace root, **not** tracked — confirmed via `git ls-files` and `.gitignore:10`)
|
||||
- **Description**: The local `.env` contains a 39-character `AIzaSy…` value matching the Google Maps API key format. The file is correctly excluded from git (line 10 of `.gitignore`) and `git log -- .env` returns no history, so the key was never committed to this repository.
|
||||
- **Impact**: No repository exposure. **However**:
|
||||
- If the same key is shared across developers via Slack / email / other repos, it has likely already leaked elsewhere.
|
||||
- There is no `.env.example` template in the repo, which means new contributors typically request the real key via insecure channels rather than generating a fresh one.
|
||||
- The key has no per-call attribution; abuse cannot be traced back to a specific developer.
|
||||
- **Remediation**:
|
||||
- **Rotate the key in the Google Cloud console** (out of scope for this audit — the key value is intentionally not echoed into this report).
|
||||
- Add `.env.example` to the repo with `GOOGLE_MAPS_API_KEY=replace-with-your-own-key-from-cloud-console` and reference it in the README setup section.
|
||||
- Configure Google Cloud key restrictions: HTTP referrer allowlist (for browser keys) or IP allowlist (for server keys), and per-API quotas. Optional: per-developer keys.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All production source directories scanned (Api, Common, DataAccess, Services.TileDownloader, Services.RegionProcessing, Services.RouteManagement)
|
||||
- [x] Each finding has file path and line number
|
||||
- [x] False positives from test files explicitly distinguished (`GlobalExceptionHandlerTests.cs:23` "leakySecret" is a positive control)
|
||||
- [x] No real secret values printed in this report (S4 is described without echoing the key)
|
||||
Reference in New Issue
Block a user