● Zion Boggan
repos/Cyclops V2/brain/brain.py
zionboggan.com ↗
131 lines · python
History for this file →
1
"""
2
Cyclops v2 - Pillar 2, the novelty brain.
3
 
4
Pipeline on a target source tree:
5
  1. (cache) skip if this commit already analyzed for this phase.
6
  2. semgrep fast-pass  -> candidate high-severity sinks (cheap, broad).
7
  3. joern dataflow      -> which candidate sinks are REACHABLE from untrusted
8
                            sources (source->sink taint). Reachable path = a
9
                            HYPOTHESIS written to the graph. NOT a finding.
10
  4. hand hypotheses to the oracle (Pillar 3) for execution proof.
11
 
12
A reachable dataflow path is a hypothesis. Only oracle.py can turn it into a
13
finding, and only memory.promote_to_finding (which requires a Proof) records it.
14
"""
15
import os, sys, json, subprocess, glob
16
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
17
from memory import Memory
18
 
19
CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME", "/opt/cyclops-v2")
20
VENV_PY      = os.environ.get("CYCLOPS_PY", "/opt/cyclops-venv/bin/python")
21
SEMGREP      = os.environ.get("CYCLOPS_SEMGREP", "/opt/cyclops-venv/bin/semgrep")
22
JOERN        = os.environ.get("CYCLOPS_JOERN", "/opt/joern/joern-cli/joern")
23
QUERY_DIR    = os.path.join(CYCLOPS_HOME, "brain", "joern-queries")
24
RULE_DIR     = os.path.join(CYCLOPS_HOME, "brain", "semgrep-rules")
25
 
26
QUERY_SINKCLASS = {
27
    "c-memory-corruption.sc":  ("memory-corruption", "RCE via Memory Corruption"),
28
    "deserialization-rce.sc":  ("deserialization",   "RCE via Deserialization"),
29
    "command-injection.sc":    ("command-injection", "RCE via Command Injection"),
30
    "path-traversal.sc":       ("path-traversal",    "Arbitrary File Read/Write"),
31
    "ssrf.sc":                 ("ssrf",              "SSRF chains to Critical"),
32
    "template-injection.sc":   ("template-injection","RCE via Template Injection"),
33
    "js-electron-rce.sc":      ("electron-rce",      "One-Click/Zero-Click RCE"),
34
}
35
 
36
 
37
def semgrep_pass(src):
38
    """Fast broad pass. Returns list of candidate sink dicts."""
39
    if not os.path.isdir(RULE_DIR) or not os.listdir(RULE_DIR):
40
        return []
41
    try:
42
        out = subprocess.run(
43
            [SEMGREP, "--quiet", "--json", "--config", RULE_DIR, src],
44
            capture_output=True, text=True, timeout=1200)
45
        data = json.loads(out.stdout or "{}")
46
    except Exception as e:
47
        print(f"[brain] semgrep error: {e}")
48
        return []
49
    cands = []
50
    for r in data.get("results", []):
51
        cands.append({
52
            "file": r.get("path"),
53
            "line": r.get("start", {}).get("line", 0),
54
            "rule": r.get("check_id"),
55
            "msg": r.get("extra", {}).get("message", ""),
56
        })
57
    print(f"[brain] semgrep: {len(cands)} candidate sinks")
58
    return cands
59
 
60
 
61
def joern_query(src, script, project):
62
    """Run one joern dataflow script. Returns parsed FINDING lines."""
63
    path = os.path.join(QUERY_DIR, script)
64
    if not os.path.exists(path):
65
        return []
66
    try:
67
        out = subprocess.run(
68
            [JOERN, "--script", path, "--param", f"inputPath={src}",
69
             "--param", f"projectName={project}"],
70
            capture_output=True, text=True, timeout=3600)
71
    except subprocess.TimeoutExpired:
72
        print(f"[brain] joern timeout on {script}")
73
        return []
74
    findings = []
75
    for ln in (out.stdout or "").splitlines():
76
        if ln.startswith("FINDING:"):
77
            try:
78
                body = ln[len("FINDING:"):].strip()
79
                locpart, rest = body.split(" ", 1)
80
                fp, _, lno = locpart.rpartition(":")
81
                kv = {}
82
                for tok in ("class", "source", "sink"):
83
                    if tok + "=" in rest:
84
                        seg = rest.split(tok + "=", 1)[1]
85
                        for nxt in (" class=", " source=", " sink="):
86
                            if nxt.strip(" =") != tok and nxt in seg:
87
                                seg = seg.split(nxt)[0]
88
                        kv[tok] = seg.strip()
89
                findings.append({"file": fp, "line": int(lno) if lno.isdigit() else 0,
90
                                 "cls": kv.get("class", ""), "source": kv.get("source", ""),
91
                                 "sink": kv.get("sink", "")})
92
            except Exception:
93
                continue
94
    return findings
95
 
96
 
97
def analyze(repo, archetype, src, lang="", commit="", released_at=""):
98
    m = Memory()
99
    tid = m.upsert_target(repo, archetype, lang=lang, commit=commit,
100
                          released_at=released_at, status="ANALYZING")
101
    if m.analysis_done(tid, "brain"):
102
        print(f"[brain] {repo}@{commit} already analyzed (cache hit) - skipping")
103
        return tid, []
104
 
105
    semgrep_pass(src)
106
 
107
    hyps = []
108
    for script, (sink_class, impact) in QUERY_SINKCLASS.items():
109
        fs = joern_query(src, script, f"cyc_{archetype}")
110
        for f in fs:
111
            hid = m.add_hypothesis(tid, sink_class, f["file"], f["line"],
112
                                   f["source"], f["sink"], "joern")
113
            hyps.append({"hid": hid, "sink_class": sink_class, "impact": impact,
114
                         "file": f["file"], "line": f["line"],
115
                         "source": f["source"], "sink": f["sink"]})
116
        if fs:
117
            print(f"[brain] {script}: {len(fs)} reachable source->sink paths")
118
    m.mark_analysis_done(tid, "brain")
119
    m.enqueue("oracle", {"target": tid, "repo": repo, "src": src,
120
                         "archetype": archetype, "hypotheses": hyps})
121
    print(f"[brain] {repo}: {len(hyps)} HYPOTHESES queued for oracle proof")
122
    return tid, hyps
123
 
124
 
125
if __name__ == "__main__":
126
    if len(sys.argv) < 4:
127
        print("usage: brain.py <repo> <archetype> <src_path> [lang] [commit]")
128
        sys.exit(1)
129
    analyze(sys.argv[1], sys.argv[2], sys.argv[3],
130
            lang=sys.argv[4] if len(sys.argv) > 4 else "",
131
            commit=sys.argv[5] if len(sys.argv) > 5 else "")