diff --git a/.agents/skills/daily/SKILL.md b/.agents/skills/daily/SKILL.md index e933a7b..44833c1 100644 --- a/.agents/skills/daily/SKILL.md +++ b/.agents/skills/daily/SKILL.md @@ -40,7 +40,7 @@ Update outcomes ONLY after upstream merge/close, never on open. Stop here if `-- Group by rule family; auto-flag tests/sample/examples/demos/fixtures as candidate-FP. Inspect top 5 real candidates in-repo via `gh api .../contents/`. Write per-finding verdicts. Evaluate the quality gate boolean. ## Phase 5 — Publish (gated) -Always: write `docs/scans/.md` + prepend `docs/index.md`. If gate TRUE and not strict-norm: file focused de-branded courtesy issue (+ PR for clean one-line fixes). If strict-norm: post-only or one-issue-per-critical. If gate FALSE: post-only. Open `docs:` PR on `elfrost/ai-patchlab`, merge, verify publication via `gh run list --workflow=pages-build-deployment --limit 1` = `success` AND the post returns 200 serving the new text. Do NOT gate on `pages/builds/latest` — on this Actions-published site it reports `errored` even when the deploy succeeded (2026-08-26). +Always: write `docs/scans/.md` + prepend a row to `docs/index.md` + a bullet to `docs/scan-log.md` (bump both scan counts). If gate TRUE and not strict-norm: file focused de-branded courtesy issue (+ PR for clean one-line fixes). If strict-norm: post-only or one-issue-per-critical. If gate FALSE: post-only. Open `docs:` PR on `elfrost/ai-patchlab`, merge, verify publication via `gh run list --workflow=pages-build-deployment --limit 1` = `success` AND the post returns 200 serving the new text. Do NOT gate on `pages/builds/latest` — on this Actions-published site it reports `errored` even when the deploy succeeded (2026-08-26). ## Phase 6 — Record Update `reports/.daily_state.json`; update memory on resolutions/lessons; print a 3-line summary. diff --git a/.claude/commands/daily.md b/.claude/commands/daily.md index cac5a3e..8a79371 100644 --- a/.claude/commands/daily.md +++ b/.claude/commands/daily.md @@ -72,7 +72,7 @@ Use `--ignore-file` if the repo has obvious sample/example/demo subtrees (until 4. **Evaluate the quality gate:** is there ≥1 real, exploitability-shaped, high-confidence item? Record the boolean — it decides Phase 5 filing. ## Phase 5 — Publish (gated) -1. **Always:** write `docs/scans/.md` from `docs/templates/scan-post.md`; prepend a new line to the Scans list in `docs/index.md`. +1. **Always:** write `docs/scans/.md` from `docs/templates/scan-post.md`; prepend a new row to the Scans table in `docs/index.md` **and** a new bullet to `docs/scan-log.md` (the full prose archive), and bump the scan counts in both headers. Three files, every time — on 2026-09-06 the log was found eight entries behind the index because this step only named `index.md`. 2. **If quality gate TRUE and repo not strict-norm:** file a focused courtesy issue on the target (de-branded, with code-path note + concrete fix). If a finding has a clean one-line/one-file fix, also fork → branch → PR referencing the issue. 3. **If repo strict-norm:** post-only, or one issue per critical finding — no grouped issue. 4. **If quality gate FALSE:** post-only (clean-scan write-up). File nothing upstream. diff --git a/docs/index.md b/docs/index.md index 09c978f..023d2ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ remediation and confidence rules to normalize the findings. > **Want this run privately against your own codebase?** I do independent > security review of AI agents, MCP servers, and LLM apps — -> [**work with me →**]({{ '/work-with-me' | relative_url }}). 95 scans, 21 confirmed fixes, methodology in the open. +> [**work with me →**]({{ '/work-with-me' | relative_url }}). 96 scans, 21 confirmed fixes, methodology in the open. > **OpenAI just launched [Daybreak](https://openai.com/index/daybreak-securing-the-world/) and Patch the Planet.** > Same remediation loop, opposite trade-off: their path is a cloud frontier model; @@ -94,12 +94,13 @@ login and static assets. Fifty-two flagged, none reported. ## All scans -94 scans, newest first. **Findings** is the raw count the tools produced; +96 scans, newest first. **Findings** is the raw count the tools produced; **Real** is what survived curation. The gap between those two columns is the entire job. | Date | Repository | Findings | Real | Outcome | |---|---|---:|---|---| +| 2026-09-06 | [ApodexAI/FrontierAgent](scans/apodexai-frontieragent.html) | 58 | 1 real — withheld | private | | 2026-09-05 | [doobidoo/mcp-memory-service](scans/doobidoo-mcp-memory-service.html) | 176 | 0 real | — | | 2026-09-04 | [basicmachines-co/basic-memory](scans/basicmachines-co-basic-memory.html) | 270 | 0 real | — | | 2026-09-03 | [samuelgursky/davinci-resolve-mcp](scans/samuelgursky-davinci-resolve-mcp.html) | 97 | 1 real — withheld | private | diff --git a/docs/scan-log.md b/docs/scan-log.md index db4ebfb..1b02aec 100644 --- a/docs/scan-log.md +++ b/docs/scan-log.md @@ -6,8 +6,17 @@ description: "The complete AI PatchLab scan log: every public repository scanned # Full scan log -Every scan in the series, newest first, with the summary written on the day of the scan. 87 scans. For the compact index, see the [scan log home]({{ '/' | relative_url }}). +Every scan in the series, newest first, with the summary written on the day of the scan. 96 scans. For the compact index, see the [scan log home]({{ '/' | relative_url }}). +- **2026-09-06** — [ApodexAI/FrontierAgent](scans/apodexai-frontieragent.html) — 58 findings (58 above the medium floor), **1 real — withheld** — an **agent runtime, terminal product and evaluation suite** (1.8k★ two weeks after going public, Apache-2.0, Apodex AI): ReAct and coordinator-plus-sub-agents workflows over a task-scoped sandbox, a fifty-module plugin tool tree, and an optional Gradio demo for a Hugging Face Space, with a real `SECURITY.md` (named mailbox, 48h ack, public vuln issues forbidden) and private vulnerability reporting enabled. **Every one of the 58 collapsed on inspection** — loader imports keyed on a runtime class, an execution epoch logged as a "token" (seven-for-seven false), `sk-test-…` placeholders inside the project's own *leak-guard test suite*, operator-configured endpoints behind the `urllib` rule, the sandbox shell itself behind `shell=True`, a tar extractor that both resolves members and passes `filter="data"`. **The real finding is the composite class no rule sees:** a download allow-list one directory too wide, in a component whose README records — as *verified* — that the sibling directory it exposes is never served to a browser; the [docstring-as-oracle](scans/hkuds-openopc.html) diff between that paragraph and one launch argument is the whole finding. Verified by execution both ways: the artifact served, then the one-line remedy keeping deliverables downloadable while the artifact returns 403. Same-session only, filed Low; class only here · reported privately via GitHub PVR, post-only +- **2026-09-05** — [doobidoo/mcp-memory-service](scans/doobidoo-mcp-memory-service.html) — 176 findings, **zero real after curation; thirtieth clean scan** — a persistent memory server for AI agents (1.9k★) that published **four critical advisories on the exact day of the scan**, all fixed at HEAD, and the tools surfaced none of them: every one was an absence or a parity gap (an auth check missing on one transport that its sibling had). The dominant cluster was the parameterized-SQL identifier FP, ninth appearance in the series · post-only, strict-norm target +- **2026-09-04** — [basicmachines-co/basic-memory](scans/basicmachines-co-basic-memory.html) — 270 findings, **zero real after curation; twenty-ninth clean scan** — a local-first Markdown knowledge base with an MCP server; the volume was fixtures, identifier-only SQL and by-design file tooling on a project whose purpose is reading and writing the user's own notes · post-only, strict-norm target +- **2026-09-03** — [samuelgursky/davinci-resolve-mcp](scans/samuelgursky-davinci-resolve-mcp.html) — 97 findings, **1 real — withheld** — an MCP server for DaVinci Resolve (desktop, stdio by default, opt-in networked transport). The class: a credential-handling weakness confined to the opt-in networked transport, local exposure only, a one-line fix following a pattern the repository already contains elsewhere (the sibling differential). The policy's first-named channel (GHSA) turned out switched off — `enabled:false`, `403` on the create call — so the report went to the mailbox the README names, and the dead channel is reported alongside the finding · reported privately, post-only +- **2026-09-02** — [HKUDS/OpenOPC](scans/hkuds-openopc.html) — 56 findings, **1 real — withheld** — a self-hosted "AI-native company" platform (1.6k★) with a genuinely careful permission model and a WebSocket control plane that drives it while authenticating nobody, bound to `0.0.0.0` by default: the [loopback-inversion](scans/liaohch3-claude-tap.html) class, and the cleanest instance in the series because the security engineering around it is good. Verified against the project's own permission resolver (ask → allow with no prompt). Filed High via GitHub PVR, `GHSA-fp48-59rc-43qg`, in triage · reported privately, post-only +- **2026-08-31** — [shy3130/tick-stock-panel](scans/shy3130-tick-stock-panel.html) — 77 findings, **1 real** — an LLM-driven A-share stock screener (4.4k★): screener conditions and ordering interpolated raw into a DuckDB query on an engine with external access enabled — the capability switch that turns "injection into a sandbox" into host access, invisible to SAST and SCA alike · ✅ **Resolved 2026-09-03 in 3 days (21st fix in the series)** — the maintainer shipped a regression test with a positive *and* a negative control +- **2026-08-30** — [SenteLabsAI/OpenExecutive](scans/sentelabsai-openexecutive.html) — 124 findings, **1 real — withheld** — an AI "virtual executive team" (3.8k★): a webhook whose signature verification is *skipped* rather than failed when its optional secret is unset, the documented default, while the sibling integration verifies unconditionally — the majority sibling is the contract. Plus a `SECURITY.md` that mandates GitHub PVR and deprecates email while PVR returns `enabled:false` · drafted for private delivery, post-only +- **2026-08-28** — [Ontos-AI/knowhere](scans/ontos-ai-knowhere.html) — 129 findings, **1 real — withheld** — a document-ingestion pipeline sold as the memory layer between dirty documents and AI agents (2.7k★): one missing-authentication gap on an internal storage-event webhook whose verifiers are stubs, verified by execution (wrong token rejected, absent header accepted), and a 68-CVE dependency table that all resolves to a single lockfile · drafted for private delivery, post-only +- **2026-08-27** — [Zleap-AI/SAG](scans/zleap-ai-sag.html) — 60 findings, **1 real** — an agent framework whose login did not verify the password: any name signed in as the owner account · ✅ **Resolved 2026-08-30 in 3 days (19th fix in the series)** — posing the product question beat proposing a patch - **2026-09-01** — [future-agi/future-agi](scans/future-agi-future-agi.html) — 1227 findings (1224 above the medium floor), **1 real — withheld** — an **end-to-end LLM/agent observability and evaluation platform** (1.9k★, Apache-2.0 + EE): OTLP trace ingestion into a ClickHouse store of prompts/completions/tokens/costs, an evaluation engine, a simulation runner, a Go **AI gateway** (`agentcc-gateway`, an OpenAI/Anthropic/Gemini-compatible proxy with per-org keys, rotation and RBAC) and a Django backend, with a real `SECURITY.md` (named email, SLA table, safe harbor, in-scope list, public vuln issues forbidden). **Largest raw count in the series — and one real finding no rule represented.** The class: **one service in a multi-service deployment is published on all interfaces while every service beside it, including every datastore, is correctly loopback-bound, and the production overlay inherits the exposure unchanged** — the exposed service is the most dangerous one in the file to leave open, and reaching it hands an unauthenticated caller a path to the trace store. Three reportability properties, each a series pattern: **the maintainer's own compose comment is the oracle** (*"bound to 127.0.0.1 so only the host can reach them"*, applied to Postgres/ClickHouse/Redis/MinIO/Temporal/collector — the finding is the outlier), the [contract-vs-artifact](scans/vexa-ai-vexa.html) footing in a deployment file; **N deployment paths, and the question is whether they agree** — the production overlay re-binds every secret with `${VAR:?must be set for production}` (genuinely good) but names only `environment:`/`restart:`, not `ports:`, so the documented production command inherits the exposure; and **I proved it with the vendor's own merge engine** — rendered the exact `deploy/README.md` command through `docker compose config` and read the published bindings out of the result (exposed service on `0.0.0.0`, every datastore on `127.0.0.1`), [running the primitive](scans/nottelabs-notte.html) rather than reasoning about override semantics. **Scope discipline kept it to one**: two other `0.0.0.0` services are the intended public surface (frontend + reverse-proxied API), and two management UIs published without a loopback bind sit behind Compose `profiles:` — opt-in, absent from the default and documented-production deployments, and the rendered config confirmed they don't appear. **Two auth hypotheses returned NO and that is part of the record**: the gateway mounts a large admin plane *outside* its `/v1/` auth prefix (mint keys, set org configs, rotate creds) — the [claude-tap inversion](scans/liaohch3-claude-tap.html) shape — but all **24** admin handlers across four files call an in-handler `requireAdmin`/`checkAdminAuth` with a constant-time compare that **fails closed on an unset token**; and the `/v1/` middleware short-circuits on a valid license token (a [caller-influenced auth branch](scans/liaohch3-claude-tap.html)) but the verifier pins RS256, resolves `kid` from a **local** key map, and validates the full claim set, so branch-then-serve is safe. **Noise**: 100 mutable-action-tag GHA rows, 94+44 raw/formatted-SQL that resolve to the [#1 identifier FP](scans/mnemosyne-oss-mnemosyne.html) (ClickHouse builders interpolate regex-validated keys and an allowlisted bucket function, bind every value via `%(param)s` — the four taint-tracked hits were audit-log f-strings and the allowlisted bucket fn), 3 gitleaks hits all covered by the repo's own `.gitleaks.toml`. **The 26 criticals are a coverage story**: `scan_dependency` found *no root manifest* because this is a **monorepo** (manifests under `futureagi/`), so the ChromaDB/LiteLLM/Authlib/Django/langchain criticals came from Trivy's lockfile parse and want the *version-match → reachable → mitigated* gate — several are Proxy-only/transport-only in libs used as clients. **Monorepo root-only coverage gap, re-confirmed as the top backlog item.** Strict-norm · reported **privately** to `security@futureagi.com` with a full dossier + `docker compose config` reproduction · post-only, finding **withheld** (class only) until remediated · the email send is the operator's manual step - **2026-08-29** — [ginlix-ai/LangAlpha](scans/ginlix-ai-langalpha.html) — 372 findings above the medium floor, **1 real** — a **financial-market agent platform** (1.7k★, Apache-2.0, daily merges, FastAPI + React + a Daytona/Docker code sandbox), and one of the most carefully defended codebases in the series. Semgrep's top finding — `pull_request_target` checking out untrusted fork code — **retired on evidence outside the repository tree**: the GitHub environments API shows a `fork-ci` environment with required reviewers and `prevent_self_review`, and the workflow pins the immutable head SHA, sets `persist-credentials: false`, and drops the token to `contents: read`. The author even documented that `uv sync` runs fork build hooks before pytest — a better threat model than the rule that flagged it. All 30 gitleaks hits are fixtures in the project's **own** secret-redaction and leak-detection tests; the 102-finding SQL cluster binds every value with `%s` and interpolates only a constant column list. What survived is a **composite**: `GET /api/v1/preview/{workspace_id}/{port}` is unauthenticated and its docstring promises it "does NOT start stopped sandboxes (to prevent denial-of-wallet)" — but it enforces that with a DB `status == "running"` check and then calls the *acquisition* path, which reaches `PTCSandbox.reconnect`, documented as "it **starts a stopped sandbox**." **Four sibling unauthenticated routes in the same codebase refuse exactly that call**, citing "a stale 'running' DB row with no warm session," and use the no-wake `get_session_if_ready` accessor whose fence parameter was made mandatory "so a new caller cannot omit the fence." The majority sibling is the contract; the fifth route never inherited it. The lead came from the tool's **coverage warning**, not its findings: Semgrep could not parse `claude.yml`, `release.yml`, or `sandbox-integration.yml`, which were precisely the workflows worth reading. - **2026-08-26** — [ascending-llc/jarvis-registry](scans/ascending-llc-jarvis-registry.html) — 235 findings above the medium floor, **1 real — withheld** — an **enterprise MCP/A2A gateway** that brokers per-user OAuth credentials to downstream tool servers (2.8k★, Apache-2.0, commercial backing, **7 distinct human PR authors in 60 days**), and the cleanest demonstration yet of why the mitigation gate is not optional. All four criticals retired: the **LiteLLM CVEs** (SQL injection, Host-header auth bypass, MCP command execution, key-gen privilege escalation) are Proxy-server-only, and this repo declares `litellm>=1.50.0` then **never imports it** — one grep outside the lockfile returns the dependency declaration and nothing else. The largest family, **21× GitHub Actions shell injection**, retired on a single fact: there is no `pull_request_target` anywhere in the repository, so every flagged workflow runs either from a fork with a read-only token and no secrets, or from a context that already requires write access. **16 gitleaks "secrets"** are docs example output and MongoDB seed fixtures, against a `tartufo.toml` that carries a written reason on every exclusion; **12× `logger-credential-leak`** logs usernames and source ids, never a credential; the lone **`unverified-jwt-decode`** is by design and its two callers both verify-then-branch, over a token layer that pins RS256, pins the `kid`, enforces issuer and audience, and blocks token-class confusion with positive equality checks. **The near-miss is the story**: the registry allows CORS origins by a regex whose `.*\.compute.*\.amazonaws\.com` arm admits the public DNS name of *every EC2 instance on the internet*, with `allow_credentials=True` — confirmed allowed against real Starlette 1.6.0, with the controls correctly denied. It is still **inert**, because every cookie the app sets is `SameSite=Lax` and `amazonaws.com` compute domains are on the Public Suffix List, so the attacker's `fetch()` carries no session at all. Four verification steps said "real"; the fifth — grep for the mitigation before flagging — retired it, and this series has filed that same class as a genuine finding against five other projects. **The one real defect is object-level authorization no scanner can see**: two sibling handlers in one router return per-user state from the same deterministic identifier, one enforces ownership against the caller and the other takes no caller identity at all, behind a permission every role holds. The intra-repo differential is the proof — the guarded sibling *is* the project's own contract. Routed privately to `security@ascendingdc.com` per the project's SECURITY.md, with a patch; public detail withheld. diff --git a/docs/scans/apodexai-frontieragent.md b/docs/scans/apodexai-frontieragent.md new file mode 100644 index 0000000..1df82e6 --- /dev/null +++ b/docs/scans/apodexai-frontieragent.md @@ -0,0 +1,162 @@ +--- +layout: default +title: "ApodexAI/FrontierAgent: security scan" +description: "Security scan of ApodexAI/FrontierAgent: 58 findings above the medium floor, every one a false positive or by design, and one real gap none of them pointed at — in the optional public demo, an artifact the project's own docs say is never served to a browser is served to the browser. Disclosed privately, detail withheld." +date: 2026-09-06 +--- + +# ApodexAI/FrontierAgent — security scan + +**Repository:** [ApodexAI/FrontierAgent](https://github.com/ApodexAI/FrontierAgent) — 1.8k★ two weeks after going public, Apache-2.0, backed by Apodex AI. An agent runtime, terminal product and evaluation suite for long-horizon research and file work: a ReAct workflow and a coordinator-plus-sub-agents "Agent Team" workflow, a task-scoped sandbox (`/inputs` read-only, `/workspace`, `/outputs`), a plugin tool tree of fifty-odd modules, and an optional Gradio demo for a Hugging Face Space. +**Commit scanned:** `1233828` (HEAD of `main` at scan time) +**Scan date:** 2026-09-06 +**Disclosure status:** **Private — detail withheld.** The repository's `SECURITY.md` names a security mailbox, forbids public issues for vulnerabilities and commits to a 48-hour acknowledgment. One Low-severity finding has been filed through the repository's private vulnerability reporting channel; only the finding *class* appears below. This page will be expanded once the maintainers resolve, or after a 90-day window. + +## Summary + +| Severity | Count (medium+) | +| --- | ---: | +| Critical | 0 | +| High | 13 | +| Medium | 42 | +| Low | 0 | +| Info | 3 (scanner meta) | + +**Total findings:** 58 above the `medium` floor — **1 real after curation, withheld; zero of the 58 pointed at it.** + +Coverage note: Semgrep reported six partial-parse errors, all on Dockerfiles and shell +scripts, zero rule timeouts, so Python coverage is complete. pip-audit resolved the +`pyproject.toml` floors (58 packages, no advisories) and Trivy read `uv.lock` (no advisories), +so for once the two install paths agree and both are clean. + +## The one real finding — class only + +**Class:** a download allow-list scoped to a parent directory (CWE-552), in a component whose +own documentation says the sibling directory under that parent is never served. + +The shape, without the specifics. The optional demo deployment gives every visitor an +isolated per-session directory tree with an unguessable id, and it is genuinely well +contained: the agent's writable root is one subtree, its readable roots are enumerated, a +tool-boundary observer refuses any path outside them, three separate layers scrub the +operator's secrets out of the stream, out of tool arguments and out of downloadable files, +and the tool set is a fail-closed allow-list with a hard deny on top. The README then +records, in its own words, the one place a secret can still land on disk — a raw run +artifact written before any scrubber runs — and explains why that is acceptable: the +artifact sits outside the agent's reach, is deleted with the session, and *is never served +to a browser*. + +That last clause is the one that does not hold. A single launch argument grants the web +framework permission to serve a directory that contains that artifact, and the framework +does exactly what its documentation says it will. The visitor who owns the session can fetch +their own raw artifact, unscrubbed. Nobody else's — the ids hold — so the scope is one +visitor reading a fuller record of their own run than the UI shows them, and that record +includes the full system prompt and, in the documented worst case, whatever a hostile or +misbehaving model endpoint managed to place in a tool call. + +I filed it as **Low**. It is same-session only, and it becomes a secret leak only when a +secret first lands in the artifact, which the project itself documents as possible but not +routine. The fix is one line, and I verified it against the same primitive: the deliverables +stay downloadable, the artifact returns 403. + +## What verification added + +Two executions, both against a local listener with the demo's exact launch arguments and a +session tree laid out the way the demo builds it. The first showed the artifact, a dotfile +beside it, and the read-only inputs directory all served with 200, while two controls +outside the allowed root were refused. The second removed the one argument and showed the +deliverable still served through the framework's own cache while every raw session path +returned 403. No deployed instance was touched. + +Reading alone would have made the claim; the framework's documentation is explicit about +what a directory in that allow-list means. But the README says the opposite was *verified*, +and when a maintainer's document says they tested it, the burden is to run it, not to argue +from the manual. What the README actually lists as tested is another session's artifact, +which fails on the unguessable id. The visitor's own artifact was never on the list. + +## What the 58 findings actually were + +Every one collapsed on inspection: + +- **Twelve `non-literal-import`** hits are the workflow and plugin loaders. The module name + is derived from a runtime dataclass's `__module__` attribute or from a registry, never from + model output. Framework plumbing. +- **Twelve mutable-action-tag** hits in three workflow files, `actions/checkout@v4` and the + like. Supply-chain hygiene worth pinning to SHAs; not a vulnerability. +- **Seven `logger-credential-leak`** hits in the scheduler. The "token" being logged is an + execution epoch used to detect stale runs. Seven-for-seven false positive, the same as this + rule's record across the series. +- **Six Gitleaks** hits, all `sk-test-…` placeholders inside `tests/test_hf_space_leaks.py` + and `tests/test_hf_space_config.py` — the project's own test suite *for its secret-leak + guards*. The scanner flagged the test that proves the guard works. +- **Six `dynamic-urllib-use`** hits. Every destination is an operator-configured endpoint + read from the environment (a vision model, an OCR pool, an SGLang health check). The + model-controlled fetches go through a separate guarded client that pins the resolved + address and walks redirects hop by hop, and none of these six are on that path. +- **Four Docker root-user** hits across the runtime images. The sandbox shell drops to an + unprivileged uid per exec and runs under a per-exec cgroup, so root in the image is the + usual hardening item, not an exposure. +- **Two `exec-detected`** hits are the reader and writer bundles compiling *their own source + fragments* into a single-file script for the sandbox. Two `dangerous-globals-use` hits + index a fixed dispatch dict by file extension. One `subprocess-shell-true` hit is the + sandbox shell tool itself — on a code-executing framework, that is the product surface, + and it runs with a minimal env, an unprivileged uid and a memory cap. +- **One `tarfile-extractall`** hit in a benchmark staging script that resolves every member + against the base directory *and* passes `filter="data"`. Credit the defense. +- One Jinja2 hit on a prompt template, one Python 3.7 importlib compatibility note. Noise. + +## Patterns observed + +This is the **composite-finding** class, and one of the cleanest examples the series has +met: two decisions, each defensible in isolation, in two different files, composing into a +contradiction of the project's own written containment. Decision one, "accept the raw +artifact because it is unreachable," lives in a README paragraph and a test docstring. +Decision two, "allow-list the sessions root so the file component can serve deliverables in +place," lives in a launch call. No rule fires on either. The bug is the edge between them. + +It is also the **docstring-as-oracle** pattern doing exactly what it is for. The README +paragraph that records the caveat is what made the finding precise: it named the artifact, +named the property it depends on, and named the property as verified. Diff that sentence +against the launch call and the gap is a single argument. Projects that write down their own +caveats are easier to review and easier to fix, and this one is a strong argument for the +habit — the reasoning was correct everywhere except one line, which is why the fix is one +line. + +Everything else this review touched held, and it touched a lot: the path guard resolves +symlinks before the prefix check and only lets a symlink *widen* access from an +operator-curated tree; the fetch guard refuses split-horizon DNS by pinning every resolved +address; the tool policy is an allow-list and a hard deny at once, installed before anything +runs, precisely so a future profile edit cannot re-admit the shell. For a two-week-old +public repository this is an unusually finished threat model, and the scanner had nothing +to say about any of it. + +## Notes on the tool + +- **58 findings, one real, and the real one is in none of them.** The recurring limit: a + download allow-list that is one directory too wide is not a pattern any SAST rule + represents, and the evidence that it matters lives in a README paragraph. +- **The `logger-credential-leak` rule is now seven-for-seven false on this repo and, as far + as the series has recorded, has never once been right.** Its trigger is the substring + "token" in a log call. Backlog: re-rank it below the fold unless the logged value + traces to a secret source. +- **Gitleaks flagged the leak-guard test suite.** A `.gitleaks.toml` or baseline would + clear it, and the finding count on a well-tested repo continues to scale with test + richness rather than risk. +- **The two dependency install paths agreed today.** The `dependency-scan-unaudited-lockfile` + meta finding still fired, correctly, because pip-audit did not read `uv.lock`; Trivy did, + and both were empty. Worth recording as the case where the meta finding is the honest + "checked both" rather than a coverage gap. + +## Disclosure timeline + +- 2026-09-06 — scan run against `1233828`; finding verified by execution, remedy verified by execution +- 2026-09-06 — reported privately via GitHub private vulnerability reporting, with both repro scripts +- 2026-09-06 — public post (this page), finding class only + +## Reproduce + +```bash +git clone https://github.com/ApodexAI/FrontierAgent /tmp/scan-target +python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/apodexai-frontieragent --min-severity medium +``` + +*The scan is reproducible; the finding detail is withheld until the maintainers resolve or a 90-day window elapses, per the project's security policy.*