← Home
LIMITS

What Lucin misses

The honest coverage gaps: a 24% false-negative rate, 17% SSRF recall, path traversal deliberately unregistered, and why precision was chosen over recall.

docsbenchmarkslimitsruleschangelog

Honesty is a feature here. This page states the boundaries plainly. If a claim wouldn't survive a security researcher reading the actual code, it doesn't belong in the docs — so these are the real edges, each tied to the code or the measured number that proves it.

SCAN is a static, pre-deploy scanner

It finds enabling misconfigurations and known dangerous patterns in code before you ship. It does not:

A clean scan means no known dangerous static patterns were found. It does not mean the agent is safe under all inputs.

Language support — Lucin is a Python scanner with a language-independent MCP surface

Target Depth
Python (.py, .pyi) Full AST: taint analysis, kind-scoped sanitizers, cross-function/intra-class flows
MCP + agent JSON configs (.mcp.json, claude_desktop_config.json, …) Full — independent of the server's implementation language
Agent Skills (SKILL.md + YAML frontmatter) Full
Shell (.sh, .bash, .zsh) Parsed with bashlex
TypeScript / JavaScript (.ts, .tsx, .js, …) Not supported. Not enumerated at all — no parser reads them
Java, Kotlin, C#, Go, Rust, Ruby, PHP, Swift, C/C++ Not supported. Not enumerated

A target with no supported files is reported as NOT ANALYSED, not as a clean scan. No score is shown, no badge is offered, and --ci exits 3 (distinct from 1 = "findings at or above threshold") so a pipeline can tell unable to analyse from analysed and failed. Every scan also prints a coverage line — Analysed 1 of 201 source files; 200 unsupported (.rs 200) — whether coverage is complete or negligible.

This is the scan-level counterpart of the finding-level evidence gate: a finding nobody can check must not be CRITICAL, so a scan that examined nothing must not score 100. Before this, a Rust agent containing Command::new("sh").arg("-c").arg(user_cmd) and a hardcoded sk-proj- key scored 100/100 — Excellent, was offered a README badge, and passed --ci --fail-on high with exit 0. Regression tests: tests/test_language_coverage.py, tests/test_ci_exit_codes.py.

Why we are not adding more languages. Agent frameworks are overwhelmingly Python and TypeScript; building Rust/Java/C# parsers is the treadmill that turns an agent scanner into a mediocre general-purpose SAST. TypeScript is the one genuine gap worth closing — it is co-Tier-1 with Python in the MCP ecosystem — and it is gated on building a JS/TS recall corpus first, because today we have no way to measure whether such support works.

What the MCP row buys you. A Go or Rust MCP server's implementation is invisible to Lucin, but its wiring is not: overprivilege, unpinned npx -y, leaked tokens in env, and filesystem-root grants are all declared in JSON, and that is where most MCP risk lives. lucin scan covers that surface regardless of what language the server is written in.

Per-class recall gaps (measured, not hidden)

Measured recall is 38/50 = 76% (python benchmarks/recall_corpus.py). The gaps:

SSRF — partial (17%)

AG-SSRF is deliberately conservative: it fires only when taint forms the URL host of a request sink (u = f"http://{host}"; get(u)). It will not flag a bare whole-URL parameter or a __init__-assigned sink, and it skips patterns that a legitimate URL-fetching tool would also match. This trades SSRF recall for the precision result (11 confirmed FP outside a documented per-repo known-capability allowlist) — by design (src/lucin/detectors/ssrf.py).

Path traversal — 0% (detector built but UNREGISTERED)

AG-PATH-TRAVERSAL is built, sound, and unit-tested — but it is intentionally not registered in the scan path. The benign corpus contains byte-identical legitimate file tools (open(param), os.path.join(base, name), Path.write_text) that are indistinguishable from the vulnerable shapes under static analysis. Registering the detector would break the precision result (11 confirmed FP outside a documented per-repo known-capability allowlist), so it is gated off — precision over recall. The gate and its rationale are documented in src/lucin/detectors/__init__.py.

Container escape — 80%

AG-DOCKER-EXEC resolves docker commands built via a variable, but misses cases where the container is driven through the docker SDK rather than a subprocess command.

Interprocedural / cross-file taint

Lucin does not perform whole-program, call-graph-based interprocedural taint analysis. The standard Python call-graph tool, PyCG, is unavailable on the build mirror, so it is not vendored or integrated. What actually runs:

analysis/cfg.py builds a real intraprocedural CFG but is imported by nothing — it was written to enable flow-sensitive taint and never wired in. Dead code today.

Consequence: a vuln where untrusted input enters in one function and reaches a sink in a different file, with no single tool exhibiting the dangerous combination, may be missed with path-level precision. The capability heuristic recovers many of these, but not all, and dynamic dispatch is treated as a conservative barrier.

Detectors held back on purpose

Temporal ledger (src/lucin/temporal/) — experimental, unwired, not a claimed capability

TemporalLedger (Merkle-chained SQLite) and the cross-session detector built on it (detectors/multiagent/memory_integrity.py) are unit-tested directly but no lucin scan/lucin verify code path ever calls record_event, so they have zero effect on any real scan today. Do not read any README/site claim of "temporal awareness" or "T3 detection" as describing shipped behavior until a scan path writes to the ledger.

TemporalLedger.verify_integrity() is tamper-evident, not tamper-proof: it proves the recorded hash chain is internally consistent, which catches accidental corruption and naive tampering. It does not protect against an attacker with the same local write access the ledger process itself has — that attacker can edit history and recompute every hash forward from the edit point, and verify_integrity() will return True regardless. Tamper-proofness against a locally-privileged attacker needs an external, independently-controlled write path (signing with a key the process can't read, or a remote append-only store) — infrastructure this project has deliberately not built (see launch/evolving conviction/PHASE_6_PLAN.md §1).

Experimental / not-yet-validated capabilities

These ship as code but are not part of the validated core:

Launch-gated residuals (cannot be faked pre-launch)

By the project's own contract (DEFINITION_OF_DONE.md), these require real users/traces and are not claimed as done:

Reproduce these claims

Every number on this page regenerates:

python benchmarks/recall_corpus.py        # 38/50 = 76%, per-class + false-negative list
python benchmarks/build_benign_corpus.py  # 11 confirmed FP (outside a documented per-repo allowlist) / 54 repos / 9,520 files
python -m pytest tests/ -q                # 572 passing