Files
ui/_docs/02_document/tests/security-tests.md
Oleksandr Bezdieniezhnykh f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
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>
2026-05-12 05:31:11 +03:00

11 KiB

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.nearbearer
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`
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.


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)?(?:/.*)?

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

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`

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.