PASS_WITH_WARNINGS. Zero Critical / High. New cycle-8 findings: - F-AZ809-1 (Medium / A04 Insecure Design): unbounded geofences.polygons enables an authenticated DoS on POST /api/satellite/route. Cap candidate: 50 or 500. - F-AZ810-1 (Low / A09): JsonException.Message echoed in UavUploadValidationFilter (new instance of cycle-7 F-AZ795-1 pattern in a second code path). - F-AZ810-2 (Low / Informational): UavTileMetadata.CapturedAt typed DateTime not DateTimeOffset; freshness window drifts in non-UTC dev environments. Zero impact in UTC-deployed prod. Carry-overs (cycle 7): F-AZ795-1, F-AZ795-2, D-AZ795-1 still open. Cycle 4 D2-cy4 still open (test-runtime Medium). Cycle-8 architectural wins recorded: per-endpoint validation reached 100% coverage; three approved validation paths formalised; OSM wire-format normalisation under strict mode (AZ-812); UAV-handler defence-in-depth retained. Highest-priority cycle-9 follow-up: F-AZ809-1 polygon cap. Co-authored-by: Cursor <cursoragent@cursor.com>
27 KiB
Static Analysis (Cycle 8)
Date: 2026-05-23
Mode: Delta scan
Scope: Source code introduced or changed by AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812. Cycle-7 baseline (static_analysis_cycle7.md) remains authoritative for the AZ-794 / AZ-795 / AZ-796 surface; this scan only audits the cycle-8 delta.
Files in scope (40 changed source files; non-test detail):
- API — 11 files
SatelliteProvider.Api/Program.cs(DI + endpoint wiring deltas —WithValidation<RequestRegionRequest>,WithValidation<CreateRouteRequest>,WithValidation<GetTileByLatLonQuery>,AddEndpointFilter<UavUploadValidationFilter>,AddTransient<UavUploadValidationFilter>,RejectUnknownQueryParamsEndpointFilterregistration)SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs(new — record with nullable bindings)SatelliteProvider.Api/Validators/RegionRequestValidator.cs(new — AZ-808)SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs(new — AZ-809)SatelliteProvider.Api/Validators/GeofencePolygonValidator.cs(new — AZ-809)SatelliteProvider.Api/Validators/RoutePointValidator.cs(new — AZ-809)SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs(new — AZ-810)SatelliteProvider.Api/Validators/UavTileBatchMetadataPayloadValidator.cs(new — AZ-810)SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs(new — AZ-810)SatelliteProvider.Api/Validators/GetTileByLatLonQueryValidator.cs(new — AZ-811)SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs(new — AZ-811)SatelliteProvider.Api/Swagger/ParameterDescriptionFilter.cs(AZ-811 — addedlat/lon/zoomdescription entries)
- Common — 6 DTO files (AZ-808/809/810/812 —
[JsonRequired]annotations + AZ-812 rename)SatelliteProvider.Common/DTO/RequestRegionRequest.csSatelliteProvider.Common/DTO/CreateRouteRequest.csSatelliteProvider.Common/DTO/GeofencePolygon.csSatelliteProvider.Common/DTO/GeoPoint.csSatelliteProvider.Common/DTO/RoutePoint.csSatelliteProvider.Common/DTO/UavTileMetadata.cs
- Test code (reviewed for fixture-only secrets + auth-bypass patterns) — 8 new validator unit tests + 4 new integration test files + several modified integration test helpers.
- Shell scripts (reviewed for embedded secrets + unsafe sequences) — 4 new probe scripts (
probe_latlon_validation.sh,probe_region_validation.sh,probe_route_validation.sh,probe_upload_validation.sh) + 1 modified perf script (run-performance-tests.sh, wire-rename diff only).
Method: Read each new file end-to-end; targeted Grep for injection / hardcoded-credential / unsafe-API patterns (password|secret|api.?key|bearer|token over SatelliteProvider.Api/Validators returned 0 matches); diff-review of every DTO change vs. its cycle-7 baseline; trace each [JsonRequired] chain through to its FluentValidation rule.
Findings
F-AZ809-1 — Unbounded geofences.polygons collection enables an authenticated DoS via CreateRouteRequest (Medium / A04 — Insecure Design)
- Location:
SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82(When(req => req.Geofences is not null, () => …)). - Description: The
CreateRouteRequestValidatorchains aRuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator())block but only enforcesNotEmptyon the collection. There is no upper bound onGeofences.Polygons.Count. The parent collectionPointsIS capped (MaxPoints = 500at line 27) — the polygons collection is the only nested list-bearing field on this endpoint without a cap. - Code:
When(req => req.Geofences is not null, () => { RuleFor(req => req.Geofences!.Polygons) .NotNull().WithMessage("`geofences.polygons` is required when `geofences` is present.") .NotEmpty().WithMessage("`geofences.polygons` must contain at least 1 polygon when `geofences` is present.") .OverridePropertyName("geofences.polygons"); RuleForEach(req => req.Geofences!.Polygons) .SetValidator(new GeofencePolygonValidator()) .OverridePropertyName("geofences.polygons"); }); - Exploit math (worst-case envelope under current configuration):
KestrelServerOptions.Limits.MaxRequestBodySize = uavBatchBodyLimit = MaxBatchSize × MaxBytes = 100 × 5 MiB = 500 MiB(set globally inProgram.cs:41-43for the UAV endpoint; the same limit applies to every endpoint by default because Kestrel exposes a per-server, not per-endpoint, default).- Minimum JSON polygon footprint:
{"northWest":{"lat":1.0,"lon":2.0},"southEast":{"lat":3.0,"lon":4.0}}≈ 90 bytes including the comma separator. - Theoretical maximum polygon count in a single 500 MiB request:
500 MiB / 90 bytes ≈ 5.8 million polygons. - With invalid polygons (e.g.
latout of range),GeofencePolygonValidatoradds 2 corner-rangeValidationFailureobjects + 1 cross-field NW-of-SE failure = ~3 failures per polygon. Worst-case allocation: ~17 millionValidationFailureinstances + the matching error-map keys before the filter formats theValidationProblemDetailsbody.
- Impact: Medium. A single authenticated authorized request can saturate the LOH (large object heap) and trigger a full GC pass on the API process. The endpoint is
RequireAuthorization()-gated (line 251 inProgram.cs) so anonymous callers cannot reach the validator — the attacker must hold a valid JWT. But once authenticated, a single malformed request degrades the service for every other tenant operator until GC reclaims the heap. Repeatable. No data leak, no privilege escalation; pure availability impact. - OWASP mapping: A04 — Insecure Design (missing rate/size limit on a collection-bearing input field). Adjacent to A05 (Security Misconfiguration — the global Kestrel limit was set for the UAV endpoint in cycle 5 but applies to every endpoint).
- Remediation: Add
Must(p => p is null || p.Count <= MaxPolygons)with a defensible upper bound. Reasonable cap candidates:MaxPolygons = 50(consistent with the historical use case — a route is unlikely to need more than a handful of geofence rectangles for AOI restriction).MaxPolygons = MaxPoints = 500(consistent with the siblingPointscap on the same DTO).- The matching
cumulative_review_batches_01-04_cycle8_report.mdalready enumeratespoints.Count <= 500(route),items.Count <= 100(UAV upload),coords.Count <= 1000(tile inventory, cycle 7) as bounded — the missing entry forgeofences.polygonsis the one inconsistency.
- Status: open — file as a cycle-9 follow-up under AZ-809 (or a sibling child ticket). Not release-blocking for cycle 8 itself: exploitation requires an authenticated caller with a valid GPS-permission-less JWT — the same threat model already had access to the cycle-7-pre-existing
inventory.Tiles.Countcap, so the marginal new exposure is moderate, not catastrophic. But it MUST be fixed before any untrusted-tenant exposure is added to the route endpoint.
F-AZ810-1 — JsonException.Message propagated to client in UavUploadValidationFilter (Low / A09 — Information Disclosure)
- Location:
SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84(thecatch (JsonException ex)block of the metadata parse). - Code:
catch (JsonException ex) { return Results.ValidationProblem(new Dictionary<string, string[]> { [MetadataField] = new[] { $"`metadata` could not be parsed as JSON: {ex.Message}" }, }); } - Description: The cycle-8
UavUploadValidationFilterechoes the rawJsonException.Messagedirectly to the client as the value oferrors["metadata"]. This is the same information-disclosure pattern as cycle-7 F-AZ795-1 (inGlobalExceptionHandler.cs:108-117), introduced in a second code path that bypasses the global exception handler (the filter intercepts and returnsResults.ValidationProblem(...)directly). Cycle-7 F-AZ795-1 remains open; cycle 8 adds a second instance of the same pattern that would also need to be sanitised by the same remediation. - Impact: Low. Same severity classification as F-AZ795-1. Auth-gated (
.RequireAuthorization(SatellitePermissions.UavUploadPolicy)atProgram.cs:238) — only callers holding a valid JWT with theGPSpermission claim can reach the filter and trigger this path. The leaked content (type names, parse positions,System.Text.Jsonfingerprint) is already inferable from the OpenAPI spec; the new path narrows the attack surface for an authenticated GPS-permissioned operator but does not expose secrets, PII, or pivot vectors. - Remediation: Sanitise the response message to a generic string (e.g.
"metadatacould not be parsed as JSON. See the server log for details.") while continuing to log the rawex.Messageserver-side under the request'scorrelationId. Best done in tandem with F-AZ795-1's remediation since both paths surface the same exception class through the same response shape. Add an integration-test assertion inUavUploadValidationTeststhat noSystem.*substring appears in the response body'serrors[]value, mirroring the cycle-7 gap noted instatic_analysis_cycle7.md§ F-AZ795-1 Test coverage gap. - Status: open — file as a cycle-9 follow-up child of the same F-AZ795-1 ticket so both call sites get the sanitiser at once.
F-AZ810-2 — UavTileMetadata.CapturedAt typed DateTime not DateTimeOffset (Low / Informational — Time-handling correctness)
-
Location:
SatelliteProvider.Common/DTO/UavTileMetadata.cs:30+SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60. -
Code:
// UavTileMetadata.cs:30 [JsonRequired] public DateTime CapturedAt { get; init; } // UavTileMetadataValidator.cs:52-60 RuleFor(m => m.CapturedAt) .Must(capturedAt => capturedAt.ToUniversalTime() <= tp.GetUtcNow().UtcDateTime.AddSeconds(futureSkewSeconds)) .WithMessage($"`capturedAt` must be within {futureSkewSeconds}s of the current time (no future-dated tiles).") .Must(capturedAt => capturedAt.ToUniversalTime() >= tp.GetUtcNow().UtcDateTime.AddDays(-maxAgeDays)) .WithMessage($"`capturedAt` must be within the last {maxAgeDays} days."); -
Description: When
System.Text.Jsondeserializes an ISO-8601 string into aDateTime, the resultingDateTime.Kinddepends on the string's offset suffix:"2026-05-22T12:00:00Z"→Kind = Utc."2026-05-22T12:00:00+03:00"→Kind = Localafter normalization to local time."2026-05-22T12:00:00"(no suffix) →Kind = Unspecified.
For
Kind = Unspecified,DateTime.ToUniversalTime()treats the value as local time, which means the freshness comparison drifts by the host's timezone offset. In a UTC-deployed prod container (LinuxTZ=UTC), the local offset is zero and there is no observable impact. In a developer's local environment (e.g.TZ=Europe/Kyiv= UTC+02:00 or +03:00), acapturedAtvalue of"2026-05-22T12:00:00"would be treated as2026-05-22T10:00:00Z(in summer), shifting the freshness window by the offset. -
Impact: Low / Informational. Zero observable impact in the supported deployment configuration (Docker containers run in UTC by default; the cycle-2
docker-compose.ymldoes not overrideTZ). The freshness rule could be loosely-bounded by the same offset (in either direction) in a dev environment with a non-UTC host TZ. No security exploit: an attacker cannot force the server's TZ; they can only submitcapturedAtvalues, and the server's UTC-deployed configuration treats those deterministically. -
OWASP mapping: A09 — Security Logging and Monitoring Failures (adjacent — a stale freshness window could mask an out-of-band attack on the upload endpoint).
-
Remediation (two options):
- Strict: Change the DTO type to
DateTimeOffsetso the parsed value always carries an explicit offset, and add a JSON converter that rejects offset-less ISO-8601 strings at deserialization (JsonConverterAttributepointing at a custom converter that checks for theZ/+HH:MMsuffix and throwsJsonExceptionif missing — surfaces as a 400 viaGlobalExceptionHandler). - Lenient: Add a FluentValidation rule that rejects
DateTime.Kind == DateTimeKind.Unspecifiedso the caller must supply a tz-aware ISO-8601 string. Keeps the DTO shape; doesn't break clients that already send"Z"suffix.
Option 2 is the minimum behaviour-preserving fix. Option 1 is correct for a v2.0 of
uav-tile-upload.md. - Strict: Change the DTO type to
-
Status: open — file as a Low cycle-9 follow-up. Not release-blocking for cycle 8 because every documented client in
uav-tile-upload.mdexample payloads and every integration-test fixture sends theZsuffix.
Pattern Sweep — Cycle-8 Delta
Injection (SQL / Command / XSS / Template)
| Pattern | Result |
|---|---|
string.Format, interpolation $"...", or concatenation feeding into a Dapper / Npgsql command in the new files |
None. The 9 new files in SatelliteProvider.Api/Validators/ and SatelliteProvider.Api/DTOs/ do not touch the data layer. |
Process.Start, subprocess, eval, Invoke-Expression, raw system() |
None. |
| User-input echoed into HTML (XSS) | None. The API returns JSON only. The cycle-8 RejectUnknownQueryParamsEndpointFilter echoes the offending parameter name back as a JSON string value — System.Text.Json performs canonical JSON escaping of control characters; no HTML injection vector. |
| Template injection (Razor / Liquid / etc.) | None. No templating in the new files. |
Authentication & Authorization
| Pattern | Result |
|---|---|
| Hardcoded credentials, secrets, API keys | `Grep -i 'password |
Missing .RequireAuthorization() on a public endpoint |
Every cycle-8 endpoint binding in Program.cs retains .RequireAuthorization(): RequestRegion (line 251), CreateRoute (267), GetTileByLatLon (213), UploadUavTileBatch (238 — additionally requires the GPS permission claim via SatellitePermissions.UavUploadPolicy). |
| Validator running before auth check | No. ASP.NET Core endpoint filters run AFTER the routing layer's authorization middleware (app.UseAuthorization() at Program.cs:206). All four cycle-8 filters (ValidationEndpointFilter<RequestRegionRequest>, ValidationEndpointFilter<CreateRouteRequest>, RejectUnknownQueryParamsEndpointFilter + ValidationEndpointFilter<GetTileByLatLonQuery>, UavUploadValidationFilter) cannot run for anonymous callers — the 401 short-circuit fires first. |
| Permission/policy regression | RequiresGpsPermission on /api/satellite/upload retained. RejectUnknownQueryParamsEndpointFilter does NOT call RequireAuthorization() itself — it relies on the endpoint chain's existing .RequireAuthorization() (line 213). Correct. |
Cryptographic Failures
| Pattern | Result |
|---|---|
| Weak hash (MD5 / SHA1) used for passwords or signatures | None in cycle 8. Pre-existing UUIDv5 SHA-1 surface (Uuidv5.Create) is untouched. |
| New crypto material introduced | None. Cycle 8 has no cryptography. |
| Plaintext transmission | API listens on https://+:8080 with ALPN (cycle-6 baseline, unchanged). |
Data Exposure
| Pattern | Result |
|---|---|
| Sensitive data in logs | The new validators and filters do NOT log the request body. GlobalExceptionHandler 5xx branch logs Method, Path, correlationId, and the exception object (pre-existing). Cycle 8 doesn't widen this. |
| Sensitive fields in API responses | F-AZ810-1 (JsonException.Message echo) above is the only new echo-back path. No password hashes, no PII (the four endpoints carry only metadata: geospatial coords, integer IDs, timestamps). The RejectUnknownQueryParamsEndpointFilter echoes the offending parameter NAME (not value) back — and the list of allowed param names IS already in the OpenAPI spec, so no new fingerprinting surface. |
| Debug endpoints in production | Swagger is gated by app.Environment.IsDevelopment() (unchanged). |
| Secrets in version control | .env* files are gitignored. The 4 new probe scripts (probe_*_validation.sh) all use ${JWT:?…}-style env-var reads with explicit "set JWT env var" error guards; no embedded credentials. |
Insecure Deserialization
| Pattern | Result |
|---|---|
Pickle / BinaryFormatter / unsafe XML / JsonConvert.DeserializeObject<T> with TypeNameHandling.All |
None in cycle 8. UavUploadValidationFilter.cs:73 uses JsonSerializer.Deserialize<UavTileBatchMetadataPayload>(...) with the global JsonSerializerOptions from IOptions<JsonOptions> — which has UnmappedMemberHandling.Disallow set by cycle-7 ConfigureHttpJsonOptions. ✓ |
| Unbounded collection sizes | F-AZ809-1 above — geofences.polygons lacks a cap. Other cycle-8 list fields ARE capped: Points.Count <= 500 (CreateRouteRequestValidator:60-61), Items.Count <= MaxBatchSize=100 (UavTileBatchMetadataPayloadValidator:27-28). Cycle-7 Tiles.Count <= 5000 / LocationHashes.Count <= 5000 unchanged. The framework-level MaxRequestBodySize bound is in force for all paths — but it's set at 500 MiB to accommodate the UAV upload endpoint, which makes per-endpoint count caps the primary defence. |
Integer Overflow / Bounded Math
The cycle-8 validators do not perform any integer arithmetic beyond Math.Max(options.ValueLengthLimit, uavQuality.MaxBatchSize * 512) (Program.cs:61 — 100 * 512 = 51_200, no overflow possible). No left-shifts, no power-of-two computations on caller-controlled values. ✓
ReDoS / Algorithmic Complexity
Cycle-8 validation rules are all O(1) per entry × N entries with bounded N (with the F-AZ809-1 exception above):
| Validator | Per-entry cost | Total cost (worst case) |
|---|---|---|
RegionRequestValidator |
O(1) — 5 range checks on scalar fields | O(1) |
CreateRouteRequestValidator (root + cross-field invariant) |
O(1) base + O(N points) + O(P polygons unbounded) | O(N × P) with unbounded P — see F-AZ809-1 |
RoutePointValidator (via RuleForEach) |
O(1) — 2 range checks | O(500) bounded by parent MaxPoints |
GeofencePolygonValidator (via RuleForEach) |
O(1) — 2 corner null + 4 range + 2 cross-field | O(P) where P is unbounded — see F-AZ809-1 |
UavTileMetadataValidator (via RuleForEach) |
O(1) — 4 range checks + 2 freshness | O(100) bounded by MaxBatchSize |
UavTileBatchMetadataPayloadValidator (root) |
O(1) | O(1) |
GetTileByLatLonQueryValidator |
O(1) — 3 cascaded NotNull + range | O(1) |
RejectUnknownQueryParamsEndpointFilter |
O(K log K) where K = caller's query-param count | O(K) bounded by Kestrel's per-request header/query limits (default 32 KB of query string) |
No regex, no recursion, no nested loops with caller-controlled bounds (except F-AZ809-1).
UavUploadValidationFilter Defence-in-Depth Verification
The filter intercepts the primary multipart-upload code path. The pre-existing UavTileUploadHandler.HandleAsync retains the SAME envelope checks (SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs:64-141) — confirming the filter is defence-in-depth on top of the handler's checks, not a replacement:
| Check | UavUploadValidationFilter (filter layer) |
UavTileUploadHandler (handler layer — defence-in-depth) |
|---|---|---|
metadata form field present + non-empty |
Line 62-68 | Line 68-71 (string.IsNullOrWhiteSpace) |
metadata parses as JSON |
Line 71-84 (catches JsonException) |
Line 74-81 (catches JsonException) — same pattern, same ex.Message echo (pre-existing parallel to F-AZ810-1) |
metadata.items non-null + non-empty |
Line 86-92 (payload is null) + UavTileBatchMetadataPayloadValidator:25-26 (NotNull/NotEmpty) |
Line 83-86 (Items.Count == 0) |
metadata.items.Count == files.Count |
Line 105-118 | Line 88-91 |
metadata.items.Count <= MaxBatchSize |
UavTileBatchMetadataPayloadValidator:27-28 |
Line 93-96 |
The handler's envelope checks are still reachable by any caller invoking IUavTileUploadHandler.HandleAsync(...) directly (e.g. unit tests, or a future programmatic flow). Cycle 8 left them intact — correct ✓. The duplicated JsonException.Message echo in the handler (line 80) is a pre-existing-class instance of the same Low finding as F-AZ810-1; the remediation MUST sanitise both call sites in lock-step or the defence-in-depth path will continue to leak.
Pre-existing-Surface Inconsistency Noted (NOT a cycle-8 regression)
Program.cs:387-399 — the CreateRoute handler still wraps its body in try { ... } catch (ArgumentException ex) { return Results.BadRequest(new { error = ex.Message }); }. This is a pre-cycle-8 inconsistency: the response shape is the unstructured {error: string} object, NOT the ValidationProblemDetails shape mandated by error-shape.md v1.0.0. Cycle 8 added WithValidation<CreateRouteRequest> at line 268 which intercepts most ArgumentException cases — so the catch block is now largely dead code, but it WILL fire if IRouteService.CreateRouteAsync itself throws ArgumentException (e.g. a future internal validation rule). When it fires, the response leaks ex.Message in a non-conformant shape.
- Severity: Low / Informational. Pre-existing inconsistency; cycle 8 reduced its reachable surface but did not eliminate it.
- Remediation: Drop the
try/catchblock now that the validator covers the same cases at the filter layer; let unexpectedArgumentExceptionpropagate toGlobalExceptionHandlerfor uniform 400 + sanitisedValidationProblemDetails(or 500 with acorrelationIdif the cause is internal). One-line edit; recommend folding into the same cycle-9 follow-up as F-AZ795-1 / F-AZ810-1 since all three converge on the same error-shape consistency improvement.
Test Code Review
SatelliteProvider.Tests/Validators/*ValidatorTests.cs (8 new files)
- Pure CPU; no I/O, no network, no file system, no DB.
- All inputs constructed inline. No fixture-file reads, no hardcoded JWTs.
- All test files share the cycle-7
ValidatorTestModuleInitializer.cs([ModuleInitializer]→GlobalValidatorConfig.ApplyOnce()at test-assembly load) — single source of truth for the camelCase property resolver. No test drift risk. - ✓ No findings.
SatelliteProvider.IntegrationTests/{CreateRouteValidationTests,GetTileByLatLonValidationTests,RegionFieldRenameTests,RegionRequestValidationTests,UavUploadValidationTests}.cs + modified helpers
- All five new files use the runner-side
JwtTestHelpers.MintAuthenticated(...)to attach a Bearer token, mirroring cycle 7's pattern. No hardcoded secret material; the token's signing secret comes from theJWT_SECRETenv var (32+ bytes, dev-only indocker-compose.tests.yml). - Test inputs use raw
HttpRequestMessagewith hand-built JSON strings — exercises the exact wire shape the validator + deserializer see in production. Each file covers the cycle-8 negative cases for its endpoint. ProblemDetailsAssertions.cswas extended (additive) — no removed assertions. The cycle-7error-shape.mdInv-2 / Inv-4 contract assertions all still apply.UavUploadTests.cs+UavUploadValidationTests.csboth clamp their fixture-generated coordinates into non-overlapping OSM-valid sub-ranges (cycle-8 hotfix commitb763da3per_docs/03_implementation/batch_04_cycle8_report.md§ AC-9) — a test-data correctness fix, not a security finding.- ✓ No findings.
scripts/probe_{latlon,region,route,upload}_validation.sh (4 new files)
- All four scripts begin with
set -euo pipefail— fail-fast on undefined vars, broken pipes, command failures. - All four read
${API_URL:-https://localhost:8080}(default to localhost) and${JWT:-}with an explicit "ERROR: set JWT env var" guard thatexit 2s if$JWTis empty. No embedded credentials. curl -kis used (justified — the dev cert is self-signed; the scripts target localhost in dev/test only — documented in each script's header).- ✓ No findings — same posture as cycle-7's
probe_inventory_validation.sh.
scripts/run-performance-tests.sh (modified, wire-rename only)
- Diff is exclusively the AZ-812 wire-format rename:
?Latitude=…&Longitude=…&ZoomLevel=…→?lat=…&lon=…&zoom=…and{"latitude":…,"longitude":…}→{"lat":…,"lon":…}across PT-01 through PT-08 invocations. No new code, no new credentials, no shell-injection surface introduced. - ✓ No findings.
Cycle-7 Carry-overs (still open at cycle-8 tip)
| Finding | Source cycle | Carry-over status | Cycle-8 interaction |
|---|---|---|---|
F-AZ795-1 — JsonException.Message propagated in GlobalExceptionHandler.cs:108-117 |
cycle 7 | open | Unchanged. Cycle 8 introduced F-AZ810-1 which is the SAME pattern in a NEW code path; both need lock-step sanitiser remediation. |
F-AZ795-2 — Generic BadHttpRequestException.Message propagated in GlobalExceptionHandler.cs:88-93 |
cycle 7 | open | Unchanged. Cycle 8 routes more endpoint failures through WithValidation<T>() which builds ValidationProblemDetails directly — reducing the practical reachability of this path on the 4 newly-validated endpoints, but not eliminating it (model-binding failures pre-validator can still hit the path). |
Verdict (Phase 2)
PASS_WITH_WARNINGS — 1 Medium (F-AZ809-1) + 2 new Lows (F-AZ810-1, F-AZ810-2) + 2 cycle-7 Low carry-overs (F-AZ795-1, F-AZ795-2). No Critical or High findings.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — not FAIL (FAIL is reserved for Critical or High). F-AZ809-1 is exploitable only by an authenticated tenant operator with a valid JWT (the route endpoint requires RequireAuthorization() without a permission scope, so any tenant with API access reaches it). The Medium is contained within the cycle-8 threat model — every cycle-8 endpoint is auth-gated — but should be the highest-priority cycle-9 follow-up: pre-existing-class Mediums tend to be the entry vector for higher-severity issues when adjacent threat-model assumptions shift (e.g. if a future feature exposes the route endpoint to an untrusted-tenant audience).
The 2 new Lows + 2 carry-over Lows are not release-blockers in isolation; they are filed for the next cycle's follow-up batch.