mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 12:11:11 +00:00
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1470,6 +1470,185 @@ Every test is observed at the SPA's public surface — DOM, ARIA, outbound netwo
|
||||
|
||||
---
|
||||
|
||||
### FT-N-16: mission-planner `getWeatherData` fail-soft when `VITE_OWM_API_KEY` is unset
|
||||
|
||||
**Traces to**: AC-42 (AZ-499 AC-3)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_OWM_API_KEY=""` (or undefined).
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` | spy installed, no calls yet |
|
||||
| 2 | Stub `import.meta.env.VITE_OWM_API_KEY = ""` and invoke `getWeatherData(50, 30)` | resolves |
|
||||
| 3 | Inspect return value | `=== null` |
|
||||
| 4 | Inspect fetch spy | `mock.calls.length === 0` |
|
||||
|
||||
**Pass criteria**: function returns `null` AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
|
||||
**Max execution time**: 1s (env stub + sync inspection only).
|
||||
**Expected result source**: AZ-499 AC-3 (no `results_report.md` row needed — behavioral test, no input data).
|
||||
|
||||
---
|
||||
|
||||
## Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
|
||||
|
||||
The scenarios below were appended via `/test-spec` cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to `traceability-matrix.md`; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per `_docs/03_implementation/batch_11_report.md`; AZ-499 AC-7 is a manual deliverable, not a test).
|
||||
|
||||
### FT-P-56: Self-hosted satellite tile URL is env-var resolved
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-1, AC-2)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_SATELLITE_TILE_URL` set, unset, or set with a trailing slash.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` and call `getTileUrl()` | returns the env value verbatim |
|
||||
| 2 | Stub `VITE_SATELLITE_TILE_URL=""` and call `getTileUrl()` | returns `DEFAULT_SATELLITE_TILE_URL` (`http://localhost:5100/tiles/{z}/{x}/{y}`) |
|
||||
| 3 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/` (trailing slash) | returns the value with the trailing slash stripped |
|
||||
| 4 | Mount `<FlightMap>` with the env unset; inspect rendered `<TileLayer>` `data-tile-url` | equals `DEFAULT_SATELLITE_TILE_URL` |
|
||||
|
||||
**Pass criteria**: all four assertions hold. Mirrors the established `getOwmBaseUrl()` / `getApiBase()` env-resolution pattern.
|
||||
**Max execution time**: 2s (jsdom render + four stub variations).
|
||||
**Expected result source**: AZ-498 AC-1, AC-2 (no `results_report.md` row needed — env-var plumbing, no input data fixture).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-57: `<TileLayer crossOrigin="use-credentials">` enables cookie-auth on tile fetches
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: `<FlightMap>` and `<MiniMap>` mounted with the default tile URL.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Mount `<FlightMap>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||||
| 2 | Mount `<MiniMap pointPosition={…}>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||||
| 3 | (e2e — gated) Issue `GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0>` from the rendered map; inspect outbound request | `request.credentials === "include"` (browser attaches the same-origin auth cookie) |
|
||||
|
||||
**Pass criteria**: every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
|
||||
**Max execution time**: 2s for steps 1+2 (fast); e2e step is part of `infrastructure.e2e.ts` — bounded by suite-e2e timeout.
|
||||
**Expected result source**: AZ-498 AC-3 (no `results_report.md` row — DOM-attribute observable).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-58: Classic/satellite map toggle, `mapType` state, and `MiniMap.Props.mapType` are removed
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-4)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: `<FlightMap>` mounted with the default tile URL; `<MiniMap>` mounted with only `pointPosition` (no `mapType` prop).
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Mount `<FlightMap>`; query `screen.queryByRole('button', { name: /satellite|classic/i })` | returns `null` |
|
||||
| 2 | Mount `<FlightMap>`; query `screen.getAllByTestId('tile-layer')` | length `=== 1` (no per-mode branching, single layer) |
|
||||
| 3 | Compile-time check: instantiate `<MiniMap pointPosition={…}>` without `mapType` | TypeScript `tsc --noEmit -p tsconfig.test.json` succeeds (STC-T1) |
|
||||
| 4 | Compile-time check: source-tree grep for any remaining `mapType` reference under `src/features/flights/` | zero hits (compilation error if not — covered by STC-T1) |
|
||||
|
||||
**Pass criteria**: no toggle button, no `mapType` state, `MiniMap.Props` has no `mapType`. Removal is permanent; the `flights.planner.satellite` i18n key was removed from both `en.json` and `ua.json` in lockstep (i18n key parity preserved via STC-FP22).
|
||||
**Max execution time**: 2s (jsdom render + grep).
|
||||
**Expected result source**: AZ-498 AC-4.
|
||||
|
||||
---
|
||||
|
||||
### FT-P-59: e2e harness exercises the new `/tiles/{z}/{x}/{y}` path
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-6); E1 (air-gap)
|
||||
**Profile**: e2e
|
||||
|
||||
**Input data**: suite-e2e compose stack up; `tile-stub` configured at `http://tile-stub:8082/tiles/{z}/{x}/{y}`.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | `infrastructure.e2e.ts` AC-2 — issue `GET http://tile-stub:8082/tiles/1/0/0` from the playwright runner | HTTP 200, response is a 256×256 image (JPEG) |
|
||||
| 2 | Inspect response headers | `Content-Type: image/jpeg`, `Cache-Control` present, `ETag` present |
|
||||
| 3 | Inspect outbound request from the SPA's `<TileLayer>` | URL matches `^http://tile-stub:8082/tiles/\d+/\d+/\d+$` (NOT `/{z}/{x}/{y}.png`, NOT the legacy `/sat/...` Esri shape) |
|
||||
| 4 | Inspect `EXTERNAL_HOSTS` route guard | OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
|
||||
|
||||
**Pass criteria**: tile fetch shape matches the satellite-provider contract documented at `_docs/02_document/contracts/satellite-provider/tiles.md`. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the `tile-stub` which accepts requests without a cookie.
|
||||
**Max execution time**: bounded by suite-e2e infrastructure-test timeout (per `e2e/tests/infrastructure.e2e.ts`).
|
||||
**Expected result source**: contract at `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0; AZ-498 AC-6.
|
||||
|
||||
---
|
||||
|
||||
### FT-P-60: mission-planner `getWeatherData` uses env-resolved key + base URL
|
||||
|
||||
**Traces to**: AC-42 (AZ-499 AC-1, AC-2, AC-4)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_OWM_API_KEY` set + `VITE_OWM_BASE_URL` either set, unset, or set with a trailing slash.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ wind: { speed: 5, deg: 90 } }` | spy installed |
|
||||
| 2 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL contains `appid=abc123` AND `units=metric` |
|
||||
| 3 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` |
|
||||
| 4 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5/` (trailing slash); invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` (slash stripped) |
|
||||
| 5 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://api.openweathermap.org/data/2.5/weather?` (default base) |
|
||||
| 6 | Inspect return value on a successful fetch | `=== { windSpeed: 5, windAngle: 90 }` (existing parsed-wind shape preserved) |
|
||||
|
||||
**Pass criteria**: every outbound URL is reconstructed from env vars; the public `getWeatherData(lat, lon)` signature and `WeatherData` return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
|
||||
**Max execution time**: 2s (env stubs + fetch-spy assertions; no real network).
|
||||
**Expected result source**: AZ-499 AC-1, AC-2, AC-4 (no `results_report.md` row — env-var plumbing).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-61: mission-planner `geocodeAddress` uses env-resolved Google API key
|
||||
|
||||
**Traces to**: AC-43 (AZ-501 AC-1)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` set to a placeholder string.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ status: 'OK', results: [{ geometry: { location: { lat, lng } } }] }` | spy installed |
|
||||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz`; invoke `geocodeAddress('Kyiv, Ukraine')` | outbound URL contains `key=env-key-xyz` AND `address=Kyiv%2C%20Ukraine` |
|
||||
| 3 | Inspect return value | `=== { lat, lng }` from the mocked response |
|
||||
|
||||
**Pass criteria**: the outbound URL is reconstructed from the env var; no literal key remains in `mission-planner/src/services/GeocodeService.ts` (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
|
||||
**Max execution time**: 2s.
|
||||
**Expected result source**: AZ-501 AC-1.
|
||||
|
||||
---
|
||||
|
||||
### FT-N-17: mission-planner `geocodeAddress` fail-soft when `VITE_GOOGLE_GEOCODE_KEY` is unset
|
||||
|
||||
**Traces to**: AC-43 (AZ-501 AC-3)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` empty / undefined.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch`; spy `console.warn` | spies installed |
|
||||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=''`; invoke `geocodeAddress('anywhere')` | returns `null`; fetch is NOT called; `console.warn` called exactly once with a message containing `VITE_GOOGLE_GEOCODE_KEY` |
|
||||
| 3 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz` and force `fetch` to reject with `Error('boom')`; invoke `geocodeAddress('anywhere')` | returns `null`; promise does NOT throw |
|
||||
|
||||
**Pass criteria**: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable".
|
||||
**Max execution time**: 2s.
|
||||
**Expected result source**: AZ-501 AC-3.
|
||||
|
||||
---
|
||||
|
||||
## Notes carried into Phase 3
|
||||
|
||||
- All tests tagged `quarantined` correspond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept.
|
||||
|
||||
@@ -35,14 +35,14 @@ The Azaion UI image carries no DB. The "Docker environment" is the test-time cho
|
||||
| `detect` | Suite `detect/` image | Sync image detect (and future async video detect F7) | per suite compose |
|
||||
| `gps-denied-desktop`, `gps-denied-onboard`, `autopilot`, `resource`, `loader` | Suite microservice images | Auxiliary services hit by the SPA (only `loader/` and `resource/` are hit on production paths today; `gps-denied-*` is target-only F12) | per suite compose |
|
||||
| `owm-stub` | Tiny HTTP server returning canned OpenWeatherMap responses | Replace direct OWM HTTPS (E10) so tests are deterministic and rate-limit-free | `8081` |
|
||||
| `tile-stub` | Tiny HTTP server returning a 256x256 PNG | Replace OSM tile servers | `8082` |
|
||||
| `tile-stub` | Tiny HTTP server serving `GET /tiles/{z}/{x}/{y}` → 256x256 JPEG with `Content-Type: image/jpeg`, `Cache-Control`, and `ETag` headers (mirrors the satellite-provider contract at `_docs/02_document/contracts/satellite-provider/tiles.md`) | Replace the suite's `satellite-provider` tile endpoint in the e2e profile (since cycle 2 / AZ-498). The stub does NOT enforce cookie auth — the same-origin cookie path is exercised once the cross-workspace satellite-provider cookie-auth ticket lands and tile traffic flows through the real service. | `8082` |
|
||||
| `test-db` | Suite-managed (Postgres per suite default) | Backs `admin/`, `flights/`, `annotations/` | Internal |
|
||||
|
||||
### Networks
|
||||
|
||||
| Network | Services | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (OWM + tile stubs replace the only external hops). |
|
||||
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (`owm-stub` + `tile-stub` replace the only external hops — OWM HTTPS, and since cycle 2 / AZ-498 the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint stands in for the previously-used external OSM/Esri tile servers). |
|
||||
|
||||
### Volumes
|
||||
|
||||
@@ -92,7 +92,7 @@ services:
|
||||
environment:
|
||||
BASE_URL: http://azaion-ui:80
|
||||
OWM_BASE_URL: http://owm-stub:8081
|
||||
TILE_BASE_URL: http://tile-stub:8082
|
||||
VITE_SATELLITE_TILE_URL: "http://tile-stub:8082/tiles/{z}/{x}/{y}"
|
||||
```
|
||||
|
||||
The compose file is part of the test-spec output; its concrete shape lands when the Decompose Tests step picks the runner (Step 5).
|
||||
@@ -129,7 +129,7 @@ The compose file is part of the test-spec output; its concrete shape lands when
|
||||
| Suite SSE | HTTPS | `/api/flights/<id>/live-gps`, `/api/annotations/annotations/events`, `/api/detect/stream/<jobId>` (F7 target) | bearer in `?token=` per ADR-008 |
|
||||
| Bundle / image inspection | filesystem / `docker inspect` | n/a | n/a |
|
||||
| OpenWeatherMap | HTTPS via `owm-stub` | per stub | none |
|
||||
| OSM tiles | HTTPS via `tile-stub` | per stub | none |
|
||||
| Satellite tiles | HTTPS via `tile-stub` (replacing the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint in the e2e profile) | per stub at `/tiles/{z}/{x}/{y}` | none in stub; production uses an HttpOnly same-origin cookie set by `admin/` (see `crossOrigin="use-credentials"` on every `<TileLayer>` per cycle 2 / AZ-498) |
|
||||
|
||||
### What the consumer does NOT have access to
|
||||
|
||||
@@ -192,7 +192,7 @@ Conclusion: classify as **Not hardware-dependent**. Docker headless Chromium rep
|
||||
3. **Compose up**: `docker compose -f e2e/docker-compose.suite-e2e.yml up -d` — brings up `azaion-ui`, `admin`, `flights`, `annotations`, `detect`, the auxiliary services, `owm-stub`, `tile-stub`, `test-db`, and the `playwright-runner`.
|
||||
4. **Run tests**: `docker compose -f e2e/docker-compose.suite-e2e.yml run --rm playwright-runner` — the runner image entrypoint is `bun run test:e2e`. Reports land in `./test-output/`.
|
||||
5. **Tear down**: `docker compose -f e2e/docker-compose.suite-e2e.yml down -v` (volumes wiped between runs).
|
||||
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `TILE_BASE_URL=http://tile-stub:8082`, `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
|
||||
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `VITE_SATELLITE_TILE_URL=http://tile-stub:8082/tiles/{z}/{x}/{y}` (since cycle 2 / AZ-498 — was `TILE_BASE_URL=http://tile-stub:8082`), `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
|
||||
|
||||
#### Local mode (for `fast` profile + developer-machine `e2e` runs)
|
||||
|
||||
|
||||
@@ -242,3 +242,35 @@ Failure / recovery scenarios at the SPA's observable boundary: bearer expiry, re
|
||||
|
||||
**Pass criteria**: row 97 — connection-lost indicator OR reconnect attempt within 10 s; stale data NOT rendered as live; reconnect attempts ≤ 1 in the 10 s window.
|
||||
**Expected result source**: `results_report.md` row 97.
|
||||
|
||||
---
|
||||
|
||||
### NFT-RES-11: Tile endpoint 401/503 does NOT crash the map
|
||||
|
||||
**Summary**: When the `satellite-provider /tiles/{z}/{x}/{y}` endpoint returns 401 (cookie-auth failure) or 503 (Google Maps upstream down), the SPA renders a broken-tile placeholder for the failing tile(s) and the rest of the application keeps working. No React error boundary fires; no full-page crash.
|
||||
**Traces to**: AC-41 (AZ-498 NFR-Reliability)
|
||||
|
||||
**Preconditions**:
|
||||
- `<FlightMap>` mounted with a valid `VITE_SATELLITE_TILE_URL`.
|
||||
- Tile endpoint configured to return 401 (auth failure) OR 503 (upstream provider down) for one or more tile coordinates.
|
||||
|
||||
**Fault injection**:
|
||||
- (auth-failure variant) Strip / invalidate the satellite-provider auth cookie before the SPA attempts a tile fetch; tile endpoint responds 401.
|
||||
- (upstream-down variant) Configure the test stub to return 503 for `GET /tiles/{z}/{x}/{y}`.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Action | Expected Behavior |
|
||||
|------|--------|------------------|
|
||||
| 1 | Mount `<FlightMap>`; trigger a tile load that fails per the fault | Leaflet emits a `tileerror` event for the affected coordinate |
|
||||
| 2 | Observe the rendered map | broken-tile placeholder shown in the failing cell; surrounding tiles continue rendering normally |
|
||||
| 3 | Observe the rest of the SPA (header, side panels, navigation) | remains interactive; no React error boundary fires; no console error of category `Uncaught` |
|
||||
| 4 | Observe a recovery path (auth restored OR upstream back) | next pan/zoom successfully fetches the tile; the placeholder is replaced with the imagery |
|
||||
|
||||
**Pass criteria**:
|
||||
- 401 response on a tile request MUST NOT crash the map; broken-tile placeholder rendered in the failing cell, rest of SPA interactive.
|
||||
- 503 response treated identically to 404/transient failure (fault budget — recovery path works after the upstream returns).
|
||||
- No new uncaught error in the console attributable to the failed tile.
|
||||
|
||||
**Expected result source**: AZ-498 NFR-Reliability (no `results_report.md` row needed — observable through DOM state and console).
|
||||
**Note on follow-up**: AZ-498 risk #5 flags an optional `tileerror` listener on `<MapContainer>` that surfaces a structured warning + an optional inline banner ("Imagery unavailable; please re-sign-in"). If/when that lands, this scenario gains a Step 5 asserting the banner appears within 2 s of the first tile error.
|
||||
|
||||
@@ -145,20 +145,41 @@ Blackbox security assertions against the SPA's observable surface: token storage
|
||||
|
||||
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
|
||||
|
||||
**Traces to**: AC-20, P10
|
||||
**Traces to**: AC-20, AC-42 (AZ-499 AC-5, AC-7), P10
|
||||
**Profile**: static (source) + static (bundle)
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected Response |
|
||||
|------|----------------|------------------|
|
||||
| 1 | Regex sweep `src/` and `mission-planner/src/` for the literal current OWM key value | `match_count == 0` (row 63) |
|
||||
| 2 | Regex sweep for `appid=` and `api_key=` literal occurrences in source URLs | `match_count == 0` (row 63) |
|
||||
| 3 | Scan `dist/**/*.js` post-build for the literal key | `match_count == 0` (Phase 3 may downgrade to "until Step 4 fix") |
|
||||
| 1 | `STC-SEC1` — Regex sweep `src/` for `appid=[a-zA-Z0-9]{6,}` (filtered to exclude `import.meta.env` / `process.env` references) | `match_count == 0` (row 63) |
|
||||
| 2 | `STC-SEC1B` — Scan `dist/**/*.js` post-build for the literal key value | `match_count == 0` (NFT-SEC-09 AC-1 dist portion) |
|
||||
| 3 | `STC-SEC1C` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed key (`335799082893fad97fa36118b131f919`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` | `match_count == 0` (row 63 — AZ-499 AC-5) |
|
||||
|
||||
**Pass criteria**: row 63.
|
||||
**Status**: `quarantined` for source check until Step 4 fix; the bundle-scan check passes immediately for `src/` (mission-planner not bundled, AC-31).
|
||||
**Expected result source**: `results_report.md` row 63.
|
||||
**Pass criteria**: row 63 (project-level AC-20) AND AZ-499 AC-5 (source scan must reject any future re-introduction of the literal key under `src/` or `mission-planner/`).
|
||||
**Status**: All three checks ACTIVE (no quarantine). The source check was un-quarantined on cycle 2 close (2026-05-12) when AZ-499 (a) replaced the hardcoded key in `mission-planner/src/services/WeatherService.ts` with `import.meta.env.VITE_OWM_API_KEY` and (b) added `STC-SEC1C` so a regression cannot silently re-introduce the literal across either source tree (closing the AZ-482 source-scan gap that previously only checked `src/` for the regex shape and `dist/` for the literal — `mission-planner/` stays out of `dist/` per STC-S5, so the dist scan alone could not catch it).
|
||||
**Defense-in-depth note**: the previously-committed key value (`335799082893fad97fa36118b131f919`) MUST be revoked at the OpenWeatherMap dashboard — this is AZ-499 AC-7, a manual deliverable, not a test. STC-SEC1C complements but does not replace key revocation.
|
||||
**Expected result source**: `results_report.md` row 63; AZ-499 AC-5.
|
||||
|
||||
---
|
||||
|
||||
### NFT-SEC-09b: Google Geocode API key is not shipped in source
|
||||
|
||||
**Traces to**: AC-43 (AZ-501 AC-1, AC-4, AC-6)
|
||||
**Profile**: static (source) + fast (env-resolution + fail-soft contract)
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected Response |
|
||||
|------|----------------|------------------|
|
||||
| 1 | `STC-SEC1D` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed Google key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=google_key_in_source` | `match_count == 0` (AZ-501 AC-4) |
|
||||
| 2 | Fast: import `mission-planner/src/services/GeocodeService.ts` and stub `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`; assert outgoing fetch URL contains the env-resolved key | URL contains `key=<env-value>` (AZ-501 AC-1; `tests/mission_planner_geocode.test.ts`) |
|
||||
| 3 | Fast: stub `VITE_GOOGLE_GEOCODE_KEY=''` and call `geocodeAddress('Kyiv')` | returns `null`, no fetch issued, single `console.warn` mentioning `VITE_GOOGLE_GEOCODE_KEY` (AZ-501 AC-3) |
|
||||
|
||||
**Pass criteria**: AZ-501 AC-1, AC-3, AC-4 — env-resolved + fail-soft + static gate against literal re-introduction.
|
||||
**Status**: ACTIVE on cycle 2 close (2026-05-12). The key was extracted from `mission-planner/src/config.ts` to a new `services/GeocodeService.ts` module to enable isolated env-resolution + fail-soft testing (mirrors AZ-499 / WeatherService pattern).
|
||||
**Defense-in-depth note**: the previously-committed key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`) MUST be revoked at the Google Cloud Console — this is AZ-501 AC-6, a manual deliverable, not a test. STC-SEC1D complements but does not replace key revocation.
|
||||
**Expected result source**: AZ-501 AC-1, AC-3, AC-4.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
|
||||
| AC-17 | ProtectedRoute spinner a11y + timeout | FT-P-32, FT-P-33 [Q], NFT-RES-04 [Q] | 58, 59 | Covered (quarantined for timeout) |
|
||||
| AC-18 | Browser support — Chromium + Firefox latest 2 | FT-P-34, NFT-PERF-10 | 60, 98 | Covered (manual smoke, no automated gate today) |
|
||||
| AC-19 | Mobile / desktop breakpoint variants | FT-P-35, FT-P-36 | 61, 62 | Covered |
|
||||
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 [Q for source until Step 4] | 63 | Covered (quarantined for source check) |
|
||||
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 (all 3 steps active — source check un-quarantined on cycle 2 / 2026-05-12 by AZ-499) | 63 | Covered |
|
||||
| AC-21 | UserSettings panel-width persistence | FT-P-37 [Q], FT-P-38 [Q], NFT-PERF-08 [Q] | 64, 65 | Covered (quarantined) |
|
||||
| AC-22 | RBAC client-side route gates | FT-N-03 [Q], FT-N-04, FT-N-05 [Q], NFT-SEC-05 [Q], NFT-SEC-06 [Q], NFT-RES-08 | 08, 09, 10 | Covered (quarantined for `/admin` + `/settings` gates) |
|
||||
| AC-23 | Auth refresh transparency | FT-P-02, FT-P-03, NFT-PERF-02, NFT-RES-01 | 11, 12 | Covered |
|
||||
@@ -51,6 +51,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
|
||||
| AC-N3 | No offline mode | NFT-RES-03, NFT-SEC-12 | 93 | Covered |
|
||||
| AC-N4 | No response-signature library | NFT-SEC-11 | 94 | Covered |
|
||||
| AC-N5 | Dropped legacy features (Sound Detections, Drone Maintenance) | NFT-SEC-13 | 95 | Covered |
|
||||
| AC-41 | Map tiles served by self-hosted `satellite-provider` via cookie auth; classic/satellite toggle removed (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-498) | FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11; STC-T1 (env-decl typecheck), STC-FP22 (i18n parity post-key removal), STC-ARCH-01 + STC-ARCH-02 (architecture gates stay green) | n/a — env-var plumbing + DOM observable + e2e contract; no `results_report.md` row required | Covered |
|
||||
| AC-42 | mission-planner OpenWeatherMap key + base URL externalized via Vite env vars; fail-soft on missing key; STC-SEC1C source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-499) | FT-P-60, FT-N-16; NFT-SEC-09 step 3 (STC-SEC1C); STC-T1 (env-decl typecheck) | 63 (literal-key scan shares row 63 with AC-20) | Covered (manual deliverable AZ-499 AC-7 — old key revocation at OWM dashboard — tracked separately, not a test) |
|
||||
| AC-43 | mission-planner Google Geocode API key extracted to a new `services/GeocodeService.ts` module + externalized via Vite env var; fail-soft + console.warn on missing key; STC-SEC1D source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12 from security audit `_docs/05_security/`, ticket AZ-501) | FT-P-61, FT-N-17; NFT-SEC-09b (STC-SEC1D); STC-T1 (env-decl typecheck) | n/a — env-var plumbing + console-warn assertion; no `results_report.md` row required | Covered (manual deliverable AZ-501 AC-6 — old key revocation at Google Cloud Console — tracked separately, not a test) |
|
||||
| AC-44 | Vite + PostCSS upgraded past CVE-2026-39363 / GHSA-p9ff-h696-f583 / GHSA-4w7w-66w2-5vf9 / GHSA-qx2v-qp2m-jg93 in both roots via `package.json` `overrides` flooring transitive resolutions to safe versions (added cycle 2 / 2026-05-12 from security audit, ticket AZ-502) | `bun audit` (zero advisories in both roots after `bun install`) | n/a — supply-chain hygiene; verified by audit tool exit code | Covered (CI gate `bun audit --severity high` in `.woodpecker/build-arm.yml` is a Phase B follow-up — see `_docs/05_security/infrastructure_review.md` F-INF-1) |
|
||||
|
||||
## Restrictions Coverage
|
||||
|
||||
@@ -104,10 +108,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
|
||||
|
||||
| Category | Total Items | Covered | Partially Covered | Not Covered | N/A (meta) | Coverage % (Covered+Partial) |
|
||||
|----------|-------------|---------|-------------------|-------------|-----------|--------------------|
|
||||
| Acceptance Criteria | 40 | 40 | 0 | 0 | 0 | 100% (24 fully ungated, 16 with Phase 3 quarantine markers) |
|
||||
| Acceptance Criteria | 44 | 44 | 0 | 0 | 0 | 100% (cycle-2 deltas: AC-41, AC-42, AC-43, AC-44 added; AC-20 source check no longer quarantined) |
|
||||
| Anti-Criteria | 5 | 5 | 0 | 0 | 0 | 100% |
|
||||
| Restrictions | 41 | 17 | 8 | 13 | 3 | 61% |
|
||||
| **Total** | **86** | **62** | **8** | **13** | **3** | **81%** |
|
||||
| **Total** | **90** | **66** | **8** | **13** | **3** | **82%** |
|
||||
|
||||
Acceptance criterion coverage exceeds the 75 % template threshold. Restriction coverage is short of 75 % because most of the un-covered restrictions are dependency-version pins (S1-S11) for which a single static check pass (planned `STC-S*` family) would lift them to Covered without changing the SPA's observable behavior.
|
||||
|
||||
@@ -128,7 +132,7 @@ Acceptance criterion coverage exceeds the 75 % template threshold. Restriction c
|
||||
|
||||
## Quarantine List (running)
|
||||
|
||||
The following 18 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition.
|
||||
The following 17 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition. (Cycle 2 / 2026-05-12 update: NFT-SEC-09 source check REMOVED from this list — closed by AZ-499 + STC-SEC1C; new AC-41 / AC-42 tests added in this cycle are NOT quarantined.)
|
||||
|
||||
| Test | Reason | Activates when |
|
||||
|------|--------|---------------|
|
||||
@@ -144,7 +148,6 @@ The following 18 tests assert against a Phase B target or a Step 4 fix and are q
|
||||
| NFT-PERF-03 / NFT-RES-02 | SSE refresh-rotation reconnect missing | Step 8 hardening |
|
||||
| NFT-PERF-08 / NFT-PERF-09 | Tied to FT-P-37 / FT-N-13 quarantines | per above |
|
||||
| NFT-SEC-05, NFT-SEC-06 | Tied to FT-N-03, FT-N-05 | per above |
|
||||
| NFT-SEC-09 (source check) | OpenWeatherMap key still in source today | Step 4 fix |
|
||||
| NFT-RES-04 | Tied to FT-P-33 | per above |
|
||||
|
||||
## Phase 3 (Data Validation Gate) — Open Items to Resolve
|
||||
|
||||
Reference in New Issue
Block a user