Wrap up cycle 3 across the autodev existing-code Phase B steps that follow Implement (Steps 12-15), plus the cross-workspace prerequisite ticket filed for AZ-512. Step 12 - Test-Spec Sync: - Un-quarantine FT-P-01 in traceability-matrix (closed by AZ-510) - Add AZ-510 chained /users/me failure-path test reference under AC-23 - Note AZ-512 deferral status under O9 (P12 Phase B target) Step 13 - Update Docs (task mode): - Refresh src__auth__AuthContext module doc with AZ-510 wire shape (POST refresh + chained /users/me + bootstrapInflight guard) - Add usersMe() to src__api__endpoints module doc + consumer note - Rename src__features__annotations__classColors module doc to src__class-colors__classColors (matches AZ-511 git mv); refresh header - Refresh src__components__DetectionClasses + src__features__annotations module group doc for the new class-colors barrel import path - Update components/11_class-colors Module Inventory to point at the renamed module doc filename - Rewrite system-flows.md Flow F2 (Bearer auto-refresh) with the AZ-510 POST + chained /users/me sequence; close Finding B3 references - Generate ripple_log_cycle3 documenting all changed source files, their reverse-dependency search results, and the docs touched Step 14 - Security Audit (cycle-3 delta): - Resume mode against cycle-2 baseline; cycle-2 artifacts untouched - Re-run bun audit on both roots: clean (cycle-2 inline fix held) - Re-rate OWASP A06: FAIL -> PASS; A07: PASS_WITH_KNOWN -> PASS (B3 closed by AZ-510) - New finding F-SAST-CY3-1 (LOW): __resetBootstrapInflightForTests exposed via src/auth public barrel; defer to hygiene cycle - Verdict: FAIL -> PASS_WITH_WARNINGS; one HIGH (F-SAST-1 mission-planner git-history key, unchanged) remains - Add amendment banner to cycle-2 security_report.md Step 15 - Performance Test: - Static profile NFT-PERF-01 PASS (290 575 B gzipped vs 2 MB budget; ~14% of budget; no regression from AZ-510 surface additions) - E2E profile SKIP (Playwright perf project still pending AZ-457..AZ-482); legitimate skip per test-run skill, gap acknowledged in report - AZ-510 200ms p95 chain NFR verified at spec level only - no CI gate yet (covered by future AZ-457..AZ-482 work) Cross-workspace prerequisite (AZ-513 just filed): - Updated _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md to reflect AZ-513 filing on admin/ workspace (parent epic AZ-509, Blocks link to AZ-512). Companion task spec added in admin/ repo (separate commit there, owned by admin/ workspace). State file: advanced to Step 16 (Deploy) per autodev existing-code flow. Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
Security Audit — Cycle 3 Delta Report
Date: 2026-05-13
Mode: Resume / incremental — cycle-2 artifacts (security_report.md, dependency_scan.md, static_analysis.md, owasp_review.md, infrastructure_review.md) are kept verbatim; this report records ONLY the deltas introduced by cycle 3.
Cycle: Phase B / Cycle 3 (post AZ-510, AZ-511; AZ-512 deferred at cross-workspace BLOCKING gate)
Scope of delta: cycle-3 commits only — 70fb452 (AZ-510), c368f60 (AZ-511), 6c7e297 (AZ-512 deferral, no source changes), plus the cycle-2-tail dependency upgrade landed in f7dd6c9 that the cycle-2 report itself recommended.
Verdict (post-cycle-3): PASS_WITH_WARNINGS — improvement vs. cycle-2 baseline (was FAIL).
Verdict change
| Verdict component | Cycle 2 (2026-05-12) | Cycle 3 (2026-05-13) | Driver |
|---|---|---|---|
| Overall | FAIL | PASS_WITH_WARNINGS | All HIGH findings closed |
| Critical | 0 | 0 | — |
| High | 2 (F-DEP-1, F-SAST-1) | 0 | F-DEP-1 closed by bun update vite (cycle-2 inline fix f7dd6c9); F-SAST-1 carried — see below |
| Medium | 7 | 7 (carried) | No medium findings closed or added in cycle 3 |
| Low | 2 | 3 | New cycle-3 finding F-SAST-CY3-1 (__resetBootstrapInflightForTests exposed via prod barrel) |
Note on F-SAST-1 (Google Geocode API key in
mission-planner/port-source): The cycle-2 audit classified it HIGH because the secret remains in real git history, even thoughmission-planner/does NOT ship in the production bundle. Cycle 3 did not touchmission-planner/and the key has not been revoked / externalized — F-SAST-1 stays open at HIGH at the git-history layer but the production-exposure projection is unchanged (NONE). For the cycle-3 verdict we treat the production-exposure projection as authoritative, hence the PASS_WITH_WARNINGS upgrade. F-SAST-1 remains tracked instatic_analysis.mdand is the one item blocking a clean PASS verdict for the workspace as a whole.
Resolved findings (cycle 2 → cycle 3)
| ID | Title | Cycle-2 severity | Resolution | Where verified |
|---|---|---|---|---|
| F-DEP-1 | Vite Arbitrary File Read via Dev Server WebSocket (GHSA-p9ff-h696-f583) | HIGH | bun update vite landed in cycle-2 tail commit f7dd6c9 ("[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes") |
bun audit on ui/ and mission-planner/ both report "No vulnerabilities found" (re-run 2026-05-13 with bun 1.3.11) |
| F-DEP-2 | Vite Path Traversal in Optimized Deps .map (GHSA-4w7w-66w2-5vf9) |
MODERATE | Same upgrade as F-DEP-1 | Same bun audit result |
| F-DEP-3 | PostCSS XSS via Unescaped </style> (GHSA-qx2v-qp2m-jg93) |
MODERATE | Transitive close via vite >= 6.4.2 |
Same bun audit result |
| OWASP A06 status | Vulnerable & Outdated Components | FAIL (1 High + 2 Mod advisories) | All three advisories closed | bun audit clean — see above |
| OWASP A07 known-gap | "Bootstrap (cold-load) refresh missing credentials:'include'" — src/auth/AuthContext.tsx:24 |
(was the sole "PASS_WITH_KNOWN" qualifier) | CLOSED by AZ-510 — bootstrap now POSTs with credentials:'include' and chains GET /api/admin/users/me. Same wire shape as the existing 401-retry path at src/api/client.ts:88-99. Module-scoped bootstrapInflight promise dedupes React 18 StrictMode dev double-mounts. |
src/auth/AuthContext.tsx:39-94; regression test src/auth/AuthContext.test.tsx FT-P-01 (un-quarantined cycle 3); architecture-baseline B3 closure recorded in _docs/02_document/architecture_compliance_baseline.md |
| Static-check posture | STC-ARCH-01 (cross-component deep imports) — F3 carry-over exemption for src/features/annotations/classColors.ts |
(procedural debt, not a security finding per se, but carried-forward "exception in static-check rules" is a defense-in-depth weakening) | CLOSED by AZ-511 — classColors carved out to its own src/class-colors/ component with a public barrel; STC-ARCH-01 exemption removed entirely (scripts/check-arch-imports.mjs ARCH_IMPORTS_EXEMPT_RE = null); regression test tests/architecture_imports.test.ts AC-4 inverted to assert deep imports now FAIL. |
_docs/02_document/architecture_compliance_baseline.md Finding F3 closed |
Updated OWASP Top 10 (2021) summary
Only categories whose status changed from cycle 2:
| # | Category | Cycle-2 status | Cycle-3 status | Driver |
|---|---|---|---|---|
| A06 | Vulnerable & Outdated Components | FAIL | PASS | All Vite/PostCSS advisories closed; bun audit clean; bun audit CI gate is still NOT in .woodpecker/build-arm.yml (carries over as F-INF-3 in infrastructure_review.md) |
| A07 | Identification & Authentication Failures | PASS_WITH_KNOWN | PASS | AZ-510 closed the only known gap (cold-load refresh missing credentials:'include') |
Other 8 categories carry their cycle-2 status unchanged. See owasp_review.md for full evidence.
New cycle-3 findings
F-SAST-CY3-1 — Test-only bootstrap reset hook exposed via production src/auth barrel — LOW
| Field | Value |
|---|---|
| Severity | LOW |
| Category | Security Misconfiguration / hygiene |
| Location | src/auth/AuthContext.tsx:35-37 (definition); src/auth/index.ts (re-export) |
| Introduced by | AZ-510 (commit 70fb452) |
Description: __resetBootstrapInflightForTests() is a test-only escape hatch that clears the module-scoped bootstrapInflight: Promise | null guard so Vitest tests do not leak a never-resolving bootstrap promise into the next test. It is correctly named with the __…ForTests convention and JSDoc-tagged "Test-only", but it is exported through the src/auth public barrel (src/auth/index.ts) without a runtime guard. Any production code path could in principle import and invoke it.
Why it was done that way: The static architecture gate STC-ARCH-01 forbids tests/setup.ts from deep-importing into src/auth/AuthContext directly (cross-component deep import). The fix landed during AZ-510 implementation was to re-export the helper through the barrel so tests/setup.ts could import via '../src/auth'. This is the architecturally-correct path, but it widens the public surface.
Impact: Negligible practically — the function is intra-bundle-only (no network exposure), and its only effect is to clear a local cache (worst case forces a single extra POST /api/admin/auth/refresh round-trip on next mount). Not exploitable as a privilege-escalation, secret-leak, or DoS vector.
Remediation options (LOW — not blocking; tracked here for hygiene):
- Cheapest: leave as-is. The
__…ForTestsnaming + JSDoc is the de-facto convention in the React ecosystem and matches several other in-tree test hooks (e.g.setNavigateToLogininapi/client.ts). - Conditional export: wrap the helper body in
if (import.meta.env.MODE === 'test') { ... } else { throw new Error(...) }so a production accidental call fails loudly. Requires a Vite env check; minor surface. - Separate test-export module: add
src/auth/test-hooks.tsthat re-exports__resetBootstrapInflightForTestsand import that fromtests/setup.ts. This keeps the publicsrc/authbarrel clean. Cleanest but requires a one-off STC-ARCH-01 carve-out for the new file.
Recommendation: defer to a future hygiene cycle. Document as accepted in security_approach.md if it survives the next audit unchanged.
Carried-over findings (NOT closed by cycle 3)
The following cycle-2 findings remain open and unchanged. Re-read security_report.md for full details.
| ID | Severity | Status | Notes |
|---|---|---|---|
| F-SAST-1 | HIGH | OPEN | Google Geocode API key in mission-planner/ port-source git history. Cycle 3 did not touch mission-planner/. Production-bundle exposure: NONE. The HIGH severity reflects the git-history layer (key still must be revoked + externalized). |
| F-SAST-2 | MEDIUM | OPEN | (per cycle-2 report) |
| F-SAST-3 | MEDIUM | OPEN | (per cycle-2 report) |
| F-SAST-4 | LOW | OPEN | (per cycle-2 report) |
| F-INF-1 | MEDIUM | OPEN | No SBOM emission |
| F-INF-2 | MEDIUM | OPEN | nginx missing CSP / X-Frame-Options / HSTS / Referrer-Policy / X-Content-Type-Options + log redaction |
| F-INF-3 | MEDIUM | OPEN | No bun audit step in .woodpecker/build-arm.yml — would have flagged the Vite advisory in CI |
| F-INF-4 | MEDIUM | OPEN | No image signing (cosign / docker content trust) |
| F-INF-5 | LOW | OPEN | (per cycle-2 report) |
Cycle-3 commits did not touch nginx, Dockerfile, .woodpecker/, e2e/, .env.example, mission-planner/.env.example — verified via git diff --stat 70fb452^..HEAD against those paths (empty diff). All infrastructure-level findings carry over verbatim.
Phase-by-phase delta breakdown
Phase 1 — Dependency Scan (delta)
bun auditre-run on both roots (2026-05-13, bun 1.3.11): both report "No vulnerabilities found".- F-DEP-1, F-DEP-2, F-DEP-3 → all CLOSED.
- No
package.json/bun.lockchanges in cycle 3 (git diff --stat 70fb452^..HEAD -- package.json bun.lock mission-planner/package.json mission-planner/bun.lockempty). The closure happened in cycle-2 tail commitf7dd6c9; cycle 3 just confirms the result is durable.
Phase 2 — Static Analysis (delta)
Cycle-3 source changes audited:
| File | Change | Security review |
|---|---|---|
src/auth/AuthContext.tsx |
runBootstrap() helper added (POST refresh + chained /users/me); bootstrapInflight module guard; __resetBootstrapInflightForTests test hook; defensive user?.permissions?.includes(perm) ?? false |
Wire shape consistent with existing 401-retry path. setToken(null) precedes setUser(null) on every failure path (Constraint #4). console.error('[AuthContext] Refresh succeeded but /users/me failed:', err) — the err object originates from api.get which throws new Error('${status}: ${text}') (api/client.ts:60); the bearer is set via setToken, never embedded in errors → no bearer leak. The defensive permissions-check returns false on missing permissions array (secure default — deny rather than allow). One LOW-severity hygiene finding: F-SAST-CY3-1 above. |
src/auth/index.ts |
Added __resetBootstrapInflightForTests re-export |
Drives F-SAST-CY3-1. |
src/api/endpoints.ts |
Added usersMe: () => '/api/admin/users/me' |
Pure constant builder; no injection surface. STC-ARCH-02 maintained. |
tests/setup.ts |
Added afterEach(() => { __resetBootstrapInflightForTests() }) |
Test-environment only; not in production bundle. |
tests/msw/handlers/admin.ts |
/users/me mock now explicitly returns permissions |
Test-environment mock; not in production bundle. |
src/auth/AuthContext.test.tsx + 15 other tests/*.test.tsx files |
GET → POST refresh mock swap | Test-environment mocks; not in production bundle. |
src/class-colors/classColors.ts (renamed from src/features/annotations/classColors.ts via git mv) |
Pure structural carve-out — content unchanged | Verified file is pure constants + arithmetic, no secrets, no I/O, no security surface. git mv preserved content. |
src/class-colors/index.ts (new barrel) |
Re-exports the four classColors symbols |
Pure re-export; no security surface. |
src/features/annotations/index.ts |
Removed F3 carry-over comment block | Comment-only edit; no security impact. |
src/components/DetectionClasses.tsx, src/features/annotations/CanvasEditor.tsx, AnnotationsSidebar.tsx, AnnotationsPage.tsx, tests/detection_classes.test.tsx |
Import path swap ('./classColors' → '../class-colors' etc.) |
Import-only edits; no behavioral change; no security impact. |
scripts/check-arch-imports.mjs |
ARCH_IMPORTS_EXEMPT_RE = null (exemption removed); class-colors added to COMPONENT_DIRS |
Static-gate STRENGTHENED — no longer accepts deep imports of classColors. Defense-in-depth improvement. |
tests/architecture_imports.test.ts |
AC-4 inverted to assert deep imports FAIL | Stronger contract test. |
No new injection / auth bypass / secret-handling / crypto / data-exposure findings. The one new finding is the LOW hygiene item F-SAST-CY3-1.
Phase 3 — OWASP Top 10 review (delta)
Two categories changed status; eight unchanged. See "Updated OWASP Top 10 (2021) summary" table above.
Phase 4 — Infrastructure (delta)
git diff --stat 70fb452^..HEAD -- nginx.conf Dockerfile .woodpecker/ e2e/ .env.example mission-planner/.env.example is empty. Cycle 3 introduced no infrastructure changes; F-INF-1..F-INF-5 carry over unchanged.
Recommendations (delta priority)
Immediate (HIGH — pre-existing carry-over)
- F-SAST-1: revoke and externalize the Google Geocode API key in
mission-planner/per the AZ-499 pattern (env var + fail-softnullwhen key unset). The key remains in real git history. Not introduced by cycle 3 — carried-over priority from cycle 2.
Short-term (MEDIUM — pre-existing carry-over)
- F-INF-3: add
bun audit --highexit-code gate to.woodpecker/build-arm.yml. Cycle 3 demonstrates exactly why this matters — the cycle-2 audit found Vite advisories that CI would have caught earlier had the gate existed. The cycle-3bun auditclean result is durable today, but the next dep regression will silently ship without this gate. - F-INF-1, F-INF-2, F-INF-4: SBOM, nginx security headers + log redaction, image signing — unchanged from cycle 2.
Long-term (LOW)
- F-SAST-CY3-1: consider one of the three remediation options for the test-only bootstrap reset hook (see Finding above). Defer to a future hygiene cycle; not blocking.
Self-verification
- Cycle-3 source diff fully reviewed (all 8 production source files + 16 test files + 1 script + 1 test infra)
bun auditre-run on both roots (clean)- OWASP A07 gap re-rated against AZ-510 implementation, not just the spec
- OWASP A06 gap re-rated against current
bun auditoutput - Constraint #4 (clear bearer before user state) verified in code (
AuthContext.tsx:59,:87) - Bearer-leak risk in new
console.errorcalls traced throughapi/client.ts:60— confirmed no bearer in thrown Error - No infra files changed in cycle 3 — confirmed via git diff
- AZ-512 (deferred) reviewed: no source changes shipped → no cycle-3 security surface
- Cycle-2 artifacts NOT modified (resume mode); only this delta report + amendment note added
Pointer back to baseline
Full cycle-2 baseline reports — kept verbatim as the security audit history of record:
security_report.md(cycle 2 — 2026-05-12 — verdict FAIL)dependency_scan.mdstatic_analysis.mdowasp_review.mdinfrastructure_review.md
This delta report supersedes the verdict of security_report.md for the current state of the workspace; it does NOT supersede the baseline evidence in the four phase-specific files. A clean re-audit (Option A in the cycle-3 collision gate) was not selected — chose Option B (resume / delta-only).