# Static Analysis — Azaion UI

**Date**: 2026-05-12
**Scope**: `src/` (production SPA), `mission-planner/src/` (port-source — NOT shipped in production bundle but in git history), `nginx.conf`, `.env.example` files
**Method**: targeted ripgrep patterns + manual review, complementing the 32 existing static checks in `scripts/run-tests.sh` (STC-SEC*, STC-N*, STC-S*, STC-ARCH-*)
**Cycle**: Phase B / Cycle 2

---

## Summary

| Severity | Count | New in this audit |
|----------|-------|-------------------|
| Critical | 0     | — |
| High     | 1     | F-SAST-1 (Google Geocode API key in `mission-planner/`) |
| Medium   | 2     | F-SAST-2 (`unpkg.com` CDN ref in `mission-planner/`), F-SAST-3 (`mission-planner` not covered by `STC-SEC2`) |
| Low      | 1     | F-SAST-4 (port-source still uses third-party tile fallbacks) |

**No NEW Critical or High findings in `src/` (production bundle).** All High-severity findings are confined to `mission-planner/` — the inferior port-source documented in `_docs/02_document/components/05_flights/description.md` as "not built; manual reference for porting work".

The 32 existing static checks (run-tests.sh) cover: no eval/Function, no `dangerouslySetInnerHTML`, no token logging, no `innerHTML=` writes, no banned ML/crypto/persistence libs, no hardcoded `/api` literals, TS strict mode, no `target=_blank`, no OWM key in `src/` or `mission-planner/`. All passed in the Cycle 2 test run (`_docs/03_implementation/test_run_report_phase_b_cycle2.md`).

---

## Findings

### F-SAST-1 — Hardcoded Google Geocode API key in `mission-planner/src/config.ts` — HIGH

**Location**: `mission-planner/src/config.ts:2`

```ts
export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys';
```

**Used by**: `mission-planner/src/flightPlanning/LeftBoard.tsx:114-115`

```ts
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_GEOCODE_KEY}`
```

**Production-bundle exposure**: NONE today. `src/` does NOT import from `mission-planner/` (verified via grep: zero matches for `from '.*mission-planner` in `src/`). The `Dockerfile` builds only the main project (`bun run build` produces `dist/` from the `src/` Vite root). `mission-planner/` is a port-source kept around for reference per `_docs/02_document/components/05_flights/description.md` line 59.

**Git-history exposure**: HIGH. The key is committed and visible to anyone who clones the repository, has read access to the upstream remote, or reads any historical revision. Same threat class as the OpenWeatherMap key resolved in **AZ-499** (`_docs/00_problem/security_approach.md` §5).

**Risk**:
- Quota/rate-limit theft (Google charges per geocode call past the free tier).
- Provider account abuse — whoever owns the Google Cloud billing account is liable.
- Accelerated risk if `mission-planner/` is ever ported into the production SPA without this finding being remediated first.

**Remediation** (mirrors AZ-499 / AC-42 pattern):
1. **Revoke the key** at https://console.cloud.google.com/google/maps-apis/credentials (manual, OUT-OF-BAND, USER ACTION). Capture evidence per the AZ-499 AC-7 protocol.
2. Externalize: `import.meta.env.VITE_GOOGLE_GEOCODE_KEY` in `mission-planner/src/config.ts`. Fail-soft if unset (mirror `WeatherService.ts` pattern from AZ-499).
3. Update `mission-planner/.env.example` to advertise the new variable + the `<your-google-geocode-api-key>` placeholder.
4. Extend the `owm_key_in_source` static-check pattern in `tests/security/banned-deps.json` to also block the literal `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys` (defense-in-depth — does not replace revocation).
5. Long-term: when geocoding lands in the production SPA, route via a suite-side proxy (no client-visible key — same architecture decision noted in `security_approach.md` §5 for OWM).

**Recommended ticket**: `AZ-NEW (Phase B) — Externalize Google Geocode key in mission-planner port-source` (mirror AZ-499 structure).

---

### F-SAST-2 — `unpkg.com` CDN reference in `mission-planner/` — MEDIUM

**Location**: `mission-planner/src/icons/PointIcons.tsx:7`

```ts
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
```

**Production-bundle exposure**: NONE today (same reason as F-SAST-1).

**Risk class**:
- Supply-chain: a compromised unpkg.com or a take-over of `leaflet@1.7.1` could replace the icon with a tracking pixel or attack payload.
- Privacy: the browser leaks the user's IP + referer to a third-party CDN on every page load that uses these icons.
- Air-gap incompatibility: the suite is documented as "air-gapped friendly" (`_docs/02_document/architecture.md`); a CDN dependency violates that.

The main `src/` is already protected: `STC-SEC2` (`scripts/run-tests.sh`) blocks `unpkg.com` in `src/`. **`mission-planner/` is currently NOT scanned by STC-SEC2** — see F-SAST-3.

**Remediation**:
- Replace with a relative import (the `leaflet` package is already a dependency; bundling the marker icon locally is one line).
- OR move this asset into the same-origin nginx static path during the eventual port.

**Recommended ticket**: bundle into the same Phase B port-source cleanup task as F-SAST-1.

---

### F-SAST-3 — `STC-SEC2` (no-CDN gate) does NOT scan `mission-planner/` — MEDIUM

**Location**: `scripts/run-tests.sh` (the `src_grep` helper passes `src` only for STC-SEC2)

**Evidence**: `STC-SEC2` is currently `src/`-scoped only; the `owm_key_in_source` and `alert_calls` checks were widened in AZ-499/AZ-466 to scan both `src/` and `mission-planner/` (see `scripts/check-banned-deps.mjs:204`), but the `unpkg.com`/CDN deny-pattern was not.

