Skip to content

feat(analyzer): implement MCP rug-pull detection (RP1-RP3)#125

Open
tcconnally wants to merge 1 commit into
NVIDIA:mainfrom
tcconnally:feat/mcp-rug-pull-analyzer
Open

feat(analyzer): implement MCP rug-pull detection (RP1-RP3)#125
tcconnally wants to merge 1 commit into
NVIDIA:mainfrom
tcconnally:feat/mcp-rug-pull-analyzer

Conversation

@tcconnally

Copy link
Copy Markdown
Contributor

Summary

Implements the MCP rug-pull analyzer that was previously a TODO stub (mcp_rug_pull.py — SADD B.3.3). Adds three detection rules for supply-chain rug-pull risks in agent skills.

New Detection Rules

RP1: Unpinned MCP Server References (MEDIUM)

Detects MCP server/package references without version pinning:

  • npx @scope/server (no @version)
  • uvx package-name (no ==version)
  • pip install mcp-server (no ==version)
  • docker run org/image (no :tag or @sha256:)

Uses a two-step approach: find command references, then check line context for version pinning indicators. This avoids the backtracking false positives that simpler regex-only approaches produce.

RP2: Permission Pre-Staging (LOW)

Detects manifest language suggesting future permission/capability expansion — a common pre-staging tactic for rug-pull attacks.

RP3: Version Unpinned (LOW)

Detects wildcard (*, latest, any) or broad (>=, ^) version constraints in manifest metadata that allow silent major-version updates.

Testing

  • 8 new unit tests covering all three rules (detection, false-positive avoidance, empty state)
  • All 629 existing tests pass
  • Test file: tests/test_mcp_rug_pull.py

Design Notes

  • RP1 uses two-step detection (find + verify) rather than single-regex, avoiding the backtracking issues that cause regex-based version-pin detection to produce false positives on compound names like @scope/mcp-server@1.2.3.
  • RP2 is intentionally low-confidence (0.60-0.70) — permission pre-staging language is an indicator, not definitive proof.
  • RP3 reads the version key directly from manifest dict rather than regex on str() output, which is more robust against different dict serialization formats.

Replaces the mcp_rug_pull analyzer stub with full RP1-RP3 detection.

RP1: Unpinned MCP server references — detects npx, uvx, pip install,
and docker commands that reference MCP servers/packages without
version pinning (@Version, ==version, :tag, @sha256:).

RP2: Permission pre-staging — detects manifest language suggesting
future permission/capability expansion (rug-pull pre-staging).

RP3: Version unpinned — detects wildcard/broad version constraints
in manifest version field allowing silent major-version updates.

Includes 8 unit tests covering all three rules.

Previously tracked as TODO(SADD B.3.3) stub.

Signed-off-by: Perseus Computing <51974392+tcconnally@users.noreply.github.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — implements RP1–RP3 in place of the previous stub, with 8 passing unit tests. The RP1 (unpinned npx/uvx/pip/docker references) and RP3 (unpinned/broad skill version) logic is sound, the version-pin guard works, and the node returns Finding objects consistent with the rest of the pipeline.

A few non-blocking suggestions (one worth fixing before merge):

  • Likely lint failure: in _check_rp1, line_start is assigned in the npx/uvx/pip loops but never used (_find_line() computes the line). Unused locals trip the default Pyflakes/ruff F841 rule — please remove them.
  • Manifest RP1 has no pin guard: the file-content loops skip pinned references via _VERSION_PIN_RE, but the _RP1_NPX_CMD.finditer(manifest_text) block does not, so a properly pinned npx ...@1.2.3 in the manifest would still be flagged. Consider applying the same guard for consistency.
  • RP2 is broader than its docstring: the docstring describes detecting permissions that exceed current code usage, but the implementation just regex-matches the presence of a permissions array (0.60) or expansion-language, so it will fire on essentially any manifest that declares permissions. Either narrow the pattern or align the docstring with the actual behavior.
  • Labeling: npx/uvx/docker matches are reported as "MCP server referenced without pinned version" even for non-MCP commands (only the pip path filters on mcp). Unpinned external commands are a legitimate rug-pull signal, but the wording could be more generic to avoid mislabeling.

None of these block the core detection, which is a clear improvement over the stub.

@rng1995

rng1995 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Please resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants