| 1 | # Cyclops v2 - the static-analysis "novelty brain" |
| 2 | |
| 3 | This directory (`brain/joern-queries/`) plus its sibling (`brain/semgrep-rules/`) are the **novelty |
| 4 | brain** of Cyclops v2: the static layer that surfaces *candidate* source→sink dataflow paths to |
| 5 | high-severity sinks. It is the front of a three-stage pipeline. |
| 6 | |
| 7 | ``` |
| 8 | ┌──────────────────────┐ candidate sinks ┌───────────────────────┐ reachable paths ┌────────────────────┐ |
| 9 | │ 1. SEMGREP fast-pass │ ───────────────────▶ │ 2. JOERN dataflow │ ─────────────────▶ │ 3. DYNAMIC ORACLE │ |
| 10 | │ brain/semgrep-rules/ │ (cheap, per-file │ brain/joern-queries/ │ (source→sink │ oracle/ - crash / │ |
| 11 | │ │ regex/AST match) │ reachableByFlows │ reachability) │ shell / secret │ |
| 12 | └──────────────────────┘ └───────────────────────┘ └────────────────────┘ |
| 13 | SOURCE-shaped? SOURCE_CONFIRMED REPLICA_CONFIRMED → |
| 14 | (sink present) (path exists) LIVE_CANARY_CONFIRMED |
| 15 | ``` |
| 16 | |
| 17 | ## The one rule that governs this whole directory |
| 18 | |
| 19 | > **A reachable dataflow path is a HYPOTHESIS, never a finding.** |
| 20 | > Cyclops v1 reasoned its way from "the sink is reachable" to "this is exploitable" and wrote |
| 21 | > reports. v2 does not. Reachability ≠ exploitation. The **only** thing that promotes a candidate is |
| 22 | > an **execution artifact** produced by the dynamic oracle: |
| 23 | > an ASan/UBSan crash with a controlled fault, a popped shell / callback / command output, a |
| 24 | > retrieved secret, or a proven state change. See `vault/00-doctrine/OPERATING-CORE.md §2` and |
| 25 | > `vault/00-doctrine/SEVERITY-GATE.md`. |
| 26 | |
| 27 | Every `FINDING:` line a Joern script prints is labelled in the pipeline as **SOURCE_CONFIRMED at |
| 28 | best** - it names a *plausible endpoint* on a demonstrable path to one of the eight impact families. |
| 29 | It is the oracle's job to fire the family's oracle artifact. If the oracle cannot, the candidate is |
| 30 | **DISPROVEN** and logged to `vault/04-cleared-axes/` - it never becomes a report. |
| 31 | |
| 32 | ## Why two stages (semgrep then Joern) |
| 33 | |
| 34 | - **Semgrep (`brain/semgrep-rules/`)** is the cheap first pass. It runs per-file, no CPG build, and |
| 35 | answers "does this repo even *contain* a dangerous sink of class X?" It is deliberately |
| 36 | **HIGH-severity sinks only** - no hardening/lint noise (missing headers, weak ciphers, etc.). Each |
| 37 | rule's `message` names the impact gate (which SEVERITY-GATE family) and carries `metadata.cwe`, |
| 38 | `metadata.confidence`, `metadata.archetype`, and `metadata.impact`. A semgrep hit is a *candidate |
| 39 | sink* - it tells the orchestrator which Joern script is worth the heavier CPG run. |
| 40 | - **Joern (`brain/joern-queries/`)** is the confirmation pass. For each candidate sink class it |
| 41 | builds a CPG and runs `reachableByFlows` to ask "is there an actual taint path from an untrusted |
| 42 | **source** to this **sink**?" A path here is a real source→sink chain, much stronger than sink |
| 43 | presence - but still only a hypothesis until the oracle proves impact. |
| 44 | |
| 45 | Semgrep narrows *which* sink classes exist; Joern proves *reachability*; the oracle proves *impact*. |
| 46 | |
| 47 | ## Sink classes (organized by target archetype) |
| 48 | |
| 49 | Archetypes live in `vault/01-archetypes/`: `ml-model-loaders`, `ai-agent-mcp`, `devops-infra`, |
| 50 | `electron-desktop`, `memory-unsafe-parsers`, `cicd-supplychain`. |
| 51 | |
| 52 | | Sink class | Joern script | Semgrep rule | Primary archetypes | Gate family | |
| 53 | |---|---|---|---|---| |
| 54 | | C/C++ memory corruption | `c-memory-corruption.sc` | *(use compiler+ASan; see note)* | memory-unsafe-parsers, ml-model-loaders | F1, F7 | |
| 55 | | Untrusted deserialization | `deserialization-rce.sc` | `deserialization.yaml`, `ml-unsafe-load.yaml` | ml-model-loaders, ai-agent-mcp | F1 | |
| 56 | | Command / code injection | `command-injection.sc` | `command-injection.yaml` | devops-infra, ai-agent-mcp, cicd-supplychain | F1 | |
| 57 | | Path traversal / zip-slip | `path-traversal.sc` | `path-traversal.yaml` | devops-infra, ml-model-loaders | F7 | |
| 58 | | SSRF | `ssrf.sc` | `ssrf.yaml` | ai-agent-mcp, devops-infra, ml-model-loaders | F7 | |
| 59 | | Template injection (SSTI) | `template-injection.sc` | `template-injection.yaml` | ai-agent-mcp, devops-infra | F1 | |
| 60 | | Electron renderer→main RCE | `js-electron-rce.sc` | `electron.yaml` | electron-desktop | F1 | |
| 61 | |
| 62 | *(C memory-corruption has no semgrep rule by design - sink *presence* of `strcpy`/`memcpy` is too |
| 63 | noisy to be useful triage; the value is the dataflow path plus the ASan oracle. The Joern script |
| 64 | still prints `CANDIDATE_SINK` lines if you want the raw sink inventory.)* |
| 65 | |
| 66 | ## How the orchestrator uses this |
| 67 | |
| 68 | 1. **Recon → archetype.** Classify the target (an ML loader? an MCP agent? an Electron app?). This |
| 69 | selects the relevant semgrep rule files and Joern scripts. |
| 70 | 2. **Semgrep fast-pass.** `semgrep --config brain/semgrep-rules/ <target>` (or the subset for the |
| 71 | archetype). Candidate sinks → which Joern scripts to run. |
| 72 | 3. **Joern confirm.** Run each indicated script (`_runner.md`). Collect `FINDING:` lines = reachable |
| 73 | source→sink paths. Prioritize: `[metadata-reachable]` SSRF, `shell=True`-confirmed cmdi, |
| 74 | no-normalization path-traversal, `trust_remote_code=True` model loads. |
| 75 | 4. **Hand to the oracle.** Each reachable path becomes an oracle task: build a replica |
| 76 | (`vault/00-doctrine/OPERATING-CORE.md` replica-first rule), drive the sink with a crafted input, |
| 77 | and look for the family's artifact. Artifact → REPLICA_CONFIRMED. No artifact → DISPROVEN + log. |
| 78 | 5. **Only the oracle opens a report.** And only inside authorized scope may a LIVE_CANARY step follow. |
| 79 | |
| 80 | ## Output contract (both layers) |
| 81 | |
| 82 | - Semgrep: standard semgrep JSON/SARIF. `metadata.impact` + `message` carry the gate mapping. |
| 83 | - Joern: parseable stdout - `CANDIDATE_SINK`, `CANDIDATE_SINKS[..]=n`, `FINDING`, |
| 84 | `DATAFLOW_PATHS[..]=n`, `SCAN_COMPLETE[..]`. See `_runner.md` for the full grammar and a JSONL |
| 85 | parser. |
| 86 | |
| 87 | ## Authorization reminder |
| 88 | |
| 89 | Per `OPERATING-CORE §1`, these queries run only against **consenting targets** (owned infra, |
| 90 | local replicas, disposable labs, or explicitly in-scope bounty assets). Static analysis of a repo is |
| 91 | read-only, but the candidates it produces drive the oracle - and the oracle only ever detonates |
| 92 | against an owned replica first. Never point the pipeline at a non-consenting third party. |