**Risk**: a future port that copies more `mission-planner/` code into `src/` could re-introduce CDN URLs that the current static gate would not catch on the source side.

**Remediation**:
- Move the no-CDN check into `tests/security/banned-deps.json` as a new section (e.g. `cdn_in_source`) and let `check-banned-deps.mjs` apply it to both roots, mirroring the AZ-499 widening pattern.
- Add the new STC-ID to `_docs/02_document/tests/security-tests.md`.

**Recommended ticket**: `AZ-NEW (Phase B) — Widen no-CDN static gate to cover mission-planner/` (small, 2-3 SP).

---

### F-SAST-4 — Port-source still uses third-party tile fallbacks — LOW

**Location**: `mission-planner/src/constants/tileUrls.ts:2-3`, `mission-planner/.env.example:25`

```ts
export const TILE_URLS = {
  classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
  satellite: import.meta.env.VITE_SATELLITE_TILE_URL || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
```

**Production-bundle exposure**: NONE today (port-source is not built).

**Risk**: if `mission-planner/` is ever shipped, AZ-498's gains are partially undone — the classic tile path goes straight to OSM with NO env override path, and the satellite fallback hits ArcGIS unauthenticated.

**Remediation**: deferred to whichever ticket consumes / replaces the `mission-planner/` port-source in the SPA. Do NOT fix in-place — `mission-planner/` is documented as inferior and slated for removal once the port is complete.

---

## Negative findings (clean — explicitly verified)

| Pattern | `src/` | `mission-planner/src/` | Coverage |
|---------|--------|------------------------|----------|
| `eval(`, `new Function(`, `setTimeout('...')` | clean | clean | manual grep this audit |
| `dangerouslySetInnerHTML`, `innerHTML=`, `outerHTML=`, `document.write` | clean | clean | manual grep this audit |
| `target='_blank'` (without `rel='noopener'`) | clean | clean | manual grep this audit |
| `console.log`/`console.error` of `token`/`bearer`/`password`/`secret`/`key`/`cookie`/`auth` | clean | clean | manual grep this audit |
| `__proto__`, `constructor[…]`, `prototype[…]` (prototype-pollution patterns) | clean | clean | manual grep this audit |
| `localStorage`/`sessionStorage`/`indexedDB` writes of bearer | clean (only test-fixture reads in `auth/AuthContext.test.tsx`) | clean | STC-SEC3 + manual grep |
| `credentials: 'include'` on every authed fetch | present on the 401-recovery path (`src/api/client.ts:90`); KNOWN MISSING on bootstrap refresh (`src/auth/AuthContext.tsx:24`, quarantined test acknowledges Step 4 fix) | n/a (no auth in port-source) | KNOWN — `security_approach.md` §1 finding F2 |
| Hardcoded OWM key `335799082893fad97fa36118b131f919` | clean | clean | STC-SEC1 + STC-SEC1B + STC-SEC1C (AZ-499) |
| Hardcoded URLs other than the OWM endpoint | clean (only `flightPlanUtils.ts:59` `DEFAULT_OWM_BASE_URL` — env-overridable fallback) | F-SAST-1, F-SAST-2, F-SAST-4 above | manual grep this audit |
| Other API-key formats: `AIza…`, `sk_live_`, `pk_live_`, `xox*`, `ghp_`, `AKIA…`, generic 32+ hex | clean | F-SAST-1 only | manual grep this audit |
| `password = '...'`/`secret = '...'`/`api_key = '...'` literals | clean (only `password` field labels and `AdminPage.tsx` form bindings) | clean | manual grep this audit |

---

## Cycle-2 delta — security review of AZ-498 + AZ-499 changes

| Change | Security review |
|--------|----------------|
| `src/features/flights/FlightMap.tsx`, `MiniMap.tsx` — `<TileLayer crossOrigin="use-credentials" url={getTileUrl()}/>` | OK. `crossOrigin="use-credentials"` only sends cookies to the SAME origin (`/tiles/{z}/{x}/{y}`) when production env points at the same-origin nginx path. Dev default `http://localhost:5100/...` is HTTP and DEV-ONLY (acknowledged in `.env.example:12-16`). Confirms cookie ride for tile auth without exposing the bearer. |
| `src/features/flights/types.ts:63` — `DEFAULT_SATELLITE_TILE_URL = 'http://localhost:5100/tiles/{z}/{x}/{y}'` | OK. Dev default; production `.env` MUST override. The `.env.example` documentation is explicit. No bearer leakage path. |
| `mission-planner/src/services/WeatherService.ts` — env-resolved key + base URL + fail-soft | OK — matches AZ-499 spec. Key never re-introduced in source (verified by STC-SEC1C). |
| `STC-SEC1C` static gate in `scripts/run-tests.sh` | OK. Defense-in-depth as designed; widens the `STC-SEC1*` family to scan `mission-planner/` for the literal OWM key. |
| Tests `src/features/flights/__tests__/satellite_tile.test.tsx`, `tests/mission_planner_weather.test.ts` | OK. Tests do not contain real secrets; they `vi.stubEnv` with placeholder strings. |

No security regressions introduced by Cycle 2.

---

## Self-verification

- [x] Both source trees scanned (`src/`, `mission-planner/src/`)
- [x] Each finding has a file path + line number + extract
- [x] Test files explicitly excluded from finding lines (only quoted as evidence of negative results)
- [x] Cycle 2 deltas individually reviewed
- [x] Existing static checks not duplicated — only NEW findings or coverage gaps reported
