mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 18:51:10 +00:00
f7dd6c98d8
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>
268 lines
11 KiB
Markdown
268 lines
11 KiB
Markdown
# Security Tests
|
|
|
|
Blackbox security assertions against the SPA's observable surface: token storage discipline, refresh cookie attributes, RBAC route gating, credentials flag, secrets-in-source checks, destructive-action policy, dependency hygiene. These complement the server's RBAC and the suite's security_approach (`_docs/00_problem/security_approach.md`); they do NOT replace server-side enforcement (O4).
|
|
|
|
### NFT-SEC-01: Bearer is never written to `localStorage` or `sessionStorage`
|
|
|
|
**Traces to**: AC-02, O2
|
|
**Profile**: static + e2e
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Static code-search on `src/` and `mission-planner/src/` for `localStorage.|sessionStorage.` near `bearer|token|accessToken` | `match_count == 0` |
|
|
| 2 | E2E: complete a login; inspect `localStorage` and `sessionStorage` keys | neither contains the bearer value |
|
|
|
|
**Pass criteria**: row 04 — `match_count == 0`; runtime storage does not contain the bearer.
|
|
**Expected result source**: `results_report.md` row 04.
|
|
|
|
---
|
|
|
|
### NFT-SEC-02: `document.cookie` does not expose the refresh token
|
|
|
|
**Traces to**: AC-03
|
|
**Profile**: e2e + static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Static code-search for `document.cookie` reads against `refreshToken|refresh-cookie` | `match_count == 0` (row 05) |
|
|
| 2 | E2E: complete login; read `document.cookie` from page context | returned string does NOT contain the refresh-token value (row 06) |
|
|
|
|
**Pass criteria**: rows 05 + 06.
|
|
**Expected result source**: `results_report.md` rows 05, 06.
|
|
|
|
---
|
|
|
|
### NFT-SEC-03: Refresh cookie attributes — `Secure`, `HttpOnly`, `SameSite=Strict`
|
|
|
|
**Traces to**: AC-03, E3, O5
|
|
**Profile**: e2e
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Login via `POST /api/admin/auth/login` against the suite stack | `Set-Cookie` header returned |
|
|
| 2 | Inspect header value | matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (case-insensitive, attribute-order-tolerant) |
|
|
|
|
**Pass criteria**: row 07 — regex match.
|
|
**Notes**: this is a server-contract assertion; the UI test exists as defence-in-depth so a suite regression is caught before it lands in production.
|
|
**Expected result source**: `results_report.md` row 07.
|
|
|
|
---
|
|
|
|
### NFT-SEC-04: `credentials: 'include'` is set on every authenticated fetch
|
|
|
|
**Traces to**: AC-01, O3
|
|
**Profile**: fast (apiClient wrapper) + e2e (live capture)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Issue an authenticated request via `apiClient` | RequestInit captured |
|
|
| 2 | Inspect | `credentials === 'include'` (row 01) |
|
|
| 3 | Repeat for the bootstrap refresh | same (row 02 — `quarantined` until Step 4 bootstrap fix) |
|
|
|
|
**Pass criteria**: rows 01 + 02.
|
|
**Expected result source**: `results_report.md` rows 01, 02.
|
|
|
|
---
|
|
|
|
### NFT-SEC-05: `/admin` route blocks non-admins client-side (defence in depth)
|
|
|
|
**Traces to**: AC-22
|
|
**Profile**: e2e — `quarantined` until role-gate is added (Step 4 / Step 8)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Log in as `op_alice` (Operator, no admin role) | session active |
|
|
| 2 | Navigate to `/admin` | URL changes |
|
|
| 3 | Inspect final URL + DOM | URL is `/flights`; `<AdminPage>` NOT mounted |
|
|
|
|
**Pass criteria**: row 08.
|
|
**Notes**: server-side RBAC is authoritative; the UI gate is a usability + leakage layer.
|
|
**Expected result source**: `results_report.md` row 08.
|
|
|
|
---
|
|
|
|
### NFT-SEC-06: `/settings` route gate is applied per RBAC
|
|
|
|
**Traces to**: AC-22
|
|
**Profile**: e2e — `quarantined`
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Log in as user without SETTINGS permission | session active |
|
|
| 2 | Navigate to `/settings` | URL changes |
|
|
| 3 | Inspect | URL is `/flights`; `<SettingsPage>` NOT mounted |
|
|
|
|
**Pass criteria**: row 10.
|
|
**Expected result source**: `results_report.md` row 10.
|
|
|
|
---
|
|
|
|
### NFT-SEC-07: `alert()` is forbidden anywhere in the SPA
|
|
|
|
**Traces to**: AC-14, O10
|
|
**Profile**: static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `\balert\(` | `match_count == 0` |
|
|
|
|
**Pass criteria**: row 50.
|
|
**Expected result source**: `results_report.md` row 50.
|
|
|
|
---
|
|
|
|
### NFT-SEC-08: ConfirmDialog gates every destructive action
|
|
|
|
**Traces to**: AC-14, AC-30, O10
|
|
**Profile**: fast
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | For each destructive surface in `_docs/ui_design/` (class delete, user deactivate, dataset bulk-overwrite, etc.) | sequence checked |
|
|
| 2 | Confirm sequence on click → before any HTTP fires | dialog present (row 51) |
|
|
| 3 | On Confirm in class-delete flow → exactly one DELETE to `^/api/admin/classes/[0-9]+$` | (row 49) |
|
|
|
|
**Pass criteria**: rows 49 + 51.
|
|
**Expected result source**: `results_report.md` rows 49, 51.
|
|
|
|
---
|
|
|
|
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
|
|
|
|
**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 | `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 (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.
|
|
|
|
---
|
|
|
|
### NFT-SEC-10: No in-browser ML libs
|
|
|
|
**Traces to**: AC-N2
|
|
**Profile**: static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Parse `package.json` and `mission-planner/package.json` dependencies | dependency lists |
|
|
| 2 | Match against `^(onnxruntime|@?tensorflow(?:js)?(?:/.*)?|tflite|coreml|tfjs|@huggingface/.*|transformers\.js)$` | zero matches |
|
|
|
|
**Pass criteria**: row 92.
|
|
**Expected result source**: `results_report.md` row 92.
|
|
|
|
---
|
|
|
|
### NFT-SEC-11: No response-signature / JOSE libs on the request path
|
|
|
|
**Traces to**: AC-N4
|
|
**Profile**: static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Parse `package.json` dependencies | list |
|
|
| 2 | Match against `^(jsrsasign|tweetnacl|@noble/.*|jose)$` | zero matches |
|
|
|
|
**Pass criteria**: row 94.
|
|
**Expected result source**: `results_report.md` row 94.
|
|
|
|
---
|
|
|
|
### NFT-SEC-12: No service worker — offline mode is explicitly absent
|
|
|
|
**Traces to**: AC-N3
|
|
**Profile**: e2e
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Load the SPA in a fresh browser context | app boots |
|
|
| 2 | Read `navigator.serviceWorker.getRegistrations()` | empty array |
|
|
|
|
**Pass criteria**: row 93 — no service worker registered.
|
|
**Expected result source**: `results_report.md` row 93.
|
|
|
|
---
|
|
|
|
### NFT-SEC-13: Dropped legacy features are not present in source
|
|
|
|
**Traces to**: AC-N5
|
|
**Profile**: static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `SoundDetections|DroneMaintenance` | `match_count == 0` |
|
|
|
|
**Pass criteria**: row 95.
|
|
**Expected result source**: `results_report.md` row 95.
|
|
|
|
---
|
|
|
|
### NFT-SEC-14: Anti-criterion AC-N1 — no concurrent-edit reconciliation surfaces
|
|
|
|
**Traces to**: AC-N1
|
|
**Profile**: e2e + static
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected Response |
|
|
|------|----------------|------------------|
|
|
| 1 | Open the same annotation in two browser sessions; edit both | both save individually |
|
|
| 2 | Inspect each session's DOM | no merge UI; no presence indicator |
|
|
|
|
**Pass criteria**: row 91.
|
|
**Notes**: this is an anti-criterion — the test enforces that the feature is NOT silently added.
|
|
**Expected result source**: `results_report.md` row 91.
|