chore(13-11): bump to v0.3.0, update docs, roadmap, and Phase 13 security gate evidence
- Version bump: 0.2.6 → 0.3.0 (Phase 13 complete — full cloud mutation surface shipped) - CLAUDE.md: update current-state, shared module map, and Phase 13 non-negotiable rules - README.md: add cloud file management, connection health, and authorized preview features; add Phase 13 mutation API table; mark Phase 13 complete - ROADMAP.md: mark Phase 13 11/11 plans complete - SECURITY.md: add Phase 13 threat register (T-13-01 through T-13-34), gate evidence (766 backend + 429 frontend tests pass, bandit 0 HIGH, npm audit 0 high/critical, gitleaks 3 pre-existing)
This commit is contained in:
+106
@@ -493,3 +493,109 @@ Phase 12.1 scopes only read-only browse, freshness truthfulness, and cross-provi
|
||||
- **Phase 13 mutations excluded:** upload, rename, move, delete, create-folder are not implemented
|
||||
- **Phase 14 byte cache excluded:** no document content download, local copy, or cache layer
|
||||
- No threat for cloud mutations (T-13-xx) or byte cache (T-14-xx) is evaluated in Phase 12.1
|
||||
|
||||
---
|
||||
|
||||
## Phase 13 — Virtual-Local Cloud Operations (2026-06-23)
|
||||
|
||||
**Audit date:** 2026-06-23
|
||||
**Phase:** 13 — Virtual-Local Cloud Operations (plans 13-01 through 13-11)
|
||||
**ASVS Level:** L2
|
||||
**Auditor:** gsd-security-auditor (claude-sonnet-4-6)
|
||||
**Phase scope:** connection health/reconnect/disconnect, authorized open/preview, sequential upload queue with typed conflict resolution, create-folder and rename with collision retry and stale guard, move with descendant safety and cross-connection block, delete with typed disclosure, metadata-only audit events across all 4 providers.
|
||||
|
||||
### Phase 13 Threat Register
|
||||
|
||||
| Threat ID | Threat | Disposition | Status | Evidence |
|
||||
|-----------|--------|-------------|--------|----------|
|
||||
| T-13-01 | IDOR — cloud mutation crosses user boundary | mitigate | CLOSED | `resolve_owned_connection` in `services/cloud_operations.py` — ownership asserted before any mutation; `test_foreign_user_cannot_browse_cloud_item`, `test_admin_cannot_browse_cloud_connection` still pass |
|
||||
| T-13-02 | Credential exposure in mutation response | mitigate | CLOSED | All mutation endpoints return `JSONResponse` with `CloudMutationResult`-shaped body; `credentials_enc`, `access_token`, `refresh_token` absent; `test_cloud_security.py::test_browse_response_excludes_credentials_and_raw_fields` still passes |
|
||||
| T-13-03 | SSRF via reconnect or upload URL | mitigate | CLOSED | WebDAV/Nextcloud upload and reconnect paths reuse `validate_cloud_url` from `storage.cloud_utils`; `test_ssrf_url_validation_invariants` still passes |
|
||||
| T-13-04 | Audit log contains provider bytes or credentials | mitigate | CLOSED | `write_audit_log` in `services/cloud_operations.py` writes only `metadata_`-level fields (`kind`, `provider_item_id`, `display_name`, `byte_size`); `test_cloud_audit.py::test_upload_success_writes_metadata_only_audit_row`, `test_move_audit_contains_no_credentials`, `test_delete_audit_contains_no_bytes` |
|
||||
| T-13-11 | Credential plaintext at provider boundary | mitigate | CLOSED | Credentials decrypted inside `run_*` helpers only; decrypted value not returned in any `CloudMutationResult`; `test_cloud_mutations.py` asserts typed results contain no token fields |
|
||||
| T-13-13 | Health check exposes connection status to wrong user | mitigate | CLOSED | `POST /connections/{id}/test` asserts ownership via `resolve_owned_connection`; returns `{"state": ..., "error_code": ..., "error_message": ...}` only (no credentials); `test_cloud_reconnect.py::test_health_check_wrong_owner_403` |
|
||||
| T-13-14 | Reconnect stores tokens in browser/response | mitigate | CLOSED | `run_reconnect` persists refreshed credentials via `encrypt_credentials`; response is `{"status": "active"}` only; `test_cloud_reconnect.py::test_reconnect_persists_token_not_exposes_it` |
|
||||
| T-13-15 | Drive broader-scope consent missing from UI | mitigate | CLOSED | `SettingsCloudTab.vue` contains `data-test="gdrive-scope-notice"` with "all files in your Google Drive" copy (D-17); `SettingsCloudTab.health.test.js::test_gdrive_scope_notice` |
|
||||
| T-13-16 | Silent overwrite on name conflict | mitigate | CLOSED | All 4 provider upload implementations return `conflict` kind with `reason: "name_conflict"` when a file exists at the target name; StorageBrowser pauses queue for user resolution; `test_cloud_backends.py::*_upload_conflict_*` |
|
||||
| T-13-17 | Raw provider URL returned from open/download | mitigate | CLOSED | `GET /connections/{id}/open/{item_id}` and `/download/{item_id}` proxy bytes through DocuVault and return `application/octet-stream` or `application/pdf`; no `Location` header with provider URL; `test_cloud_mutations.py::test_open_file_never_returns_provider_url` |
|
||||
| T-13-18 | WebDAV SSRF bypass through upload path | mitigate | CLOSED | `WebDAVBackend.upload` calls `validate_cloud_url` on the resolved upload target before submission; `test_webdav_backend.py::test_upload_rejects_ssrf_url` |
|
||||
| T-13-19 | Upload reconciliation bypass | mitigate | CLOSED | `run_upload` in `services/cloud_operations.py` calls `upsert_cloud_item` then `update_folder_state` only on `MUT_KIND_UPLOADED`; non-success branches bypass reconciliation; `test_cloud_mutations.py::test_upload_conflict_skips_reconcile` |
|
||||
| T-13-20 | Audit payload secrecy (upload) | mitigate | CLOSED | `metadata_` dict contains only `kind`, `provider_item_id`, `display_name`, `byte_size`; `test_cloud_audit.py::test_upload_success_writes_metadata_only_audit_row` asserts no token/url/content |
|
||||
| T-13-21 | False success audit event on failure path | mitigate | CLOSED | Conflict, offline, and reauth_required branches in `run_upload` return before `write_audit_log`; `test_cloud_audit.py::test_upload_conflict_writes_no_audit_row` |
|
||||
| T-13-22 | Queue resume flow implicit | mitigate | CLOSED | StorageBrowser requires explicit `upload-queue-resolve` event for all five resolution actions; no implicit or silent paths; `StorageBrowser.capabilities.test.js::test_queue_requires_explicit_resolution` |
|
||||
| T-13-23 | Preview/download UI exposes provider URL | mitigate | CLOSED | `onFileOpen` calls `api.openCloudFile` (DocuVault-authorized endpoint); `window.open()` to provider URL is forbidden by rule (D-02/T-13-07); `CloudFolderRenderedFlow.test.js::test_open_uses_open_cloud_file_api` |
|
||||
| T-13-24 | Collision naming unbounded | mitigate | CLOSED | `run_create_folder` and `run_rename` use bounded retry loop (≤5 attempts) with `keep_both_name()`; collision exhaustion returns typed error; `test_cloud_mutations.py::test_create_folder_collision_exhaustion` |
|
||||
| T-13-25 | Stale mutation creates duplicate on external change | mitigate | CLOSED | Stale guard in `run_create_folder`, `run_rename` calls `update_folder_state(refresh_state="warning")` and returns `stale` kind before provider submission; `test_cloud_mutations.py::test_create_folder_stale_guard_*`, `test_rename_stale_guard_*` |
|
||||
| T-13-26 | Identity reconciliation missing after create/rename | mitigate | CLOSED | `run_create_folder` and `run_rename` call `upsert_cloud_item` with returned provider_item_id before returning success; `test_cloud_mutations.py::test_create_folder_reconciles_item`, `test_rename_reconciles_item` |
|
||||
| T-13-27 | Move to descendant or cross-connection | mitigate | CLOSED | `run_move` checks descendant chain + self-move + connection-id equality before provider submission; `test_cloud_mutations.py::test_move_rejects_descendant_destination`, `test_move_rejects_cross_connection` |
|
||||
| T-13-28 | Stale move operates on changed item | mitigate | CLOSED | Stale guard in `run_move` stops mutation, refreshes source folder state, returns `stale` kind; `test_cloud_mutations.py::test_move_stale_guard_*` |
|
||||
| T-13-29 | Delete audit leaks bytes or credentials | mitigate | CLOSED | Typed delete results carry `is_folder`/`item_kind`; audit metadata contains no token or byte; `test_cloud_audit.py::test_delete_audit_contains_no_bytes` |
|
||||
| T-13-30 | Health UX auto-probes on navigate | mitigate | CLOSED | `testConnection` is a store action never called from navigation; `CloudFolderRenderedFlow.test.js::no_health_probe_on_folder_navigate_D13` asserts `testCloudConnection` never called during browse |
|
||||
| T-13-31 | Destructive cloud action without disclosure | mitigate | CLOSED | Delete requires `is_folder`/`supports_trash` disclosure to UI before confirmation; StorageBrowser renders permanent-delete warning for non-trash providers; `CloudFolderRenderedFlow.test.js::test_delete_shows_folder_warning` |
|
||||
| T-13-32 | Google Drive broader scope no consent copy | mitigate | CLOSED | `data-test="gdrive-scope-notice"` with "all files in your Google Drive" copy confirmed in SettingsCloudTab; `SettingsCloudTab.health.test.js::test_gdrive_scope_notice` |
|
||||
| T-13-33 | Closeout docs claim unshipped features | mitigate | CLOSED | Phase 13 closeout plan is isolated to documentation/version/gate tasks only; no implementation work in plan 13-11 |
|
||||
| T-13-34 | Secret scan skipped at release | mitigate | CLOSED | Secret scan via `gitleaks detect --redact` run locally; 3 pre-existing findings (all from before Phase 13, none from Phase 13 files); no hardcoded API keys, tokens, or credentials in any Phase 13 file |
|
||||
|
||||
### Phase 13 Security Gate Evidence
|
||||
|
||||
**Gate 1 — Full backend test suite:**
|
||||
```
|
||||
docker compose run --rm backend pytest -q --tb=no
|
||||
766 passed, 17 skipped, 4 deselected, 10 xfailed
|
||||
(1 pre-existing failure: test_extract_docx — ModuleNotFoundError: No module named 'docx', libmagic/python-docx not installed in container; unrelated to Phase 13)
|
||||
```
|
||||
|
||||
**Gate 2 — Frontend test suite:**
|
||||
```
|
||||
cd frontend && npm run test
|
||||
429 passed (48 test files)
|
||||
```
|
||||
|
||||
**Gate 3 — Bandit (backend static analysis):**
|
||||
```
|
||||
python3 -m bandit -r backend/ --exclude backend/tests
|
||||
Total issues (by severity): Undefined: 0, Low: 11, Medium: 0, High: 0
|
||||
Total issues (by confidence): Undefined: 0, Low: 0, Medium: 2, High: 9
|
||||
(All High-confidence findings are Low-severity — pre-existing pattern; 0 HIGH severity findings)
|
||||
```
|
||||
|
||||
**Gate 4 — npm audit (frontend dependency scan):**
|
||||
```
|
||||
cd frontend && npm audit --audit-level=high
|
||||
found 0 vulnerabilities
|
||||
```
|
||||
|
||||
**Gate 5 — pip-audit (backend dependency scan):**
|
||||
pip-audit not installable via local Python 3.9 (requirements.txt targets Python 3.12). No new Python packages added in Phase 13 plans 03 through 11. Existing packages audited in Phase 8 (0 critical/high CVEs). Security-critical packages remain pinned: PyJWT 2.12+, pwdlib[argon2], cryptography, pyotp, slowapi. This is the same local tooling gap documented in Phase 12.
|
||||
|
||||
**Gate 6 — Hardcoded-secret scan:**
|
||||
```
|
||||
gitleaks detect --redact
|
||||
3 findings — all pre-existing in commits from before Phase 13 (auth.test.js 2026-05-31, test_cloud_utils.py 2026-05-28, test_auth_deps.py 2026-05-22). No findings in any Phase 13 file.
|
||||
```
|
||||
Additionally: `git ls-files '.env' '.env.*'` returns only `.env.example` (no `.env` tracked).
|
||||
|
||||
**Gate 7 — Owner/admin/credential-secrecy invariants:**
|
||||
- `test_cloud_security.py::test_foreign_user_cannot_browse_cloud_item` — PASS (IDOR block)
|
||||
- `test_cloud_security.py::test_admin_cannot_browse_cloud_connection` — PASS (admin block)
|
||||
- `test_cloud_security.py::test_browse_response_excludes_credentials_and_raw_fields` — PASS (no credentials in response)
|
||||
- `test_cloud_security.py::test_ssrf_url_validation_invariants` — PASS (SSRF protection)
|
||||
- `test_cloud_audit.py::test_upload_success_writes_metadata_only_audit_row` — PASS (audit secrecy)
|
||||
- `CloudFolderRenderedFlow.test.js::no_health_probe_on_folder_navigate_D13` — PASS (no-probe invariant)
|
||||
|
||||
**Gate 8 — docker compose config:**
|
||||
```
|
||||
docker compose config --quiet
|
||||
(no output — all required environment variables resolve without error)
|
||||
```
|
||||
|
||||
### Phase 13 Accepted Risks
|
||||
|
||||
| Risk ID | Component | Accepted Risk | Rationale |
|
||||
|---------|-----------|---------------|-----------|
|
||||
| T-13-SC-pip | pip-audit local tooling | pip-audit not runnable against Python 3.12 requirements in Python 3.9 local env | No new Python packages added in Phase 13; existing packages audited in Phase 8; same accepted gap as Phase 12 |
|
||||
| T-13-docx | test_extract_docx failure | ModuleNotFoundError: python-docx/libmagic not in container image | Pre-existing; unrelated to Phase 13; all Phase 13 test coverage passes |
|
||||
|
||||
### Phase 13 Unregistered Flags
|
||||
|
||||
None. Phase 13 adds a write surface (mutations) but all mutation endpoints are owner-scoped, credential-free in responses, SSRF-protected on WebDAV/Nextcloud paths, and produce metadata-only audit events. No unregistered attack surfaces were discovered during plan execution.
|
||||
|
||||
Reference in New Issue
Block a user