{
 "verdict_summary": "Not a Basecamp module: this is the open-review harness itself. Escaping and the Venice-key separation are sound, but the trust boundary is broken: a hostile module can (a) symlink host files, including the Venice key, into the published review, (b) rewrite the evidence and report that judge it, and (c) exfiltrate freely because the DNS sink is bypassed and raw-IP egress is unrecorded. Shell injection in review.sh, an overridable/under-priced model budget, a 40-minute outer timeout on a 150-minute review, and repo-controlled scanner configs compound the problem. Findings are triage, not approval.",
 "findings": [
  {
   "id": "FR-1",
   "severity": "Critical",
   "title": "Host-file disclosure and evidence forgery via symlink-following in render.py",
   "files": "pipeline/render.py:45-49,92-97,124-126; pipeline/review.sh:40; pipeline/reviewer.sh:11",
   "exploit": "The module runs as root with /run read-write. A symlink such as /run/media/leak.txt -> /home/reviewer/.pi/agent/.env (or /run/report.md -> attacker file) is dereferenced by shutil.copytree/shutil.copy in render.py on the host and published under /srv/reviews. Probe: render.py on a run whose report.md and media/leak.txt were symlinks published TOP-SECRET-VENICE-KEY-abc123; a symlinked logs/ directory published HOSTFILE-SECRET-XYZ. In production this leaks the Venice key read by reviewer.sh:11.",
   "fix": "Resolve realpath and reject anything outside $RUN; use os.walk(followlinks=False), skip symlinks, open with O_NOFOLLOW; copy only an explicit allowlist of regular files after lstat; separate module-writable data from host-published evidence.",
   "confirmed": "run"
  },
  {
   "id": "FR-2",
   "severity": "High",
   "title": "Untrusted module can tamper with all evidence and the report that judges it",
   "files": "pipeline/review.sh:40; pipeline/review-in-sandbox.sh:49; pipeline/analyse.py:10-11; pipeline/render.py:92-97,124-126; pipeline/desk.sh:31",
   "exploit": "The module shares root and the writable /run with its capture. It can truncate strace.log, replace bait.json with {}, delete dns.log, and overwrite report.md/report.json or /run/media. Verified /run/behaviour and /run/src are 0775 root-writable (touch succeeded) and that a symlinked report.md is published; no host-side append-only copy exists to compare.",
   "fix": "Capture strace/DNS/network on the host or in a separate privileged observer into files the sandbox cannot see; make /run/src and published evidence host-owned/read-only to the module; run the module under a different non-root UID; hash/inode-verify evidence before publishing.",
   "confirmed": "run"
  },
  {
   "id": "FR-3",
   "severity": "High",
   "title": "Network recording is ineffective: DNS sink bypassed, egress unrestricted, raw-IP connects ignored",
   "files": "pipeline/dns_sink.py:19; pipeline/review-in-sandbox.sh:20; pipeline/analyse.py:16-27; pipeline/reviewer.sh:26",
   "exploit": "/etc/resolv.conf points at external resolvers, not the 127.0.0.1 sink, so dns.log stays empty. Direct UDP DNS to 1.1.1.1 and 195.179.224.53 and TCP to 8.8.8.8:53, 1.1.1.1:443, 140.82.121.4:443 all succeeded. analyse.py computes raw-IP connects but never reports them, and the leaky control sample's DNS lookup left dns.log at 0 bytes. dirfd-relative bait reads plus a literal-IP connect produced an empty summary.",
   "fix": "Point the resolver at the sink or intercept DNS in a netns; enforce default-deny egress allowing only gateway/mirrors (a logger is not a control); report AF_INET/AF_INET6 connect destinations and per-process UIDs in analyse.py; add a sandbox self-test for a lookup and a literal-IP connect.",
   "confirmed": "run"
  },
  {
   "id": "FR-4",
   "severity": "High",
   "title": "Shell injection from repository, commit and lgx URL into bash -lc",
   "files": "pipeline/review.sh:40,49,58",
   "exploit": "REPO/COMMIT/LGX are placed inside single quotes in a bash -lc string. REPO=\"x'; echo INJECTED > /tmp/probe-ci; echo '\" executed the marker command in the clone container, which has /run read-write and bridge networking, before static checks. The same applies to LGX in the curl line.",
   "fix": "Run git/curl with argument vectors and --, validate URL schemes/hosts, never build a shell string from input; clone in a container with only the intended source directory writable and run checks first.",
   "confirmed": "run"
  },
  {
   "id": "FR-5",
   "severity": "Medium",
   "title": "DIEM budget can be exceeded and is under-priced; gateway tokens readable by the module",
   "files": "pipeline/gateway.py:21-27,77,82-84; pipeline/reviewer.sh:19-27; pipeline/review-in-sandbox.sh:29; pipeline/sandbox-check.sh:19-21",
   "exploit": "Unknown model IDs use the DeepSeek fallback price. A probe POST for claude-3-opus-20240229 with 1M/1M tokens was forwarded and logged cost 1.65, spend 1.65 against budget 0.8; the first-request pre-check reserves only 0.0024 DIEM. The module can read REVIEW_TOKEN from 0644 models.json and REPORT_TOKEN from /proc/1/environ (both confirmed present), then spend budget and write gateway.jsonl/live.txt. The gateway also fails open when REVIEW_TOKEN is empty (gateway.py:74), making a standalone launch an unauthenticated relay.",
   "fix": "Allowlist models and prices; reserve max_tokens*output_price + prompt cost before forwarding; never put tokens in container env or world-readable files (use a Unix socket/per-process credential); cap body size and threads.",
   "confirmed": "run"
  },
  {
   "id": "FR-6",
   "severity": "Medium",
   "title": "A 40-minute outer timeout truncates the advertised 150-minute review and prevents the report pass",
   "files": "pipeline/reviewer.sh:26; pipeline/review-in-sandbox.sh:45-64; pipeline/desk.sh:71-77",
   "exploit": "timeout 2400 podman run is 40 minutes, while inner MAX_MIN default is 150 and the report-pass block lives inside the container. Reviews using the advertised time are killed by the outer timeout; the report pass never runs and review.sh publishes no report.",
   "fix": "Derive the outer timeout from REVIEW_MAX_MIN (plus grace) or remove it; keep a host watchdog that still allows the report pass; test a >40-minute review.",
   "confirmed": "read"
  },
  {
   "id": "FR-7",
   "severity": "Medium",
   "title": "Repository-controlled scanner config can suppress findings; scanner failure is silent",
   "files": "pipeline/review.sh:52-56,62; sandbox/reviewer.Containerfile:45-52",
   "exploit": "gitleaks auto-loads a repository .gitleaks.toml. With a committed AKIA key the exact command found 1 finding; adding [allowlist] paths=['a\\.txt'] made it report no leaks. A malformed config exits fatally and review.sh's || true hides it. .semgrepignore and osv-scanner.toml give analogous suppression.",
   "fix": "Use explicit host-owned scanner configs, disable repo config/ignore discovery, treat missing/empty reports as failures, and hash configs and outputs.",
   "confirmed": "run"
  },
  {
   "id": "FR-8",
   "severity": "Medium",
   "title": "desk install path traversal via manifest name (and lgx symlinks), executed as root",
   "files": "pipeline/desk.sh:48-57,113-121",
   "exploit": "An lgx manifest name of ../../../../../../tmp/lgx-pwn-dir made the real install_one rm -rf and copy into /tmp/lgx-pwn-dir (marker confirmed). tar xzf runs as root and accepts absolute-target symlink members; cp -a follows a symlinked variants/linux-amd64. Inside the sandbox this can delete/overwrite /run, /opt/bc or /usr before the review.",
   "fix": "Reject names not matching ^[A-Za-z0-9._-]+$ and . / ..; realpath-verify dest under $U; use a safe extractor rejecting symlink/hardlink/absolute members; install as non-root; never rm -rf an untrusted path without the prefix check.",
   "confirmed": "run"
  },
  {
   "id": "FR-9",
   "severity": "Medium",
   "title": "No report schema validation: a malformed report.json aborts publication",
   "files": "pipeline/render.py:124-140,225; docs/proposal.md:35",
   "exploit": "report.json with findings as an object raised AttributeError at render.py:135; only chain.txt, logs and report.json were written, no index.html. The module can overwrite report.json (FR-2), so it can suppress the published review page. No schema exists despite the proposal claim.",
   "fix": "Validate against a strict schema with size/type limits; always write index.html with a failed banner on invalid input; never let model output decide whether publication completes.",
   "confirmed": "run"
  },
  {
   "id": "FR-10",
   "severity": "Medium",
   "title": "Self-certifying and overstated security claims contradict the implementation",
   "files": "docs/proposal.md:32,35,40,47,50-57,120; test-modules/README.md:5; test-modules/leaky-build/collect.js:1; NOTES.md",
   "exploit": "Claims of recorded network, schema validation, build-vs-lgx comparison, uninstall, write overlay, process/resource capture, signed logs and pinned inputs are not implemented: no build compare, no overlay, no uninstall, no signature (bare hash chain), unpinned FROM/curl|bash/npm. The test-module README and collect.js comments tell the reviewer a credential-stealing postinstall is harmless/not for other use.",
   "fix": "State only implemented guarantees or mark roadmap; sign the chain with a key outside the host or drop the claim; pin images/downloads by digest; treat repository prose as untrusted and never let it set scope/severity.",
   "confirmed": "read"
  },
  {
   "id": "FR-11",
   "severity": "Low",
   "title": "Caret version matching is wrong for 0.x dependencies",
   "files": "pipeline/fetch_deps.py:18-27,40-49",
   "exploit": "The real satisfies() returns True for 0.9.0 against ^0.1.0; semver requires <0.2.0. A module declaring ^0.1.0 can receive 0.9.0 from the catalogue, outside its intended range.",
   "fix": "Implement correct caret rules (lock minor for 0.x, patch for 0.0.z) or use a maintained semver library with boundary tests.",
   "confirmed": "run"
  },
  {
   "id": "FR-12",
   "severity": "Low",
   "title": "Gateway DoS surface and incomplete secret redaction",
   "files": "pipeline/gateway.py:35-38,77,148; pipeline/reviewer.sh:12",
   "exploit": "A token-holding module can send a huge or unfinished Content-Length and open many connections; the handler reads the body unbounded and the server spawns a thread per connection, exhausting host memory/threads. Redaction misses AWS secret values and leaves private-key bodies (only the PEM header matches) in published gateway.jsonl. Gateway binds 0.0.0.0 and relies on the firewall.",
   "fix": "Cap body size, threads and timeouts; bind loopback/Unix socket; perform field-aware redaction (whole PEM blocks, aws_secret_access_key, seed phrases, bearer tokens) before publishing.",
   "confirmed": "read"
  },
  {
   "id": "FR-13",
   "severity": "Low",
   "title": "Unpinned host/image supply chain for code with access to the Venice key",
   "files": "host/30-agent.sh:13-16; sandbox/reviewer.Containerfile:9-13,26,52; pipeline/reviewer.sh:7",
   "exploit": "curl | bash (nodesource), unpinned shrooms install-agent.sh, unpinned npm -g, ubuntu:24.04 tag, unpinned pipx semgrep, and localhost/open-reviewer:latest are all mutable inputs to code that holds the Venice key.",
   "fix": "Pin every dependency by digest or exact version with checksums/signatures; vendor the installer; build the reviewer image reproducibly.",
   "confirmed": "read"
  },
  {
   "id": "FR-14",
   "severity": "Low",
   "title": "Report pass shares the contaminated sandbox and conversation with the untrusted module",
   "files": "pipeline/review-in-sandbox.sh:66-73; pipeline/reviewer-prompt.md; pipeline/render.py:124-140",
   "exploit": "The report is model-written in the same pi conversation and container, after untrusted module/repo text; prompt injection or a symlinked/faked report.md can reach publication. BACKLOG.md notes the report stage is not caged.",
   "fix": "Generate the report in a fresh context from a typed host-collected evidence bundle, validate it, and keep it outside the module's mount namespace.",
   "confirmed": "read"
  },
  {
   "id": "FR-15",
   "severity": "Low",
   "title": "Reviews are served over plain HTTP and the hash chain is unsigned",
   "files": "host/Caddyfile:4-14; host/20-web.sh:11-18; pipeline/render.py:99-106; docs/proposal.md:120",
   "exploit": "A network attacker can read/modify reviews in transit; chain.txt is only a hash chain with no signature or published root, so a host-side rewrite cannot be detected, contrary to the proposal's signed-log claim.",
   "fix": "Serve TLS, sign chain.txt with a key outside the review host, publish the signature/signing identity and verification instructions.",
   "confirmed": "read"
  },
  {
   "id": "FR-16",
   "severity": "Low",
   "title": "Any file staged in run evidence/media directories is published same-origin",
   "files": "pipeline/render.py:92-97; host/Caddyfile:5-6",
   "exploit": "render.py copies the whole media/logs/checks/build/behaviour trees; a module-placed media/page.html with a phishing link was published at dest/media/page.html and is reachable via Caddy's browsable /media/. CSP blocks scripts/forms, so it is same-origin content injection rather than XSS.",
   "fix": "Copy an explicit allowlist of media extensions with content sniffing, strip unknown files, disable directory browse, and serve raw evidence with Content-Disposition or from a separate origin.",
   "confirmed": "run"
  }
 ],
 "ran": [
  "Mapped the repository and read pipeline/*.sh, pipeline/*.py, sandbox/*, host/*, docs/proposal.md, NOTES.md, test-modules/*",
  "Reviewed pre-run gitleaks/osv/semgrep results in /run/checks",
  "FR-1: render.py on a run with report.md and media/leak.txt symlinks to a marker, and a logs symlink to a directory; marker published in dest/report.md, dest/media/leak.txt and dest/logs/fakedir/id_ed25519",
  "FR-2: touch /run/behaviour/.wtest succeeded; /run, /run/behaviour, /run/media, /run/logs, /run/src are 0775 root-writable",
  "FR-3: external /etc/resolv.conf; direct UDP DNS to 195.179.224.53 and 1.1.1.1 succeeded; TCP to 8.8.8.8:53, 1.1.1.1:443, 140.82.121.4:443 succeeded; dns.log stayed 0 bytes after the leaky control sample",
  "FR-3/analyse: synthetic relative-dirfd read and literal-IP connect produced empty bait_files_read/contacted_hosts; absolute path + DNS control was flagged",
  "FR-4: REPO=\"x'; echo INJECTED > /tmp/probe-ci; echo '\" against the exact git clone line created /tmp/probe-ci",
  "FR-5: gateway.py with mock upstream and REVIEW_BUDGET=0.8 accepted claude-3-opus-20240229 and recorded cost 1.65 / spent 1.65; REVIEW_TOKEN and REPORT_TOKEN readable in /proc/1/environ, models.json 0644",
  "FR-6: code inspection of timeout 2400 versus MAX_MIN=150 and the in-container report pass",
  "FR-7: gitleaks found a committed AKIA key, then a repository .gitleaks.toml allowlist suppressed it (0 findings); malformed config exits fatally",
  "FR-8: real install_one with manifest name ../../../../../../tmp/lgx-pwn-dir wrote /tmp/lgx-pwn-dir/marker; GNU tar rejected a ../ member but accepted an absolute symlink member",
  "FR-9: render.py with report.json findings as an object raised AttributeError and wrote no index.html",
  "FR-11: extracted real ver/satisfies from fetch_deps.py; satisfies('0.9.0','^0.1.0') returned True",
  "FR-10/FR-15: grepped proposal claims against code; confirmed no schema, no signing, no overlay/uninstall/build-compare",
  "FR-16: render.py published a module-authored media/page.html at dest/media/page.html"
 ],
 "checked_clean": [
  "HTML escaping of all module/report-derived fields in render.py, plus Caddy CSP default-src 'none', nosniff and forced JSONL content type",
  "Venice inference key kept on the host and absent from the sandbox per sandbox-check.sh; gateway bearer-token check works for the configured token",
  "Bait generation uses unique random canaries and records path->canary; absolute-path read plus DNS lookup is detected",
  "/pipeline and the Basecamp AppImage are mounted read-only and sandbox-check verifies they cannot be written",
  "Dependency downloads are verified against catalogue SHA-256 and cached by hash",
  "Model calls are logged with usage, provider 5xx/429 are retried, and known secret patterns (including bait canaries) are redacted",
  "gitleaks found only the intentional fake-key template; osv-scanner reported no vulnerable dependency metadata; semgrep flagged the curl|bash supply-chain line",
  "Kill-by-PID discipline and recorder renaming avoid the earlier pkill self-kill failure"
 ],
 "not_tested": [
  "No Basecamp module run: the target repo has no metadata.json, no lgx and no module UI, so there was nothing to install/open/screenshot",
  "Host-only components: podman, Caddy, shrooms mesh, real /home/reviewer/.pi/agent/.env and Venice key were unavailable",
  "Full end-to-end review (clone to publish); only individual components were exercised locally",
  "Nested strace was denied (PTRACE_TRACEME EPERM); analyser tests used synthetic syscall logs plus the live global capture",
  "Kernel/container escape; FR-1 is a data-flow escape via symlinks, not a kernel exploit",
  "DNS parser fuzzing, disguise/bait on real podman, very large evidence/media trees, Caddy path handling",
  "Leaky sample's outbound TLS POST body (no TLS MITM in the sandbox), only its DNS/connect side was observed"
 ],
 "repo_tried_to_instruct_reviewer": true
}