pyrograph 0.1.0

GPU-accelerated taint analysis for supply chain malware detection
Documentation
use super::*;
use crate::labels::{label_node, SanitizerDef, SinkDef, SourceDef};

pub fn default_label_set() -> LabelSet {
    LabelSet {
        sources: vec![
            // ── Credentials & Environment ─────────────────────────────
            SourceDef { id: "process-env".into(), pattern: "process.env".into(), category: "credential".into() },
            SourceDef { id: "process-argv".into(), pattern: "process.argv".into(), category: "cli".into() },
            SourceDef { id: "process-stdin".into(), pattern: "process.stdin".into(), category: "cli".into() },
            SourceDef { id: "process-platform".into(), pattern: "process.platform".into(), category: "system".into() },
            // Note: process.cwd() is NOT a source — it's a path, not a secret.
            // CLI tools legitimately use process.cwd() to write files.
            // ── File System Reads ─────────────────────────────────────
            SourceDef { id: "fs-read".into(), pattern: "fs.readFileSync".into(), category: "file".into() },
            SourceDef { id: "fs-read2".into(), pattern: "fs.readFile".into(), category: "file".into() },
            SourceDef { id: "fs-readdir".into(), pattern: "fs.readdirSync".into(), category: "file".into() },
            SourceDef { id: "fs-readdir2".into(), pattern: "fs.readdir".into(), category: "file".into() },
            // ── Buffers & Decoding ────────────────────────────────────
            SourceDef { id: "buffer-from".into(), pattern: "Buffer.from".into(), category: "buffer".into() },
            SourceDef { id: "atob".into(), pattern: "atob".into(), category: "buffer".into() },
            SourceDef { id: "btoa".into(), pattern: "btoa".into(), category: "buffer".into() },
            // ── Network Input (response data is tainted) ─────────────
            SourceDef { id: "net-socket".into(), pattern: "net.Socket".into(), category: "network-input".into() },
            SourceDef { id: "http-create-server".into(), pattern: "http.createServer".into(), category: "http".into() },
            SourceDef { id: "https-create-server".into(), pattern: "https.createServer".into(), category: "http".into() },
            // Network calls that return response data — the response is tainted
            // because it comes from an untrusted remote server. This catches the
            // "fetch malicious payload → eval" supply chain attack pattern.
            SourceDef { id: "https-get-response".into(), pattern: "https.get".into(), category: "network-input".into() },
            SourceDef { id: "http-get-response".into(), pattern: "http.get".into(), category: "network-input".into() },
            SourceDef { id: "https-request-response".into(), pattern: "https.request".into(), category: "network-input".into() },
            SourceDef { id: "http-request-response".into(), pattern: "http.request".into(), category: "network-input".into() },
            // Note: fetch() response taint is handled by .then() callback wiring,
            // not by making fetch a source. Making fetch a source causes FPs on
            // legitimate fetch().then(res => res.json()).then(json => use(json)).
            SourceDef { id: "net-connect-response".into(), pattern: "net.connect".into(), category: "network-input".into() },
            SourceDef { id: "request-body".into(), pattern: "request.body".into(), category: "http".into() },
            SourceDef { id: "request-query".into(), pattern: "request.query".into(), category: "http".into() },
            SourceDef { id: "request-params".into(), pattern: "request.params".into(), category: "http".into() },
            SourceDef { id: "request-headers".into(), pattern: "request.headers".into(), category: "http".into() },
            SourceDef { id: "request-cookies".into(), pattern: "request.cookies".into(), category: "http".into() },
            // ── System Information ────────────────────────────────────
            SourceDef { id: "os-hostname".into(), pattern: "os.hostname".into(), category: "system".into() },
            SourceDef { id: "os-userinfo".into(), pattern: "os.userInfo".into(), category: "system".into() },
            SourceDef { id: "os-platform".into(), pattern: "os.platform".into(), category: "system".into() },
            SourceDef { id: "os-networkinterfaces".into(), pattern: "os.networkInterfaces".into(), category: "system".into() },
            SourceDef { id: "os-cpus".into(), pattern: "os.cpus".into(), category: "system".into() },
            // ── Crypto ───────────────────────────────────────────────
            SourceDef { id: "crypto-random-bytes".into(), pattern: "crypto.randomBytes".into(), category: "crypto".into() },
            // ── npm Scripts ──────────────────────────────────────────
            SourceDef { id: "npm-preinstall".into(), pattern: "preinstall".into(), category: "npm-script".into() },
            SourceDef { id: "npm-postinstall".into(), pattern: "postinstall".into(), category: "npm-script".into() },
            SourceDef { id: "npm-install".into(), pattern: "install".into(), category: "npm-script".into() },
            // ── Sensitive File Paths (as string literals in code) ─────
            SourceDef { id: "file-npmrc".into(), pattern: ".npmrc".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-ssh-dir".into(), pattern: ".ssh/".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-wallet".into(), pattern: "wallet.dat".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-aws-creds".into(), pattern: ".aws/credentials".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-docker-config".into(), pattern: ".docker/config.json".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-kube-config".into(), pattern: ".kube/config".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-gitconfig".into(), pattern: ".gitconfig".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-bash-history".into(), pattern: ".bash_history".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-chrome-cookies".into(), pattern: "Cookies".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-chrome-login".into(), pattern: "Login Data".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-passwd".into(), pattern: "/etc/passwd".into(), category: "sensitive-file".into() },
            SourceDef { id: "file-shadow".into(), pattern: "/etc/shadow".into(), category: "sensitive-file".into() },
            // ── Shell commands as sources ────────────────────────────
            SourceDef { id: "shell-curl".into(), pattern: "curl".into(), category: "shell".into() },
            SourceDef { id: "shell-wget".into(), pattern: "wget".into(), category: "shell".into() },
            SourceDef { id: "shell-xmrig".into(), pattern: "xmrig".into(), category: "shell".into() },
            SourceDef { id: "shell-schtasks".into(), pattern: "schtasks".into(), category: "shell".into() },
            SourceDef { id: "shell-reg-add".into(), pattern: "reg add".into(), category: "shell".into() },
            SourceDef { id: "shell-powershell".into(), pattern: "powershell".into(), category: "shell".into() },
        ],
        sinks: vec![
            // ── Code Execution (Critical) ─────────────────────────────
            SinkDef { id: "eval".into(), pattern: "eval".into(), category: "exec".into() },
            SinkDef { id: "function-ctor".into(), pattern: "Function".into(), category: "exec".into() },
            SinkDef { id: "exec".into(), pattern: "child_process.exec".into(), category: "exec".into() },
            SinkDef { id: "exec-sync".into(), pattern: "child_process.execSync".into(), category: "exec".into() },
            SinkDef { id: "spawn".into(), pattern: "child_process.spawn".into(), category: "exec".into() },
            SinkDef { id: "spawn-sync".into(), pattern: "child_process.spawnSync".into(), category: "exec".into() },
            SinkDef { id: "exec-file".into(), pattern: "child_process.execFile".into(), category: "exec".into() },
            SinkDef { id: "exec-file-sync".into(), pattern: "child_process.execFileSync".into(), category: "exec".into() },
            SinkDef { id: "fork".into(), pattern: "child_process.fork".into(), category: "exec".into() },
            SinkDef { id: "vm-run-in-new-context".into(), pattern: "vm.runInNewContext".into(), category: "exec".into() },
            SinkDef { id: "vm-run-in-context".into(), pattern: "vm.runInThisContext".into(), category: "exec".into() },
            SinkDef { id: "vm-script-run".into(), pattern: "vm.Script".into(), category: "exec".into() },
            SinkDef { id: "set-timeout".into(), pattern: "setTimeout".into(), category: "exec".into() },
            SinkDef { id: "set-interval".into(), pattern: "setInterval".into(), category: "exec".into() },
            SinkDef { id: "set-immediate".into(), pattern: "setImmediate".into(), category: "exec".into() },
            SinkDef { id: "process-next-tick".into(), pattern: "process.nextTick".into(), category: "exec".into() },
            SinkDef { id: "module-resolve-filename".into(), pattern: "module._resolveFilename".into(), category: "exec".into() },
            // ── Network Exfiltration (High) ───────────────────────────
            SinkDef { id: "fetch".into(), pattern: "fetch".into(), category: "network".into() },
            SinkDef { id: "http-request".into(), pattern: "http.request".into(), category: "network".into() },
            SinkDef { id: "https-request".into(), pattern: "https.request".into(), category: "network".into() },
            SinkDef { id: "https-get".into(), pattern: "https.get".into(), category: "network".into() },
            SinkDef { id: "http-get".into(), pattern: "http.get".into(), category: "network".into() },
            SinkDef { id: "net-connect".into(), pattern: "net.connect".into(), category: "network".into() },
            SinkDef { id: "net-create-connection".into(), pattern: "net.createConnection".into(), category: "network".into() },
            SinkDef { id: "dns-lookup".into(), pattern: "dns.lookup".into(), category: "network".into() },
            SinkDef { id: "dns-resolve".into(), pattern: "dns.resolve".into(), category: "network".into() },
            SinkDef { id: "dns-resolve4".into(), pattern: "dns.resolve4".into(), category: "network".into() },
            SinkDef { id: "dns-resolve6".into(), pattern: "dns.resolve6".into(), category: "network".into() },
            SinkDef { id: "dns-resolveCname".into(), pattern: "dns.resolveCname".into(), category: "network".into() },
            SinkDef { id: "dns-resolveMx".into(), pattern: "dns.resolveMx".into(), category: "network".into() },
            SinkDef { id: "dns-resolveTxt".into(), pattern: "dns.resolveTxt".into(), category: "network".into() },
            SinkDef { id: "dns-resolveNs".into(), pattern: "dns.resolveNs".into(), category: "network".into() },
            SinkDef { id: "dns-resolveSrv".into(), pattern: "dns.resolveSrv".into(), category: "network".into() },
            SinkDef { id: "websocket-send".into(), pattern: "WebSocket".into(), category: "network".into() },
            SinkDef { id: "xmlhttprequest".into(), pattern: "XMLHttpRequest".into(), category: "network".into() },
            // Note: axios, got, node-fetch are NOT sinks — they're high-level HTTP
            // clients where env-based configuration (baseURL, headers) is standard
            // practice. Only low-level Node.js APIs (fetch, http.request, etc) are
            // sinks because they directly send data over the network.
            SinkDef { id: "navigator-sendbeacon".into(), pattern: "navigator.sendBeacon".into(), category: "network".into() },
            SinkDef { id: "net-create-server".into(), pattern: "net.createServer".into(), category: "network".into() },
            // ── File System Write (Medium) ────────────────────────────
            SinkDef { id: "fs-write-file-sync".into(), pattern: "fs.writeFileSync".into(), category: "file".into() },
            SinkDef { id: "fs-write-file".into(), pattern: "fs.writeFile".into(), category: "file".into() },
            SinkDef { id: "fs-append-file".into(), pattern: "fs.appendFile".into(), category: "file".into() },
            SinkDef { id: "fs-append-file-sync".into(), pattern: "fs.appendFileSync".into(), category: "file".into() },
            SinkDef { id: "fs-create-write-stream".into(), pattern: "fs.createWriteStream".into(), category: "file".into() },
            SinkDef { id: "fs-copy-file".into(), pattern: "fs.copyFile".into(), category: "file".into() },
            SinkDef { id: "fs-rename".into(), pattern: "fs.rename".into(), category: "file".into() },
            // ── Shell execution markers ──────────────────────────────
            SinkDef { id: "shell-sh".into(), pattern: "shell:sh".into(), category: "exec".into() },
            SinkDef { id: "shell-bash".into(), pattern: "shell:bash".into(), category: "exec".into() },
            SinkDef { id: "shell-eval".into(), pattern: "shell:eval".into(), category: "exec".into() },
            SinkDef { id: "shell-exec".into(), pattern: "shell:exec".into(), category: "exec".into() },
            // ── Vulnerability Sinks (safe with taint coloring) ────────
            // These only fire when HTTP user input (request.body/query/params)
            // flows to them, NOT when credential/env sources flow to them.
            // Taint coloring in cpu/mod.rs checks (source_category, sink_category)
            // combinations to prevent FPs like process.env.PORT → db.query().
            SinkDef { id: "sql-query".into(), pattern: "db.query".into(), category: "sql".into() },
            SinkDef { id: "sql-raw".into(), pattern: "sequelize.query".into(), category: "sql".into() },
            SinkDef { id: "sql-knex-raw".into(), pattern: "knex.raw".into(), category: "sql".into() },
            SinkDef { id: "sql-exec".into(), pattern: "connection.execute".into(), category: "sql".into() },
            SinkDef { id: "sql-pool-query".into(), pattern: "pool.query".into(), category: "sql".into() },
            SinkDef { id: "xss-res-send".into(), pattern: "res.send".into(), category: "xss".into() },
            SinkDef { id: "xss-res-write".into(), pattern: "res.write".into(), category: "xss".into() },
            SinkDef { id: "xss-innerhtml".into(), pattern: "innerHTML".into(), category: "xss".into() },
            SinkDef { id: "xss-document-write".into(), pattern: "document.write".into(), category: "xss".into() },
        ],
        sanitizers: vec![
            // ── Type Coercion (breaks taint by converting to primitive) ──
            // Note: JSON.parse is NOT a sanitizer — it parses JSON but the data
            // retains its original values. JSON.parse(stolen_data) → data is still stolen.
            // Only true sanitizers that TRANSFORM data into a safe form belong here.
            SanitizerDef { id: "parse-int".into(), pattern: "parseInt".into() },
            SanitizerDef { id: "parse-float".into(), pattern: "parseFloat".into() },
            SanitizerDef { id: "number-ctor".into(), pattern: "Number".into() },
            SanitizerDef { id: "boolean-ctor".into(), pattern: "Boolean".into() },
            // ── Encoding (transforms data into safe representation) ──────
            SanitizerDef { id: "encode-uri-component".into(), pattern: "encodeURIComponent".into() },
            SanitizerDef { id: "encode-uri".into(), pattern: "encodeURI".into() },
            SanitizerDef { id: "escape".into(), pattern: "escape".into() },
            // ── XSS Sanitizers ───────────────────────────────────────────
            SanitizerDef { id: "validator-escape".into(), pattern: "validator.escape".into() },
            SanitizerDef { id: "validator-sanitize".into(), pattern: "validator.trim".into() },
            SanitizerDef { id: "dompurify-sanitize".into(), pattern: "DOMPurify.sanitize".into() },
            SanitizerDef { id: "xss".into(), pattern: "xss".into() },
            SanitizerDef { id: "sanitize-html".into(), pattern: "sanitizeHtml".into() },
            SanitizerDef { id: "he-encode".into(), pattern: "he.encode".into() },
            SanitizerDef { id: "lodash-escape".into(), pattern: "_.escape".into() },
            // Note: crypto hashing is NOT sanitization for supply chain malware.
            // Attackers hash env vars (crypto.createHash('md5').update(env.SECRET))
            // to create unique victim identifiers. The hash output is still sensitive.
            // bcrypt/argon2 for password storage IS safe, but at internet scale we
            // can't distinguish password storage from victim fingerprinting.
            // ── Logging (output goes to local logs, not attacker) ────────
            SanitizerDef { id: "console-log".into(), pattern: "console.log".into() },
            SanitizerDef { id: "console-error".into(), pattern: "console.error".into() },
            SanitizerDef { id: "console-warn".into(), pattern: "console.warn".into() },
            SanitizerDef { id: "console-info".into(), pattern: "console.info".into() },
            SanitizerDef { id: "console-debug".into(), pattern: "console.debug".into() },
            // ── Path Sanitizers (prevent path traversal) ────────────────
            SanitizerDef { id: "path-basename".into(), pattern: "path.basename".into() },
            SanitizerDef { id: "path-normalize".into(), pattern: "path.normalize".into() },
            // ── HTML Escaping ───────────────────────────────────────────
            SanitizerDef { id: "escape-html".into(), pattern: "escapeHtml".into() },
            SanitizerDef { id: "entities-encode".into(), pattern: "entities.encode".into() },
        ],
    }
}

pub(crate) fn apply_labels(graph: &mut TaintGraph, label_set: &LabelSet) {
    let count = graph.node_count();
    for id in 0..count as u32 {
        if let Some(node) = graph.node(id) {
            let label = label_node(label_set, &node.name)
                .or_else(|| node.alias.as_deref().and_then(|alias| label_node(label_set, alias)));
            if let Some(label) = label {
                if let Some(n) = graph.node_mut(id) {
                    n.label = Some(label);
                }
            }
        }
    }
}