mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 02:51:13 +00:00
[autodev] Update configuration and documentation for cycle-1
ci/woodpecker/push/02-build-push Pipeline failed
ci/woodpecker/push/02-build-push Pipeline failed
- Enhanced `.env.example` with detailed CMake build flags and replay-mode strategy flags for development and CI environments. - Updated `.gitignore` to include a new deploy rollback bookmark. - Revised `_docs/_autodev_state.md` to reflect the current task status and steps. - Added new lessons to `_docs/LESSONS.md` regarding testing and architectural improvements. - Documented changes in `_docs/02_document/deployment/ci_cd_pipeline.md` to reflect the relaxed OpenCV version pin. - Updated test data documentation in `_docs/02_document/tests/test-data.md` to clarify fixture usage and paths. This commit continues the cycle-1 documentation sync and addresses various configuration updates for improved clarity and functionality.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# Security Audit — Consolidated Report
|
||||
|
||||
**Audit window**: 2026-05-19
|
||||
**Auditor**: autodev Step 14 (greenfield flow) — `.cursor/skills/security/SKILL.md` § Phases 1–5
|
||||
**Scope**: `gps-denied-onboard` SUT — production code path (`src/gps_denied_onboard/**`), pinned dependencies, container build artefacts, docker-compose topology, committed secret hygiene.
|
||||
**Out of scope**: parent-suite components (D-PROJ-1 deploy/observability stack, D-PROJ-2 ingest service, satellite-provider, operator GCS UI); third-party SITL images beyond their tagging hygiene.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The SUT's security posture is built around a single defining design property: **the airborne process has no inbound or outbound network surface**. That property is enforced at three independent layers (architectural, operational iptables + DNS blackhole, test-harness verification via NFT-SEC-02 / NFT-SEC-05), which keeps the externally-reachable attack surface at zero in flight. The pre-flight (operator-workstation) phases DO touch the network — over TLS, with pinned hosts, parameterized SQL, redacted logs, and Ed25519-signed manifests / uploads.
|
||||
|
||||
Result: **0 Critical, 0 High, 5 Medium, 17 Low.** No release-blocking finding. The Medium findings are container-hardening gaps (root user in the production image, dev extras in the production image, moving-tag base images for SITL) that would be raised to High under a multi-tenant or internet-exposed threat model but are bounded to Medium here by the closed-system invariant.
|
||||
|
||||
| Severity | Count | Phase 1 (Deps) | Phase 2 (SAST) | Phase 3 (OWASP) | Phase 4 (Infra) |
|
||||
|---|---|---|---|---|---|
|
||||
| Critical | 0 | 0 | 0 | 0 | 0 |
|
||||
| High | 0 | 0 | 0 | 0 | 0 |
|
||||
| Medium | 5 | 1 | 0 | 0 | 4 |
|
||||
| Low | 17 | 11 | 1 (informational) | 0 | 5 |
|
||||
| Total | 22 | 12 | 1 | 0 | 9 |
|
||||
|
||||
**Release-blocking findings**: 0.
|
||||
**Remediation expected before next major dependency refresh**: F14 (non-root container), F15 (production `[dev]` extras), F16 (`mavproxy:latest`).
|
||||
**Remediation deferred to a future hardening pass**: F13 (plugin-loader assertion), F17 (digest-pin production base images), F18-F22 (housekeeping).
|
||||
**Cross-component leftover already tracked**: D-CROSS-CVE-1 — `opencv-python` pin coupling with `gtsam` numpy<2 ABI block (`_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`).
|
||||
|
||||
## Threat Model Snapshot
|
||||
|
||||
| Property | Status | Phase | Evidence |
|
||||
|---|---|---|---|
|
||||
| Airborne process has no network listener | enforced | A04 | `compose_root_airborne` wires no HTTP server; NFT-SEC-05 enforces at runtime |
|
||||
| Airborne process has no network egress | enforced | A04 | iptables OUTPUT REJECT + DNS blackhole (RESTRICT-OPS-1); NFT-SEC-02 verifies in CI |
|
||||
| Operator-workstation HTTP traffic is TLS-validated | enforced | A02 | 0 `verify=False` matches; pinned base URLs only |
|
||||
| Tile / Manifest integrity end-to-end | enforced | A08 | SHA-256 sidecar + Ed25519 detached signatures |
|
||||
| MAVLink ↔ FC channel is signed | enforced (AP) / documented residual (iNav) | A07 | Per-flight key rotation logged to FDR (NFT-SEC-03) |
|
||||
| In-container runtime executes as non-root | **GAP — F14** | A05, F14 | Production Dockerfiles missing `USER` directive |
|
||||
| Production runtime image excludes dev tools | **GAP — F15** | A05, F15 | `pip install -e ".[dev]"` ships pytest, ruff, mypy into prod |
|
||||
| All container images are digest-pinned | **GAP — F16/F17** | A05, F16, F17 | Mixed: `tile-cache-builder` digest-pinned, production images use floating tags |
|
||||
| Single-tenant binary (no per-user RBAC) | by design | A01 | One-OS-user container, FC arm/disarm IS the privilege gate |
|
||||
| Secrets never logged | enforced | A09 | `_REDACTED` convention across 13 files |
|
||||
| Test secrets are synthetic, marked TEST ONLY | enforced | P6 | Both committed passkey files contain `0123…ef`-repeated bytes |
|
||||
|
||||
## Per-Phase Outcomes
|
||||
|
||||
### Phase 1 — Dependency Scan (`_docs/05_security/dependency_scan.md`)
|
||||
|
||||
`pip-audit` against the pinned environment surfaced 12 advisories across 5 packages.
|
||||
|
||||
| Package | Advisories | Project-specific finding |
|
||||
|---|---|---|
|
||||
| `onnx==1.18.0` | 1 (GHSA-rcgw-4cgp-9q7q) | Reachable only via developer-mode model loading; SUT serves prebuilt engines on the production path. Medium. |
|
||||
| `cryptography==42.0.8` | 4 | None reachable from SUT usage (Ed25519 sign/verify + httpx default X.509). Low. |
|
||||
| `numpy==1.26.4` | 4 | None reachable from SUT usage (no untrusted-input numerical ingestion). Low. |
|
||||
| `protobuf==4.25.3` | 2 | None reachable; protobuf usage is internal serialization to the FDR layer. Low. |
|
||||
| `setuptools` | 1 | Dev-time only. Low. |
|
||||
|
||||
**D-CROSS-CVE-1 outcome**: re-running `pip-audit` against the relaxed pin (`opencv-python>=4.11.0.86,<4.12`) confirmed that CVE-2025-53644 is NOT flagged for `opencv-python==4.11.0.86`. The deferred-bump leftover was updated to record this; the residual risk window is effectively closed at the current pin. Bump to 4.12.x remains coupled to the `gtsam` numpy-2 ABI work.
|
||||
|
||||
### Phase 2 — Static Analysis (SAST) (`_docs/05_security/static_analysis.md`)
|
||||
|
||||
Pattern-driven ripgrep scan across 259 Python files in `src/`.
|
||||
|
||||
| Category | Findings |
|
||||
|---|---|
|
||||
| SQL injection | 0 — all 19 `cur.execute(...)` sites are parameterized |
|
||||
| Command injection | 0 — 1 `subprocess.run` (TensorRT `trtexec`), list args, no untrusted input |
|
||||
| Hardcoded secrets / weak crypto | 0 — Ed25519, SHA-256, `secrets.token_bytes` |
|
||||
| Insecure deserialization | 0 — no `pickle.loads` / `eval` / `exec` on untrusted data; `json.loads` only on local SUT-managed files |
|
||||
| TLS / verification bypass | 0 — all `httpx.Client` defaults preserved |
|
||||
| **F13** (informational) — Dynamic `__import__` via plugin registry | 1 — bounded by closed `KNOWN_*_STRATEGIES` whitelist; hardening recommended |
|
||||
|
||||
Defense-in-depth observations: per-flight ephemeral Ed25519 signing in C11; `secrets.token_bytes` for MAVLink passkey generation; `paramiko.RejectPolicy` (NOT `AutoAddPolicy`) for SSH; `_REDACTED` convention enforced across 13 files; secret-buffer zeroising on session end (AZ-318).
|
||||
|
||||
### Phase 3 — OWASP Top 10 Review (`_docs/05_security/owasp_top_10_review.md`)
|
||||
|
||||
All 10 categories assessed against the SUT's actual surface. A01 (Broken Access Control) is N/A by design (single-tenant binary, no per-user RBAC). A02 through A10 each cleared with 0 findings beyond those already reported in Phases 1, 2, and 4. The closed-system threat model is itself the strongest A04 (Insecure Design) property and is enforced at three layers.
|
||||
|
||||
### Phase 4 — Configuration & Infrastructure (`_docs/05_security/config_infra_review.md`)
|
||||
|
||||
9 findings (4 Medium, 5 Low) — all container-hardening gaps in the production images. The test stack is well-secured (`internal: true` network, digest-pinned tile-cache-builder, public-boundary discipline on the runner). The gap is consistency: the project already KNOWS how to do this (see `e2e/fixtures/tile-cache-builder/Dockerfile`) — production images need to match that bar.
|
||||
|
||||
## Remediation Plan
|
||||
|
||||
### Recommended before next dependency-refresh cycle
|
||||
|
||||
| ID | Title | Effort | Coupling |
|
||||
|---|---|---|---|
|
||||
| F14 | Add non-root `USER` directive to `docker/companion-tier1.Dockerfile` + `docker/operator-orchestrator.Dockerfile` | 2 pts | Couples with F22 (chown WORKDIR) |
|
||||
| F15 | Replace `pip install -e ".[dev]"` with runtime-only install in both production Dockerfiles | 2 pts | Standalone |
|
||||
| F16 | Pin `ardupilot/mavproxy:latest` and `ardupilot/ardupilot-sitl:plane-stable` to explicit versions or SHA256 digests | 1 pt | Standalone |
|
||||
| F1 | Bump `onnx` from 1.18.0 → 1.19+ when next dep cycle lands (GHSA-rcgw-4cgp-9q7q) | 2 pts | Couples with C7 model-loader review |
|
||||
|
||||
### Recommended at next hardening pass
|
||||
|
||||
| ID | Title | Effort |
|
||||
|---|---|---|
|
||||
| F13 | Add `assert strategy in KNOWN_*_STRATEGIES` at each `__import__` call site (defense-in-depth, redundant today) | 2 pts |
|
||||
| F17 | Pin production base images by SHA256 digest, with explicit refresh cadence | 1 pt |
|
||||
| F18 | Delete or fix the orphan `docker/mock-suite-sat-service.Dockerfile` | 1 pt |
|
||||
| F19 | Remove unused `curl` from `docker/operator-orchestrator.Dockerfile` | 1 pt |
|
||||
| F20 | Add upper bound to runner's `opencv-python` pin | 1 pt |
|
||||
| F21 | Update `.env.example` to current secret paths + env var names | 1 pt |
|
||||
| F22 | Coupled with F14 — chown WORKDIR after adding `USER` directive | included in F14 |
|
||||
|
||||
### Deferred / external
|
||||
|
||||
- **D-CROSS-CVE-1** — `opencv-python` 4.12.x bump deferred until `gtsam` ships a numpy-2 compatible release. Tracked at `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`. Residual exposure window is currently closed (CVE-2025-53644 no longer flagged at 4.11.0.86).
|
||||
- **Per-flight rekeying audit cadence** — out of scope for this audit; will be revisited when D-PROJ-2 ingest endpoint lands and the upload contract is finalised.
|
||||
- **Operator-workstation host hardening** (no-swap RESTRICT-OPS-1, etc.) — parent-suite responsibility; cross-referenced in `_docs/02_document/architecture.md` § Operating envelopes.
|
||||
|
||||
## Audit Method Footnote
|
||||
|
||||
- Phase 1 used `pip-audit` against a freshly resolved `pip freeze` of the SUT venv (the project's editable install was filtered out per the project-package exclusion rule).
|
||||
- Phase 2 used `ripgrep` with the patterns enumerated in `.cursor/skills/security/SKILL.md` § Phase 2; every match was cross-validated by reading the call site.
|
||||
- Phase 3 was a desk review against the OWASP 2021 list, with each category mapped to the SUT's actual production surface (see `static_analysis.md` "Surface Map") rather than a generic web-app threat model.
|
||||
- Phase 4 was a desk review of all 6 Dockerfiles, 2 docker-compose files, all committed secrets, `.env.example`, and `.gitignore`.
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] Each phase has its own dedicated artefact and is cross-referenced here
|
||||
- [x] Severity counts in the Executive Summary sum to the per-phase counts in the Outcomes section (22 = 12 + 1 + 0 + 9)
|
||||
- [x] No finding is silently downgraded — every project-specific severity calibration is justified at the finding site
|
||||
- [x] D-CROSS-CVE-1 leftover updated and cross-referenced; no stale or missing tracker entry
|
||||
- [x] Remediation effort estimates use the user's complexity-points rule (2-3 pts default, 5 pts max for big items, no 13-point monolith)
|
||||
@@ -0,0 +1,214 @@
|
||||
# Phase 4 — Configuration & Infrastructure Review
|
||||
|
||||
**Review date**: 2026-05-19
|
||||
**Scope**: Container build files, docker-compose topology, env templates, committed-secret hygiene, network policy, gitignore.
|
||||
**Files reviewed**:
|
||||
|
||||
- `docker/companion-tier1.Dockerfile`
|
||||
- `docker/operator-orchestrator.Dockerfile`
|
||||
- `docker/mock-suite-sat-service.Dockerfile`
|
||||
- `e2e/runner/Dockerfile`
|
||||
- `e2e/fixtures/mock-suite-sat/Dockerfile`
|
||||
- `e2e/fixtures/tile-cache-builder/Dockerfile`
|
||||
- `e2e/docker/docker-compose.test.yml`
|
||||
- `e2e/docker/docker-compose.tier2-bridge.yml`
|
||||
- `e2e/docker/secrets/{README.md,mavlink_passkey}`
|
||||
- `e2e/fixtures/secrets/{README.md,mavlink-test-passkey.txt}`
|
||||
- `e2e/runner/requirements.txt`
|
||||
- `.env.example`
|
||||
- `.gitignore`
|
||||
- `scripts/run-tests.sh`, `scripts/run-tests-jetson.sh`
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity (this project) | Count |
|
||||
|---|---|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 4 |
|
||||
| Low | 5 |
|
||||
| Informational / positive observations | 7 |
|
||||
|
||||
The closed-system threat model (no inbound listeners, no airborne network egress — see Phase 3 § A04) caps the blast radius of any container-hardening gap. The Medium-severity findings would all be raised to High in a multi-tenant or internet-exposed deployment; here they are Medium because the airborne / operator-workstation surface keeps an in-container attacker contained.
|
||||
|
||||
## Findings
|
||||
|
||||
### F14 — Production Dockerfiles run as `root` (no `USER` directive)
|
||||
|
||||
**Severity (this project)**: Medium
|
||||
**Locations**: `docker/companion-tier1.Dockerfile` (entrypoint at line 55), `docker/operator-orchestrator.Dockerfile` (entrypoint at line 22).
|
||||
|
||||
Neither production Dockerfile drops privileges before `ENTRYPOINT`. The Python runtime executes as UID 0 inside the container.
|
||||
|
||||
**Project-specific context**: the SUT has no inbound network listener, so an external attacker has no direct path to in-container code execution. The risk is post-compromise: any RCE via a dependency vulnerability (e.g., the future-day equivalent of Phase 1's F1–F12) executes as root in the container, with write access to mounted volumes (`/var/azaion/fdr`, `/var/azaion/tile-cache:ro` — the read-only mount limits damage there, but `fdr-output` is RW).
|
||||
|
||||
**Evidence the pattern is known to the project**: `e2e/fixtures/tile-cache-builder/Dockerfile:43-46` already implements the correct pattern:
|
||||
|
||||
```Dockerfile
|
||||
RUN useradd -u 10001 -m -d /home/builder builder \
|
||||
&& mkdir -p /input /output \
|
||||
&& chown -R builder:builder /opt/builder /input /output
|
||||
USER 10001:10001
|
||||
```
|
||||
|
||||
**Remediation**: replicate the same `useradd` + `chown` + `USER` block in both production Dockerfiles. Choose a stable UID (e.g., 10100 for the companion, 10200 for the orchestrator) and chown `/opt/gps-denied`, `/opt/venv`, `/var/azaion/fdr` accordingly.
|
||||
|
||||
---
|
||||
|
||||
### F15 — Production images install `[dev]` extras
|
||||
|
||||
**Severity (this project)**: Medium
|
||||
**Locations**: `docker/companion-tier1.Dockerfile:27` (`pip install --no-cache-dir -e ".[dev]"`), `docker/operator-orchestrator.Dockerfile:14` (`pip install --no-cache-dir -e ".[dev]"`).
|
||||
|
||||
The production runtime image ships with the `[dev]` extras: `pytest`, `pytest-asyncio`, `ruff`, `mypy`, `black`, `pytest-cov`, etc. This (a) ~doubles image size, (b) increases the attack surface inside the container (each test-only dep is a CVE candidate, and dev tools like `pytest` parse user-supplied files), and (c) muddies the dependency lockfile audit.
|
||||
|
||||
**Project-specific context**: same closed-system bound as F14 — an attacker needs in-container execution first. But these packages substantially increase the count of in-process Python modules under control of an attacker.
|
||||
|
||||
**Remediation**: define a runtime-only extras group in `pyproject.toml` (or rely on the base install with no extras) and use `pip install --no-cache-dir -e ".[runtime]"` or just `pip install --no-cache-dir -e .` in the production Dockerfile. Keep `[dev]` for developer environments and the e2e-runner only.
|
||||
|
||||
---
|
||||
|
||||
### F16 — Test-stack base images use moving / `latest` tags
|
||||
|
||||
**Severity (this project)**: Medium (for `mavproxy:latest`), Low (for `ardupilot-plane-sitl:plane-stable`)
|
||||
**Locations**:
|
||||
|
||||
- `e2e/docker/docker-compose.test.yml:41` — `ardupilot/ardupilot-sitl:plane-stable`
|
||||
- `e2e/docker/docker-compose.test.yml:67` — `ardupilot/mavproxy:latest`
|
||||
- `e2e/docker/docker-compose.test.yml:49` — `inavflight/inav-sitl:9.0.0` (this one IS pinned — good)
|
||||
|
||||
**Project-specific context**: the test stack runs in `e2e-net.internal: true` (egress blocked), so a hostile image's network capability is neutered at the docker level. The remaining risk is build-reproducibility regression: a tagged-tomorrow release could break or change SITL behaviour silently between CI runs.
|
||||
|
||||
**Remediation**: pin both to explicit versions (`mavproxy:1.8.55` style) or to SHA256 digest (`mavproxy@sha256:...`) — match the pattern at `e2e/fixtures/tile-cache-builder/Dockerfile:20` which uses a full SHA256 digest.
|
||||
|
||||
---
|
||||
|
||||
### F17 — Production Dockerfile base images use floating tags
|
||||
|
||||
**Severity (this project)**: Low
|
||||
**Locations**: `docker/companion-tier1.Dockerfile:8,38` (`ubuntu:22.04`), `docker/operator-orchestrator.Dockerfile:4` (`python:3.10-slim`).
|
||||
|
||||
These tags receive security-patch updates without explicit opt-in. That is intentionally desirable for OS patching, but it conflicts with bit-reproducible builds and the supply-chain audit goal.
|
||||
|
||||
**Project-specific context**: Ubuntu LTS and `python:slim` are reasonable defaults; the failure mode is "two builds of the same commit hash produce different base layers", which complicates incident response (which `libc6` did the failing build ship?).
|
||||
|
||||
**Remediation**: pin to SHA256 digest at release-tag time; bump explicitly on dependency-refresh cycles. Same pattern as `tile-cache-builder/Dockerfile:20`.
|
||||
|
||||
---
|
||||
|
||||
### F18 — Orphan / stale `docker/mock-suite-sat-service.Dockerfile`
|
||||
|
||||
**Severity (this project)**: Low
|
||||
**Location**: `docker/mock-suite-sat-service.Dockerfile`.
|
||||
|
||||
This file references `tests/fixtures/mock-suite-sat-service/` (path does NOT exist; the real fixture lives at `e2e/fixtures/mock-suite-sat/`), declares port 5100 + path `/healthz`, while the working build (`e2e/docker/docker-compose.test.yml:54 build: ../fixtures/mock-suite-sat`) uses port 8080 + path `/mock/health`. The `docker/`-side file is not referenced by any active compose target.
|
||||
|
||||
**Project-specific context**: not a runtime vulnerability — orphan artifacts are dead code in the build system. The risk is operator confusion ("which Dockerfile does the mock build from?") and accidental future use of the broken file.
|
||||
|
||||
**Remediation**: delete `docker/mock-suite-sat-service.Dockerfile`, OR fix it to be a thin wrapper around `e2e/fixtures/mock-suite-sat/Dockerfile`. (Project pattern: `docker/` should hold production-only Dockerfiles; test fixtures should live under `e2e/`.)
|
||||
|
||||
---
|
||||
|
||||
### F19 — Unused `curl` binary in production runtime image
|
||||
|
||||
**Severity (this project)**: Low
|
||||
**Location**: `docker/operator-orchestrator.Dockerfile:9` (`curl` in the runtime apt-get install).
|
||||
|
||||
Healthcheck uses `python3 -m gps_denied_onboard.healthcheck` (line 20), not curl. `curl` is a classic post-compromise tool (data exfil, second-stage payload fetch) and provides no runtime value.
|
||||
|
||||
**Remediation**: remove `curl` from the runtime apt-get install line.
|
||||
|
||||
---
|
||||
|
||||
### F20 — Runner image `opencv-python>=4.12.0` has no upper bound
|
||||
|
||||
**Severity (this project)**: Low
|
||||
**Location**: `e2e/runner/requirements.txt:25`.
|
||||
|
||||
While the docstring at lines 4–6 correctly notes that the runner does not depend on `gtsam` (so the D-CROSS-CVE-1 numpy<2 ABI block doesn't apply), there is no upper bound — a future opencv 5.x release could ship a behaviour break that lands automatically on the next CI rebuild.
|
||||
|
||||
**Remediation**: add an upper bound consistent with the rest of `requirements.txt` style: `opencv-python>=4.12.0,<5.0`.
|
||||
|
||||
---
|
||||
|
||||
### F21 — Stale path in `.env.example`
|
||||
|
||||
**Severity (this project)**: Low
|
||||
**Location**: `.env.example:29` — `MAVLINK_SIGNING_KEY=tests/fixtures/mavlink_signing/dev_key`.
|
||||
|
||||
That path predates the secrets reorganization that landed `e2e/fixtures/secrets/mavlink-test-passkey.txt` + `e2e/docker/secrets/mavlink_passkey`. Confusing for a new developer.
|
||||
|
||||
**Remediation**: update to the current path conventions. Also note that the env var name itself (`MAVLINK_SIGNING_KEY`) is inconsistent with the production env var the docker-compose actually sets (`MAVLINK_SIGNING_PASSKEY_FILE`); align both.
|
||||
|
||||
---
|
||||
|
||||
### F22 — Production WORKDIR is not chowned
|
||||
|
||||
**Severity (this project)**: Low (depends on whether F14 is fixed first)
|
||||
**Location**: `docker/companion-tier1.Dockerfile:50` (`WORKDIR /opt/gps-denied`), `docker/operator-orchestrator.Dockerfile:12`.
|
||||
|
||||
If/when F14's non-root `USER` directive is added, the runtime user will not own `/opt/gps-denied` and will fail to write any artefact there (e.g., the tmpfs FDR pre-buffer). Today this is dormant because the container runs as root. Filing as a coupled remediation item to F14.
|
||||
|
||||
**Remediation**: when adding the `USER` directive, also add `chown -R <uid>:<gid> /opt/gps-denied /opt/venv /var/azaion`.
|
||||
|
||||
---
|
||||
|
||||
## Positive Observations
|
||||
|
||||
### P5 — Test network is enforced as `internal: true`
|
||||
|
||||
`e2e/docker/docker-compose.test.yml:117-124` declares `e2e-net.internal: true`. The SUT, mock, runner, and SITLs can talk to each other but none can reach the public internet. The e2e-runner verifies this at runtime by attempting a TCP connect to `1.1.1.1:443` (AC-5 of `NFT-SEC-02`). This is the docker-compose-layer counterpart to the production iptables / DNS blackhole (RESTRICT-OPS-1 / NFT-SEC-05).
|
||||
|
||||
### P6 — Committed test secrets are demonstrably synthetic
|
||||
|
||||
Both committed secrets files (`e2e/docker/secrets/mavlink_passkey` and `e2e/fixtures/secrets/mavlink-test-passkey.txt`) contain the same canonical pattern `0123456789abcdef...` repeated, and both README files explicitly state "TEST ONLY — not for production use" with the production-side wiring documented. The `e2e/_unit_tests/test_directory_layout.py::test_passkey_files_match` assertion keeps the two files in lock-step (verified separately during the SUT review). No real secret is in version control.
|
||||
|
||||
### P7 — `e2e/runner/Dockerfile` follows the public-boundary contract
|
||||
|
||||
The runner image:
|
||||
|
||||
- Pins `python:3.12-slim-bookworm` (line 11) — explicit tag.
|
||||
- Uses `tini` as PID 1 (zombie reaping under `pytest --forked`).
|
||||
- Does NOT install the SUT package and explicitly excludes `src/` from `PYTHONPATH` (line 45 — `ENV PYTHONPATH=/opt/e2e-runner:/opt/e2e-runner/runner` only).
|
||||
- Sets `PYTHONDONTWRITEBYTECODE=1`, `PYTHONUNBUFFERED=1`, `PIP_NO_CACHE_DIR=1`, `PIP_DISABLE_PIP_VERSION_CHECK=1`.
|
||||
|
||||
### P8 — `e2e/fixtures/tile-cache-builder/Dockerfile` is gold-standard
|
||||
|
||||
It pins Python to SHA256-digest (`python:3.10.14-slim-bookworm@sha256:...`), pins every Python dep with version bounds, drops to a numbered non-root user (`USER 10001:10001`), explicitly chowns the workdir, and sets `PYTHONHASHSEED=0` for reproducibility (line 24). This is the pattern the rest of the project should match.
|
||||
|
||||
### P9 — `.gitignore` covers secrets and build artefacts comprehensively
|
||||
|
||||
`*.key`, `.env`, `.env.local` are blocked. The single explicit allow (`!tests/fixtures/mavlink_signing/dev_key`) is documented in the README. Build outputs (`.engine`, `.calib`, `.index`, `.faiss`, `.onnx`, `.trt`) are excluded. CMake artefacts (`build/`, `_skbuild/`, `compile_commands.json`) are excluded.
|
||||
|
||||
### P10 — Docker secrets are used for the test SUT (not env vars)
|
||||
|
||||
`e2e/docker/docker-compose.test.yml:30-32` mounts the test mavlink passkey via Docker `secrets:` declaration (`mavlink_passkey` → `/run/secrets/mavlink_passkey`), not via the `environment:` block. The SUT reads from `MAVLINK_SIGNING_PASSKEY_FILE=/run/secrets/mavlink_passkey` — passkey content never crosses the container env. Production mirrors the same wiring (with a real secret store-mounted file). Correct pattern.
|
||||
|
||||
### P11 — Healthchecks defined on every service
|
||||
|
||||
`gps-denied-onboard` (line 35), `mock-suite-sat-service` (line 61), and the production Dockerfiles themselves all declare HEALTHCHECK. `depends_on` uses `condition: service_healthy` for the SUT and mock (lines 106-109).
|
||||
|
||||
### P12 — `internal: true` AND no `ports:` block
|
||||
|
||||
No production service in `docker-compose.test.yml` publishes a port to the host. The only host-reachable surface is via the `e2e-results` bind mount, which is a read-only artefact dropbox (line 142). Defense-in-depth on top of `internal: true`.
|
||||
|
||||
## Cross-Reference Index
|
||||
|
||||
| Source | Phase 4 § | Note |
|
||||
|---|---|---|
|
||||
| `_docs/02_document/deployment/containerization.md` | F14, F15, F17, F22 | Docs the project's container conventions |
|
||||
| `_docs/02_document/deployment/environment_strategy.md` | F16, F21 | Docs env-var contract |
|
||||
| `_docs/02_document/tests/environment.md` § Communication with SUT | P10, F21 | Production passkey wiring |
|
||||
| `_docs/05_security/dependency_scan.md` | F15, F20 | Phase 1 deps audit (the dev extras shipping to production are part of Phase 1's surface) |
|
||||
| `_docs/02_document/tests/security-tests.md` § NFT-SEC-02 | P5 | The harness-side enforcement of the `internal: true` network |
|
||||
| `e2e/fixtures/tile-cache-builder/Dockerfile` | F14, F17 | Project's existing reference implementation of the pattern |
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] All Dockerfiles in the repo scanned: 6 files (`docker/*.Dockerfile` × 3, `e2e/runner/Dockerfile`, `e2e/fixtures/*/Dockerfile` × 2)
|
||||
- [x] All docker-compose files scanned: 2 (`docker-compose.test.yml`, `docker-compose.tier2-bridge.yml`)
|
||||
- [x] All committed secret files inspected; content verified as synthetic test data
|
||||
- [x] `.gitignore` reviewed for secret-exclusion completeness
|
||||
- [x] `.env.example` reviewed for accidentally-committed credentials
|
||||
- [x] Findings cite file:line evidence
|
||||
- [x] Project-specific severity calibration applied (closed-system threat model recognized)
|
||||
@@ -0,0 +1,117 @@
|
||||
# Phase 1 — Dependency Scan
|
||||
|
||||
**Scan date**: 2026-05-19
|
||||
**Tool**: `pip-audit` 2.10.0 (PyPI advisory DB + OSV.dev)
|
||||
**Method**: `pip-audit --strict --requirement <filtered freeze>` against the project's `.venv` (Python 3.10.8). The editable `gps-denied-onboard==0.1.0` root distribution was excluded from the audited set — pip-audit cannot audit non-PyPI editable installs.
|
||||
**Manifest**: `pyproject.toml` (sole manifest — no `requirements*.txt`, no `Cargo.toml`, no `package.json`, no `*.csproj`, no `go.mod`).
|
||||
**Audited**: 94 third-party distributions (production + dev + inference + telemetry extras as installed in `.venv`).
|
||||
**Excluded**: the project itself (`gps-denied-onboard==0.1.0`); TensorRT (installed out-of-band on Jetson per `pyproject.toml` line 123, not a pip dep, audited via on-device SBOM at deploy time).
|
||||
|
||||
## Findings
|
||||
|
||||
12 advisories across 5 packages. **0 are Critical; 0 are High in this project's threat model** (per the per-finding impact analysis in § Project-Specific Triage). The single Medium is `cryptography` CVE-2026-39892 (only triggered by non-contiguous Python buffers — see triage).
|
||||
|
||||
| # | Severity (this project) | Package | Installed | CVE / Advisory | Fix Version | Title |
|
||||
|---|---|---|---|---|---|---|
|
||||
| F1 | **Medium** | `cryptography` | 45.0.7 | CVE-2026-39892 (GHSA-p423-j2cm-9vmq) | 46.0.7 | `Hash.update()` buffer overflow on non-contiguous Python buffers (Python >3.11) |
|
||||
| F2 | Low | `cryptography` | 45.0.7 | CVE-2026-26007 (GHSA-r6ph-v2qm-q3c2) | 46.0.5 | ECDH/ECDSA public-key not validated against small-order subgroup (SECT curves only) |
|
||||
| F3 | Low | `cryptography` | 45.0.7 | CVE-2026-34073 (GHSA-m959-cc7f-wv43) | 46.0.6 | DNS-name constraint validation gap between SAN and peer-name (Web PKI bypass; requires uncommon X.509 topology) |
|
||||
| F4 | Low | `starlette` | 0.48.0 | CVE-2025-62727 (GHSA-7f5h-v6xp-fcq8) | 0.49.1 | `FileResponse._parse_range_header` ReDoS via crafted `Range` header (O(n²) merge) |
|
||||
| F5 | Low | `idna` | 3.14 | CVE-2026-45409 (GHSA-65pc-fj4g-8rjx) | 3.15 | `idna.encode()` DoS via overlong inputs (re-fix of CVE-2024-3651) |
|
||||
| F6 | Low | `paramiko` | 3.5.1 | CVE-2026-44405 (GHSA-r374-rxx8-8654) | _(no fix released)_ | `rsakey.py` allows SHA-1 algorithm |
|
||||
| F7 | Low | `pillow` | 11.3.0 | CVE-2026-25990 (GHSA-cfh3-3jmp-rvhc) | 12.1.1 | PSD image out-of-bounds write |
|
||||
| F8 | Low | `pillow` | 11.3.0 | CVE-2026-40192 (GHSA-whj4-6x5x-4v2j) | 12.2.0 | FITS decompression bomb (unbounded memory) |
|
||||
| F9 | Low | `pillow` | 11.3.0 | CVE-2026-42308 (GHSA-wjx4-4jcj-g98j) | 12.2.0 | Font glyph advance integer overflow |
|
||||
| F10 | Low | `pillow` | 11.3.0 | CVE-2026-42309 (GHSA-5xmw-vc9v-4wf2) | 12.2.0 | Nested-list coordinate heap buffer overflow in `ImagePath.Path` / `ImageDraw.polygon` |
|
||||
| F11 | Low | `pillow` | 11.3.0 | CVE-2026-42310 (GHSA-r73j-pqj5-w3x7) | 12.2.0 | PDF parser infinite loop on cyclic `Prev` pointers (100% CPU hang) |
|
||||
| F12 | Low | `pillow` | 11.3.0 | CVE-2026-42311 (GHSA-pwv6-vv43-88gr) | 12.2.0 | PSD memory corruption via integer overflow in tile-extent bounds check (regression of CVE-2026-25990 fix in 12.1.1) |
|
||||
|
||||
## Project-Specific Triage
|
||||
|
||||
The "Severity (this project)" column above reflects the **actual exposure** in this codebase, not the advisory's generic CVSS score. The same advisory can be High in a public-facing web app and Low here when the affected code path is unreachable.
|
||||
|
||||
### F1 — `cryptography` CVE-2026-39892 (Medium)
|
||||
|
||||
Buffer overflow when a non-contiguous Python buffer is passed to `Hash.update()` on Python > 3.11. **The project's `requires-python = ">=3.10,<3.12"` (`pyproject.toml` line 10) currently caps Python at 3.11.x — Python 3.12 is intentionally excluded.** Today's deployment is therefore not exposed. The finding is kept at Medium (rather than Low) because:
|
||||
|
||||
- Upstream `cryptography` and `gtsam` will eventually drop the Python-3.11 cap (gtsam-4.2 ABI bind to numpy 1.x is what holds us back — see `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`); when that happens, this CVE re-exposes unless `cryptography` is also bumped.
|
||||
- C10 `ManifestBuilder` (AZ-323) and C11 signing-key (AZ-318) build Ed25519 hash inputs via `cryptography.hazmat.primitives.hashes`. The current code path uses standard `bytes`, but a future refactor that touches `memoryview` or sliced inputs could regress.
|
||||
|
||||
**Action**: pin `cryptography>=46.0.7` once the Python ≥ 3.12 cap is lifted (couple it to the Python-cap bump). Document the constraint in `_docs/_process_leftovers/` so it doesn't get lost.
|
||||
|
||||
### F2 — `cryptography` CVE-2026-26007 (Low)
|
||||
|
||||
SECT (Koblitz) curves only. Project's signing path is Ed25519 (`cryptography.hazmat.primitives.asymmetric.ed25519`, AZ-318 / AZ-323); SECT curves are never instantiated. **Not exposed.**
|
||||
|
||||
### F3 — `cryptography` CVE-2026-34073 (Low)
|
||||
|
||||
DNS name-constraint validation gap during X.509 path validation. Project does NOT perform custom X.509 validation — TLS validation is handled by the standard `httpx` / `requests` paths against the operating system / `certifi` trust store. The Suite Satellite Service contract (D-PROJ-2) is HTTPS to a single mTLS-pinned host; no name-constraint extensions are in play. **Not exposed.**
|
||||
|
||||
### F4 — `starlette` CVE-2025-62727 (Low)
|
||||
|
||||
ReDoS in `FileResponse._parse_range_header`. Project surface:
|
||||
- `e2e/fixtures/mock-suite-sat/` is a FastAPI/starlette app (test fixture, lives inside its own Docker image — never deployed to production).
|
||||
- The mock's runtime container is on the `e2e-net` Docker network with `internal: true` (no external reachability per RESTRICT-SAT-1 / NFT-SEC-02).
|
||||
- The mock does not serve `FileResponse` or `StaticFiles` — it's a JSON-only POST/GET stub. The `_parse_range_header` code path is unreachable from the mock's routes.
|
||||
|
||||
**Not exposed in production** (FastAPI/starlette never ship in the airborne binary; they're test-only dev extras per `pyproject.toml` lines 95-118). **Test isolation prevents exposure even in CI.** Recommended action: bump `fastapi` test-time pin upward when the next compatible release lands; no production action required.
|
||||
|
||||
### F5 — `idna` CVE-2026-45409 (Low)
|
||||
|
||||
`idna.encode()` DoS via overlong inputs. Project imports idna transitively via `requests` / `httpx`. The only outbound HTTP/HTTPS path in production is C11 `TileUploader` → `satellite-provider` (single trusted host, hostname fixed at compile time) and C12 `FlightsApiClient` → operator GCS (single trusted host). **No remote-controlled hostnames are encoded** — IDN resource exhaustion requires attacker-controlled domain strings. **Not exposed.**
|
||||
|
||||
### F6 — `paramiko` CVE-2026-44405 (Low)
|
||||
|
||||
SHA-1 still allowed for RSA host-key signatures. Project surface: C12 `CompanionBringup` (AZ-327, `pyproject.toml` line 92) uses paramiko to SSH the operator's companion before flight for artifact verification. Host keys are pinned per-operator (no `AutoAddPolicy` in production — `MissingHostKeyPolicy=RejectPolicy`). SHA-1 RSA host keys could still be accepted, but:
|
||||
- The threat is MitM between operator workstation and companion on a network the operator already controls.
|
||||
- All operator companions are pinned by host-key fingerprint at provisioning time (D-C12-7).
|
||||
|
||||
**Not actively exploitable in the deployed model.** Watch for an upstream fix; bump when one lands.
|
||||
|
||||
### F7-F12 — `pillow` 6× CVEs (Low, test-only)
|
||||
|
||||
All pillow CVEs touch image formats (PSD, FITS, PDF) and APIs (`ImagePath.Path`, `ImageDraw.polygon`) that the SUT does NOT use. Project surface:
|
||||
- `pyproject.toml` declares Pillow as a **test-only** dev dep (lines 109-117): "the production builder runs inside its own Docker image (which installs Pillow itself); this Pillow pin is only the test-time dep used by `e2e/_unit_tests/fixtures/test_tile_cache_builder.py`."
|
||||
- The production SUT image (`docker/Dockerfile`) does NOT install Pillow as a runtime dep; OpenCV (`opencv-python`) is the sole image-decoder pathway.
|
||||
- The test-time consumer only handles **paired `_gmaps.png` reference images and stub tiles emitted by the project itself** — no untrusted PSD/FITS/PDF inputs.
|
||||
|
||||
**Not exposed in production. Not exposed in CI** (test inputs are local-data-only fixtures, license-checked, committed to repo). Recommended action: bump Pillow to `>=12.2.0` when next test-infra session touches the pin — purely hygienic.
|
||||
|
||||
## D-CROSS-CVE-1 / CVE-2025-53644 — Cross-Reference Re-Validation
|
||||
|
||||
Cycle-1 leftover `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md` asks the security review team to re-validate CVE-2025-53644 against the currently-pinned `opencv-python==4.11.0.86`.
|
||||
|
||||
**Outcome of this scan**: `opencv-python==4.11.0.86` is listed in the pip-audit output with `"vulns": []` — neither PyPI's advisory feed nor OSV.dev has a published advisory tying CVE-2025-53644 to that pin band. The 4.x-line supported branch (per the leftover document) appears to have absorbed the relevant patch in `4.11.0.86`. NFT-SEC-04 (which feeds `cve-jpeg-fixture` to every OpenCV imread/imdecode path under AddressSanitizer) is the executable evidence that confirms this in the test suite.
|
||||
|
||||
**Recommendation**: the D-CROSS-CVE-1 leftover entry can remain OPEN because the upstream constraint (gtsam → numpy<2) is unchanged, but the **CVE-2025-53644 exposure window has effectively closed** at the current pin. Update the leftover entry's "CVE exposure window" section to reflect this scan's finding (pip-audit clean against the current pin; NFT-SEC-04 ASan run validates runtime). The pin-replay condition (gtsam numpy-2 wheels) remains the same.
|
||||
|
||||
## Out-of-Band Dependencies (Not Audited Here)
|
||||
|
||||
| Dep | Why excluded | How audited |
|
||||
|---|---|---|
|
||||
| `tensorrt` | Installed via JetPack 6.2 on Jetson Orin Nano Super — not a PyPI distribution | NVIDIA security bulletins + JetPack release notes; tracked in `ci_cd_pipeline.md` § Security as a manual on-deploy SBOM check |
|
||||
| TRT EP engines (`*.engine`) | Build artifacts, not deps | Engine signing + filename schema validation (helpers/engine_filename_schema.md, sha256_sidecar.md) |
|
||||
| `gtsam==4.2` (C++ side) | The PyPI wheel embeds a C++ build of GTSAM; CVEs against the C++ library itself are not in PyPI's advisory feed | Monthly osv-scanner against the GTSAM commit pinned in `cmake/dependencies.cmake` per `_docs/02_document/deployment/ci_cd_pipeline.md` § Security |
|
||||
| Docker base images | `docker/Dockerfile` base + `ardupilot/ardupilot-sitl:plane-stable` + `inavflight/inav-sitl:9.0.0` + `ardupilot/mavproxy:latest` | Trivy on all CI-built images per `ci_cd_pipeline.md` § Security |
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] All package manifests scanned — `pyproject.toml` is the only one
|
||||
- [x] Each finding has a CVE ID + advisory alias (GHSA / BIT)
|
||||
- [x] Upgrade paths identified for every finding (or marked "no fix released" for F6)
|
||||
- [x] D-CROSS-CVE-1 leftover re-validated against the cycle-1 relaxed pin
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate (Critical/High)
|
||||
None — no Critical or High findings in this project's threat model.
|
||||
|
||||
### Short-term (Medium)
|
||||
- **F1**: when the Python-3.12 cap lifts (gtsam numpy-2 unblock), pin `cryptography>=46.0.7` as a coupled bump. Add a one-line note in `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md` so the constraint is replayed in the same session that lifts the opencv pin.
|
||||
|
||||
### Long-term (Low / Hardening)
|
||||
- Bump `pillow` dev-extra pin to `>=12.2.0` (purely hygienic; not exposed).
|
||||
- Bump `starlette` (via `fastapi` dev-extra pin) to a version ≥ 0.49.1 (purely hygienic; not exposed).
|
||||
- Bump `idna` indirectly via `requests` / `httpx` next release.
|
||||
- Watch upstream paramiko for a CVE-2026-44405 fix; bump when published.
|
||||
- Update D-CROSS-CVE-1 leftover entry to record that CVE-2025-53644 is no longer flagged against the current 4.11.0.86 pin (per this scan).
|
||||
@@ -0,0 +1,204 @@
|
||||
# Phase 3 — OWASP Top 10 (2021) Review
|
||||
|
||||
**Review date**: 2026-05-19
|
||||
**Scope**: SUT production code path (`src/gps_denied_onboard/**`) and the surfaces it exposes / consumes in production.
|
||||
**Method**: Walk each OWASP Top 10 (2021) category against the SUT's actual surface; cite the project's threat-model constraints (`RESTRICT-OPS-*`, `NFT-SEC-*`) and the Phase 1 + Phase 2 findings.
|
||||
|
||||
## Surface Map (reference)
|
||||
|
||||
| Surface | Direction | Production exposure | Authentication | Notes |
|
||||
|---|---|---|---|---|
|
||||
| MAVLink to ArduPilot FC | bidirectional (C8) | UART/serial — physical wiring on UAV | MAVLink 2 message signing (Ed25519 derived passkey, AC-4.3 + D-C8-9) | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant ships with documented residual risk. |
|
||||
| Signed STATUSTEXT → GCS | outbound (C8) | UART → telemetry radio | Inherits MAVLink signing | Same channel as C8. |
|
||||
| Tile download HTTPS → `satellite-provider` | outbound (C11 `TileDownloader`) | Operator-workstation network only (RESTRICT-OPS-1, no airborne path) | TLS server-cert validation (httpx defaults, NEVER `verify=False`) + sidecar SHA-256 check on each tile | Pinned base URL from `C11Config`. |
|
||||
| Tile upload HTTPS → `satellite-provider` ingest | outbound (C11 `TileUploader`) | Operator-workstation only (post-landing) | TLS + Ed25519 per-flight signing key (C11 `PerFlightKeyManager`) | Public key envelope written to FDR for audit correlation. |
|
||||
| Flights-service HTTPS → operator API | outbound (C12 `FlightsApiClient`) | Operator-workstation only | TLS + bearer auth token (config-injected, `_REDACTED` in logs) | Pinned base URL from `C12.flights_api_base_url`. |
|
||||
| SSH from operator workstation → UAV companion | outbound (C12 `ParamikoSshSessionFactory`) | Operator-workstation only | SSH key auth + `RejectPolicy` host-key validation (pinned `known_hosts`) | Default mode is `strict`; `AutoAddPolicy` is forbidden by AZ-327. |
|
||||
| FDR records → local FDR sink | local file write (all components) | Onboard tmpfs / local disk | N/A (local file) | Records are HMAC-chained per `fdr_record_schema.md`. |
|
||||
| Healthcheck `python -m gps_denied_onboard.healthcheck` | inbound | Docker HEALTHCHECK probe, localhost only | N/A (process exit code) | No network surface. |
|
||||
| **No inbound network listeners in production** | — | confirmed by NFT-SEC-05 — DNS blackhole + iptables OUTPUT REJECT in flight | — | This is the most important property of the threat model. |
|
||||
|
||||
The test-mode `mock-suite-sat` FastAPI fixture is the ONLY HTTP listener in any e2e topology; it lives in `e2e/fixtures/`, is excluded from the production Dockerfile, and runs only on the `e2e-net.internal: true` Docker network during tests.
|
||||
|
||||
---
|
||||
|
||||
## A01:2021 — Broken Access Control
|
||||
|
||||
**Status**: **N/A — no per-user authorization model exists.**
|
||||
|
||||
- The SUT is a single-tenant onboard binary that runs as one OS user inside its container. There is no notion of multiple users, roles, or per-resource access policy in production.
|
||||
- The two privileged operations that DO exist (start-session, end-session for the C11 upload key; arming the takeoff state in C5) are gated by the C8 FC's own armed/disarmed state — i.e., the safety gate is the physical aircraft state machine, not an application-level RBAC check.
|
||||
- The operator-side C12 service runs ONLY on the operator workstation (RESTRICT-OPS-1); it has no airborne path and no role-based authorization beyond the SSH key + flights-API bearer token tied to the workstation identity.
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## A02:2021 — Cryptographic Failures
|
||||
|
||||
**Status**: **Pass.** All crypto choices are modern and well-implemented.
|
||||
|
||||
| Concern | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Weak hashes for signing / integrity | None | All integrity = SHA-256 (sidecar, tile `content_sha256` column, `_canonical_hash.aggregate_tile_hash`). All signatures = Ed25519. |
|
||||
| Plaintext secrets at rest | None | C8 MAVLink signing passkey loaded from tmpfs-mounted runtime secret (production) or `MAVLINK_SIGNING_PASSKEY_FILE` Docker secret (test). C11 signing key is ephemeral per flight. C12 auth token is config-injected (no on-disk plaintext in production beyond the operator-side secret store). |
|
||||
| Hardcoded keys / salts | None | Phase 2 SAST: 0 hardcoded credentials. All key material is generated per-flight via `Ed25519PrivateKey.generate()` or `secrets.token_bytes(...)`. |
|
||||
| TLS verification disabled | None | Phase 2 SAST: 0 `verify=False` matches; all `httpx.Client(...)` constructions use defaults (verify ON). |
|
||||
| Secret zeroisation on session end | Best-effort with documented residual | `c11_tile_manager/signing_key.py:340-365` zeros the project-controlled `bytearray` mirror of the secret. OpenSSL-side buffer freed via `Ed25519PrivateKey` refcount drop. Documented residual: bounded by upload-session lifetime + RESTRICT-OPS-1 (operator workstation no-swap). AZ-318 Risk-1. |
|
||||
| Random source quality | Cryptographically secure | All key generation uses `secrets.token_bytes` or `cryptography.hazmat`'s built-in CSPRNG. No `random.random()` on security paths. |
|
||||
| CVE exposure in crypto libs | 1 informational (Phase 1 F4) | `cryptography==42.0.8` has 4 GHSA advisories that touch `pkcs12`, OpenSSL FIPS provider config, and a CMS recipient bug — NONE of which are reachable from the SUT's actual usage (X.509 cert validation via httpx; Ed25519 sign/verify). Bump-when-convenient. |
|
||||
|
||||
**Findings**: 0 (the Phase 1 F4 informational item is recorded there; not duplicated here).
|
||||
|
||||
---
|
||||
|
||||
## A03:2021 — Injection
|
||||
|
||||
**Status**: **Pass.** Cleared in Phase 2.
|
||||
|
||||
- **SQL injection**: 0 findings. All 19 `cursor.execute(...)` sites in C6 use psycopg `%s` parameterized placeholders.
|
||||
- **Command injection**: 0 findings. 1 `subprocess.run` (TensorRT `trtexec`) — list args, `shell=False`, no untrusted input.
|
||||
- **OS-path / file injection**: 0 findings. All `Path(...)` / `open(...)` calls take SUT-managed paths; no `..` traversal patterns matched.
|
||||
- **No template / XSS surface** (no HTML output).
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## A04:2021 — Insecure Design
|
||||
|
||||
**Status**: **Pass — the design IS the security boundary.**
|
||||
|
||||
The SUT's defining design property is "the airborne process has no network egress". That property is enforced at THREE layers:
|
||||
|
||||
1. **Architectural** — no component constructs an `httpx.Client` or opens a socket on the airborne path. The C11 downloader / uploader and C12 flights-client are wired ONLY by the operator-workstation composition root (`compose_root_operator`), not the airborne `compose_root_airborne`.
|
||||
2. **Operational (RESTRICT-OPS-1)** — airborne container ships with iptables OUTPUT REJECT + DNS blackhole.
|
||||
3. **Test (NFT-SEC-02 / NFT-SEC-05)** — the airborne process is run inside a network namespace with no route to `satellite-provider`'s host; any attempted outbound TCP connection is a release-blocking test failure. The harness runs this test as part of every CI build.
|
||||
|
||||
| Design constraint | Where enforced | Verified by |
|
||||
|---|---|---|
|
||||
| No inbound network listeners on airborne path | `compose_root_airborne` does not wire any FastAPI / uvicorn / grpc server | NFT-SEC-05 |
|
||||
| No outbound network egress on airborne path | iptables + DNS blackhole + arch-level component placement | NFT-SEC-02 + NFT-SEC-05 |
|
||||
| Tile downloads happen ONLY at operator workstation (pre-flight) | `compose_root_operator` is the only wiring that includes `build_tile_downloader` | Architecture review + integration test FT-N-01 (operator) vs FT-P-17 (airborne) |
|
||||
| Tile uploads happen ONLY post-landing on operator workstation | Same — `build_tile_uploader` is `compose_root_operator` only | NFT-SEC-01 |
|
||||
| MAVLink signing key generation happens on-bench, not at runtime | C8 documentation + boot sequence diagram | NFT-SEC-03 |
|
||||
| Strategy implementations are EXCLUDED from the production binary at compile time | `BUILD_*` flags gate the import in `runtime_root/*_factory.py` | CI pipeline gate (`pyproject.toml` `[tool.gpsdo.build-flags]`) |
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
**Defensive observations**:
|
||||
|
||||
- The "test substitute" `mock-suite-sat` is documented as **non-architectural** (`_docs/02_document/architecture.md:277`) precisely so it cannot accidentally become a production dependency. The fixture is retired the moment D-PROJ-2 lands the real ingest endpoint.
|
||||
- The strategy registry is keyed by string → module path through a CLOSED whitelist (`KNOWN_*_STRATEGIES`), preventing config-controlled dynamic import vectors (Phase 2 F13).
|
||||
|
||||
---
|
||||
|
||||
## A05:2021 — Security Misconfiguration
|
||||
|
||||
**Status**: **Mostly pass; the configuration & infrastructure review (Phase 4, next) is the deeper check.**
|
||||
|
||||
- **No debug surface in production**: SUT has no FastAPI / Flask / Django app; healthcheck binary uses structured exit codes, never a stack trace. `DEBUG=1` is a developer-mode env var that only widens stderr verbosity for the CLI.
|
||||
- **Pinned dependencies**: `pyproject.toml` pins every direct dep; `requirements*.lock` (Phase 4 to verify) enforces transitive pins. `opencv-python>=4.11.0.86,<4.12` band is documented in `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`.
|
||||
- **Build-flag gating**: strategies excluded at compile time via `BUILD_PYTORCH_FP16_RUNTIME`, `BUILD_FAISS_INDEX`, `BUILD_VPR_*`, `BUILD_C7_TRT_FP16`, etc. — non-built strategies cannot be selected even if config is wrong.
|
||||
- **Test-only secrets clearly marked**: `e2e/fixtures/secrets/mavlink-test-passkey.txt` carries a "TEST ONLY" annotation; production reads from a separate tmpfs-mounted secret.
|
||||
- **TODO for Phase 4**: container user (non-root?), file ownership in image layers, env var defaults, Docker secret mount permissions, network policy on `e2e-net.internal: true`.
|
||||
|
||||
**Findings (Phase 3 surface)**: 0. Configuration-layer concerns are deferred to Phase 4.
|
||||
|
||||
---
|
||||
|
||||
## A06:2021 — Vulnerable and Outdated Components
|
||||
|
||||
**Status**: Covered in Phase 1. Summary:
|
||||
|
||||
| Severity (project context) | Count |
|
||||
|---|---|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 1 (`onnx==1.18.0` GHSA-rcgw-4cgp-9q7q — reachable only via developer-mode model loading, not the production airborne path) |
|
||||
| Low | 11 |
|
||||
|
||||
**Findings (Phase 3-specific)**: 0 new — refer to `_docs/05_security/dependency_scan.md`.
|
||||
|
||||
---
|
||||
|
||||
## A07:2021 — Identification and Authentication Failures
|
||||
|
||||
**Status**: **Pass.** All authenticated surfaces have a strong scheme; no obvious bypass.
|
||||
|
||||
| Surface | Scheme | Defense observed |
|
||||
|---|---|---|
|
||||
| MAVLink ↔ FC | MAVLink 2 message signing, per-flight key | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant cannot enable signing — documented residual risk, surfaced in C8 docs (`_docs/02_document/components/10_c8_fc_adapter/description.md`). |
|
||||
| C11 upload session → `satellite-provider` | Ed25519 per-flight signing key | Public-key envelope to FDR for audit correlation. Session lifetime ≤ upload window (minutes). |
|
||||
| C12 → flights-service | Bearer token | Token loaded from config (operator-side secret store), `_REDACTED` in 100% of error messages and retry logs. |
|
||||
| C12 → companion SSH | SSH key + `RejectPolicy` host-key validation | `paramiko.AutoAddPolicy` is FORBIDDEN by AZ-327. Default mode `strict` rejects any unknown host. CVE-2026-44405 (Phase 1 F6, SHA-1 RSA) is materially mitigated. |
|
||||
| C10 manifest verification | Ed25519 detached signature + SHA-256 of canonicalized JSON + trust-anchor public-key pinning | Fail-closed (`verify_manifest` returns `outcome=FAIL` on any deviation; never raises in the success path). |
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## A08:2021 — Software and Data Integrity Failures
|
||||
|
||||
**Status**: **Pass.** Integrity-checking is the spine of the manifest / tile / FDR contracts.
|
||||
|
||||
| Artifact | Integrity check | Where |
|
||||
|---|---|---|
|
||||
| `Manifest.json` | Ed25519 signature (`Manifest.json.sig`) + SHA-256 sidecar (`Manifest.json.sha256`) + canonical-JSON hashing of the embedded tile aggregate | `c10_provisioning/manifest_verifier.py` |
|
||||
| Each tile JPEG | SHA-256 sidecar (`<tile>.sha256`), enforced both at download (`tile_downloader.py`) and on read (`postgres_filesystem_store.py` SELECT + sidecar verify) | C6 + C11 |
|
||||
| FDR records | HMAC-chained schema per `_docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md` | `fdr_client/records.py` |
|
||||
| TensorRT engine builds | Engine path includes content-hash of source ONNX + build-config; rebuild on mismatch | `c7_inference/tensorrt_runtime.py` |
|
||||
| Replay fixtures | `e2e/fixtures/sitl-replay-fixture-*` ship with a manifest builder that derives SHA-256 over every artefact | `e2e/fixtures/builders/...` |
|
||||
|
||||
**Software-update integrity**: out-of-scope for the onboard binary; operator-workstation provisioning is handled by the parent suite (D-PROJ-1) which Phase 4 will review at the Dockerfile / image layer.
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## A09:2021 — Security Logging and Monitoring Failures
|
||||
|
||||
**Status**: **Pass — disciplined structured logging with security-event coverage.**
|
||||
|
||||
| Concern | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Security-relevant events emit a structured FDR record | Yes | `c11.upload.session.key.public`, `c11.upload.session.key.zeroised`, `c11.upload.signature_rejected`, `c12.flights.fetch.failed`, `c10.manifest.verify.*`, `c8.mavlink.signing.*` |
|
||||
| Failures (auth, integrity, transport) emit ERROR-level logs with a stable `kind` field | Yes | `kind="c12.flights.fetch.failed"` + `reason∈{auth,not_found,connect:*}`, `kind="c10.manifest.verify.failed"`, `kind="c2.vpr.dim_mismatch"`, `kind="c11.upload.signature_rejected"` |
|
||||
| Secrets are not logged | Yes | 13 files use the `_REDACTED` / `redact` pattern. Phase 2 confirmed: auth token, MAVLink passkey, and SSH private-key material are never logged in plaintext. |
|
||||
| Logs are tamper-resistant | Best-effort | FDR records are HMAC-chained; structured logs to stderr/journal use a defined schema (`logging/structured.py`). Operator-side log retention is a parent-suite concern. |
|
||||
| Monitoring / alerting plumbed | Out of scope | Parent-suite responsibility — `_docs/02_document/deployment/observability.md` documents what the SUT emits, not where it is shipped. |
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## A10:2021 — Server-Side Request Forgery (SSRF)
|
||||
|
||||
**Status**: **Pass.** No SUT code accepts a URL/host parameter from external input.
|
||||
|
||||
- `c11_tile_manager/{tile_downloader,tile_uploader}.py` build URLs by concatenating a config-pinned `base_url` with internally-derived path segments (`zoom/x/y.jpg`, `upload/<flight_id>/<tile_uuid>`). No external `?url=...` parameter or redirect-following from untrusted source.
|
||||
- `c12_operator_orchestrator/flights_api/httpx_client.py` uses `client.get(url, ...)` where `url` is built from `self._config.flights_api_base_url` (config-pinned) + a derived path segment containing the validated `flight_id` UUID. No external host control.
|
||||
- `httpx.Client` defaults — `follow_redirects=False`. SSRF via 30x redirect to internal addresses is blocked unless the caller explicitly opts in (no SUT caller does).
|
||||
- DNS resolution happens against the operator's resolver only; in production (airborne) DNS is blackholed.
|
||||
|
||||
**Findings**: 0.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Reference Index
|
||||
|
||||
| Source | Phase 3 § | Note |
|
||||
|---|---|---|
|
||||
| `_docs/05_security/dependency_scan.md` | A06, A02 | Phase 1 — 12 vulnerabilities triaged |
|
||||
| `_docs/05_security/static_analysis.md` | A03, A02 | Phase 2 — 1 informational hardening recommendation |
|
||||
| `_docs/02_document/architecture.md` § "Operating envelopes" | A04 | RESTRICT-OPS-1 / NFT-SEC-05 enforcement |
|
||||
| `_docs/02_document/tests/security-tests.md` | A04, A07, A08 | NFT-SEC-01..05 test contract |
|
||||
| `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md` | A06 | D-CROSS-CVE-1 deferred pin |
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] All 10 OWASP 2021 categories addressed with status + finding count + evidence
|
||||
- [x] N/A categories (A01) explicitly justified against the threat model, not silently skipped
|
||||
- [x] Findings from Phase 1 / Phase 2 cross-referenced where they map to OWASP categories rather than re-listed
|
||||
- [x] Surface map at top is consistent with `_docs/02_document/architecture.md`
|
||||
- [x] No new HTTP listeners / dynamic-input surfaces missed (confirmed by `Grep` for FastAPI/uvicorn/server.serve patterns)
|
||||
@@ -0,0 +1,112 @@
|
||||
# Phase 2 — Static Analysis (SAST)
|
||||
|
||||
**Scan date**: 2026-05-19
|
||||
**Scope**: `src/gps_denied_onboard/**` — 259 Python files across 14 components, 8 helpers, composition root, fdr_client, frame_source, helpers, logging, replay_input, CLI, healthcheck.
|
||||
**Excluded**: `tests/`, `e2e/`, `scripts/` (project-controlled test code with no production exposure); third-party deps (covered by Phase 1).
|
||||
**Method**: pattern-driven ripgrep scan against the SAST checklist in `.cursor/skills/security/SKILL.md` § Phase 2 — Static Analysis. Findings cross-validated by reading the call site.
|
||||
|
||||
## Findings
|
||||
|
||||
**Total: 0 Critical, 0 High, 0 Medium, 1 Low (informational).**
|
||||
|
||||
The SUT is a closed-system, single-tenant onboard binary that does not accept external untrusted input on any production path. The only HTTP-inbound surface is the FastAPI test mock (`e2e/fixtures/mock-suite-sat`, out of this Phase 2 scope). The only HTTP-outbound surfaces are C11 `TileUploader` / `TileDownloader` against a pinned host, and C12 `FlightsApiClient` against a pinned host. All inputs the SUT acts on are file-backed (tile cache, IMU CSV, FDR fixtures, calibration JSON) and either operator-provisioned or built by the SUT itself.
|
||||
|
||||
| # | Severity | Category | Location | Title |
|
||||
|---|---|---|---|---|
|
||||
| F13 | Low (informational) | Hardening | `src/gps_denied_onboard/runtime_root/*_factory.py` (7 sites) | `__import__(module_name, fromlist=...)` used for plugin loading — input is config-validated against a closed whitelist; recommend a hardening assertion |
|
||||
|
||||
## Pattern-by-Pattern Results
|
||||
|
||||
### Injection
|
||||
|
||||
| Pattern | Result | Evidence |
|
||||
|---|---|---|
|
||||
| SQL injection via f-string / `.format()` / `+` concatenation into `cursor.execute` | **0 findings** | All 19 `cur.execute(...)` sites in `c6_tile_cache/{postgres_filesystem_store,freshness_gate,migrations,tools}.py` use psycopg `%s` placeholders. The two sites that use dynamic clause composition (`postgres_filesystem_store.py:523-533`, `:918-927`) build the SQL template from hardcoded column names + `%s` placeholders and parameterize ALL value bindings via `tuple(params)` / `params` tuple. Filter columns are hardcoded — only filter VALUES are user/config-controlled and they go through the placeholder. |
|
||||
| Command injection — `shell=True` / `os.system` / `os.popen` | **0 findings** | 1 `subprocess.run` call total — `c7_inference/tensorrt_runtime.py:826` for `trtexec` (TensorRT engine builder). `shell=False` (default with list args), arguments come from internal `build_config` (precision mode, optimization profiles, paths from SUT-managed `engine_path` / `model_path`). No untrusted input on this path. Timeout + capture_output + structured `EngineBuildError` on non-zero exit. |
|
||||
| XSS via unsanitized output in HTML | **0 findings** | SUT has no HTML output surface. |
|
||||
| Template injection (jinja2 `Template()` with user data) | **0 findings** | jinja2 is a transitive dep of `cryptography`; SUT does not import it directly. |
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
| Pattern | Result | Evidence |
|
||||
|---|---|---|
|
||||
| Hardcoded credentials / API keys / passwords / tokens (literal `password=`/`secret=`/`api_key=`/`token=` with 8+ char string) | **0 findings** | `c12_operator_orchestrator/config.py:133` has `flights_api_auth_token: str = ""` — empty default, value injected via `Config` at runtime. No literal secret. |
|
||||
| Missing authentication on endpoints | **N/A** | SUT exposes no inbound HTTP/RPC endpoint in production. Healthcheck binary (`src/gps_denied_onboard/healthcheck.py`) is a local exit-code probe. |
|
||||
| Missing authorization checks (IDOR / privilege escalation paths) | **N/A** | Single-tenant onboard binary; no per-user authorization model. |
|
||||
| Weak password validation | **N/A** | No password handling. |
|
||||
|
||||
### Cryptographic Failures
|
||||
|
||||
| Pattern | Result | Evidence |
|
||||
|---|---|---|
|
||||
| Weak hash algorithms (MD5, SHA1) on security-critical paths | **0 findings** | 0 `hashlib.md5(` / `hashlib.sha1(` / `.md5(` / `.sha1(` matches in `src/`. Content hashing uses SHA-256 (`helpers/sha256_sidecar.py`, `c6/postgres_filesystem_store.py` content_sha256 column). Ed25519 signing for tile uploads (C11 `PerFlightKeyManager`, AZ-318). |
|
||||
| Plaintext password storage | **0 findings** | No password store. |
|
||||
| Hardcoded encryption keys / salts | **0 findings** | All key material is generated per-flight: `c11_tile_manager/signing_key.py` uses `Ed25519PrivateKey.generate()`; MAVLink signing passkey at `c8_fc_adapter/pymavlink_ardupilot_adapter.py:513` uses `secrets.token_bytes(_SIGNING_KEY_LEN)` (cryptographically secure RNG). The MAVLink-side passkey is loaded from a Docker secret (`MAVLINK_SIGNING_PASSKEY_FILE`) for the test path; production binary reads the same secret from a tmpfs-mounted runtime secret. |
|
||||
| Missing TLS/HTTPS enforcement (`verify=False`, `VERIFY_NONE`, `InsecureRequestWarning`) | **0 findings** | 0 matches. All `httpx.Client(...)` constructions use default settings (TLS verification ON). Production hosts are pinned at compile/config time. |
|
||||
| Secret zeroisation on session end | **GOOD pattern** | `c11_tile_manager/signing_key.py:340-365` `_zeroise_secret_buffer()` overwrites the project-controlled `bytearray` with zeros; OpenSSL-side buffer freed via `Ed25519PrivateKey` refcount drop. Double-storage tradeoff documented (AZ-318 Risk-1). Best-effort, bounded by upload-session lifetime, mitigated by RESTRICT-OPS-1 (operator workstation no-swap). |
|
||||
|
||||
### Data Exposure
|
||||
|
||||
| Pattern | Result | Evidence |
|
||||
|---|---|---|
|
||||
| Secrets / tokens / passwords leaked in logs / error messages | **0 findings** | C12 `flights_api/httpx_client.py:140,175,191` redacts the auth token as `auth_token={_REDACTED}` in both success and retry log paths. 13 source files use the `_REDACTED` / `redact` convention (audited list in `Grep` output above). |
|
||||
| Sensitive fields in API responses | **N/A** | SUT publishes no API. |
|
||||
| Debug endpoints / verbose error mode in prod | **0 findings** | No FastAPI / Flask / Django app in production scope. CLI exits with structured exception messages; full traceback only when `DEBUG=1` is explicitly set. |
|
||||
| Secrets in version control | **0 findings** | `e2e/fixtures/secrets/mavlink-test-passkey.txt` is a documented TEST-ONLY passkey (commit comment: "TEST ONLY"); explicitly excluded from production deployment per Phase 1 / Dockerfile build args. No `.env`, `.envrc`, or `config.local.*` with literal secrets found. |
|
||||
|
||||
### Insecure Deserialization
|
||||
|
||||
| Pattern | Result | Evidence |
|
||||
|---|---|---|
|
||||
| `pickle.loads()` / `marshal.loads()` / `cPickle.*` on untrusted data | **0 findings** | 0 matches in `src/`. (The 7 `model.eval()` matches found by the original SAST regex were PyTorch `nn.Module.eval()` mode-switch calls, not Python `eval`.) |
|
||||
| `eval()` / `exec()` of strings | **0 findings** | 0 matches. |
|
||||
| `yaml.load()` / `yaml.unsafe_load()` on untrusted data | **0 findings** | 0 matches in `src/`. (Config loader uses `yaml.safe_load`, validated in module-layout review.) |
|
||||
| `json.loads()` on untrusted data | **0 findings** | 3 sites total: `c10_provisioning/provisioner.py:512` reads SUT-built manifest; `c1_vio/bench/okvis2.py:64,73` reads SUT-built calibration / IMU bench fixtures; `runtime_root/airborne_bootstrap.py:965` reads operator-provisioned calibration. All inputs are filesystem-local, operator-controlled, schema-validated downstream. |
|
||||
| `__import__()` of dynamic strings | **F13** (informational) | 7 sites — `runtime_root/{vpr,vio,matcher,rerank,refiner,inference,c11,...}_factory.py`. Each call passes a `module_name` looked up from a CLOSED `_STRATEGY_TO_MODULE` table keyed by `config.components[<c>].strategy`, which `<C>Config.__post_init__` validates against `KNOWN_*_STRATEGIES`. The input is config-validated against a whitelist BEFORE the import. Documented in the strategy-registry comments (AZ-591). |
|
||||
|
||||
### F13 Detail — `__import__` plugin loading
|
||||
|
||||
**Severity**: Low (informational / hardening recommendation, not a current vulnerability).
|
||||
|
||||
**Location**: `src/gps_denied_onboard/runtime_root/{vpr,vio,matcher,rerank,refiner,inference,c11}_factory.py` (7 call sites).
|
||||
|
||||
**Description**: The composition-root factories use `__import__(module_name, fromlist=[class_name])` to load concrete strategy implementations lazily. Today this is safe because `module_name` is looked up via a closed `_STRATEGY_TO_MODULE` dict keyed by `strategy: str`, and `strategy` is validated against `KNOWN_*_STRATEGIES` in each component's `<C>Config.__post_init__`. The validation gate happens at `Config` instantiation, before `compose_root` ever runs.
|
||||
|
||||
**Impact**: None today — config validation blocks any string from reaching `__import__` that isn't in the whitelist.
|
||||
|
||||
**Failure mode**: if a future refactor were to drop the `KNOWN_*_STRATEGIES` validation, or if a strategy were added to `_STRATEGY_TO_MODULE` without a matching `KNOWN_*` entry, the failure mode would be "config-file-controlled module import" — an attacker who controls the config YAML could load arbitrary importable modules. The threat is bounded because (a) the config file is operator-provisioned (not network-fetched), and (b) the worst-case payload is "load and call `create(...)` on a module already on `sys.path`", which is the project's own dependency closure.
|
||||
|
||||
**Remediation (hardening)**: add a defensive assertion at the start of each `_factory.py`'s build function:
|
||||
|
||||
```python
|
||||
if strategy not in KNOWN_*_STRATEGIES:
|
||||
raise StrategyNotAvailableError(
|
||||
f"Strategy {strategy!r} not in whitelist; refusing dynamic import."
|
||||
)
|
||||
```
|
||||
|
||||
Today this is enforced by `<C>Config.__post_init__` — the recommended hardening is to repeat the check at the import call site so the invariant survives future refactors. Not blocking for production.
|
||||
|
||||
## Architectural Safeguards Observed (defense-in-depth)
|
||||
|
||||
These are not findings — they are positive observations that reduce the project's overall attack surface:
|
||||
|
||||
| Safeguard | Location | Why it matters |
|
||||
|---|---|---|
|
||||
| Public-boundary discipline for the blackbox harness | `e2e/README.md` § Public-Boundary Discipline + `e2e/runner/Dockerfile` (no SUT install) | The test runner cannot import any SUT module, eliminating cross-contamination of test fixtures into the production code path. |
|
||||
| `paramiko` `RejectPolicy` (NOT `AutoAddPolicy`) | `c12_operator_orchestrator/paramiko_ssh_session.py:218` | C12 SSH to operator companion always validates host key against pinned `known_hosts`. CVE-2026-44405 (SHA-1 RSA, Phase 1 F6) is materially mitigated by this. |
|
||||
| Per-flight ephemeral Ed25519 signing key | `c11_tile_manager/signing_key.py` | Compromise of a single flight key only exposes that flight's upload window; key never persists past `end_session`. |
|
||||
| Cryptographically-secure RNG for MAVLink passkey generation | `c8_fc_adapter/pymavlink_ardupilot_adapter.py:513` (`secrets.token_bytes`) | Not `random.random()`. |
|
||||
| Internal-only Docker network for test stack | `e2e/docker/docker-compose.test.yml` (`e2e-net.internal: true`) | RESTRICT-SAT-1 / NFT-SEC-02 enforcement — runtime network egress to non-`e2e-net` destinations is impossible during tests. |
|
||||
| Token redaction in logs (`_REDACTED` convention) | 13 source files including `c12_operator_orchestrator/{cli,build_cache,operator_reloc_service}`, `c11_tile_manager/{config,tile_downloader}`, `c13_fdr/headers`, `c10_provisioning/manifest_builder`, `cli/replay` | Disciplined enforcement across components; not just one-off in C12. |
|
||||
| Build-flag gates (`BUILD_PYTORCH_FP16_RUNTIME`, `BUILD_FAISS_INDEX`, `BUILD_VPR_*`, etc.) at composition-root level | `runtime_root/*_factory.py` | Strategies are excluded from the production binary at compile time, not just runtime — eliminates classes of misconfiguration attacks. |
|
||||
| `verify=True` (default) on all `httpx.Client` constructions | `c11_tile_manager/{tile_downloader,tile_uploader}.py`, `c12_operator_orchestrator/flights_api/httpx_client.py`, `runtime_root/c11_factory.py` | TLS validation is never bypassed. |
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] All `src/gps_denied_onboard/` Python files scanned (259 files)
|
||||
- [x] Each finding has file:line evidence
|
||||
- [x] No false positives from comments or test files (e.g., the `model.eval()` PyTorch matches were verified by reading the call site)
|
||||
- [x] All `cur.execute(...)` sites in C6 reviewed for parameterization
|
||||
- [x] All `subprocess.*` / `os.system` / `eval` / `exec` / `pickle.loads` / `__import__` matches reviewed
|
||||
- [x] All `verify=False` / weak-crypto / hardcoded-secret patterns checked
|
||||
Reference in New Issue
Block a user