Skip to content

fix(scanner): spec-parsing + version-pin correctness in package-runner source resolution (MCP-2445)#677

Merged
Dumbris merged 4 commits into
mainfrom
mcp-2445-scanner-spec-parsing
Jun 15, 2026
Merged

fix(scanner): spec-parsing + version-pin correctness in package-runner source resolution (MCP-2445)#677
Dumbris merged 4 commits into
mainfrom
mcp-2445-scanner-spec-parsing

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a cluster of correctness bugs in scanner source resolution (Codex scanner audit, PRs #653#670). The scanner mis-parsed several package-runner launch commands and ignored requested version pins, so it scanned the wrong source (or degraded to tool-definitions-only).

Refs against origin/main.

Bugs fixed

  • Subcommand runners mis-parsed. pipx run X"run", pnpm dlx X"dlx" (returned the subcommand, not the package).
  • pnpm dead branch. resolveFromPackageFetch dispatched pnpm but isPackageRunnerCommand excluded it → pnpm never reached the fetch.
  • Version pin ignored. resolveNpxCache / findUvxArchiveDir / findContainerTargetDir picked newest-mtime, ignoring the requested version.
  • --with mis-handled. resolveUvxCache only honored --from; uvx --with <dep> <pkg> resolved the extra dep, not the target.
  • No fetch timeout. A hung/throttled registry could stall the scan indefinitely.
  • Unpinned cisco image. docker/scanners/cisco/Dockerfile installed cisco-ai-mcp-scanner unpinned.

Changes

  • New command-aware runnerPackageSpec() parser: subcommand runners (pipx run, pnpm dlx, yarn dlx, bun x), package-naming flags (npx --package/-p, uvx --from), and skips non-target value flags (uvx --with <dep>, -p/--python <ver>, -c <cmd>). Replaces firstPackageArg and the ad-hoc parse loops in resolveNpxCache / resolveUvxCache / npxTargetPackage / uvxTargetPackage (host/container parity).
  • isPackageRunnerCommand now includes pnpm/yarn/bun; npm fetch path dispatches them.
  • Exact version pin honored in local caches (npx package.json version, uvx .dist-info version, container npx bucket), falling back to newest/first when unpinned or unmatched.
  • 90s bounded timeout around the published-source fetch (download + extract).
  • Pinned cisco-ai-mcp-scanner==4.7.3 (reproducible, rug-pull guard).

Tests (TDD)

  • TestRunnerPackageSpec — table tests: pipx run X, pnpm dlx X, yarn dlx X, bun x X, npx -p, uvx --from, uvx --with, uvx -p/--pythonX.
  • TestResolveNpxCacheHonorsPinnedVersion, TestResolveUvxCacheArchiveHonorsPinnedVersion — pinned version selected over newer mismatch.
  • TestResolveUvxCacheWithFlag--with <dep> does not shadow the target.
  • TestNpxContainerLocateScript — generated /bin/sh locate script run on the host: version pin selects the matching bucket; no pin returns first match.
go test ./internal/security/... -race   # green
golangci-lint run --config .github/.golangci.yml ./internal/security/scanner/   # 0 issues

Docs updated in the same PR (docs/features/security-scanner-plugins.md).

Related MCP-2445

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 15, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 312b263
Status: ✅  Deploy successful!
Preview URL: https://5d7db630.mcpproxy-docs.pages.dev
Branch Preview URL: https://mcp-2445-scanner-spec-parsin.mcpproxy-docs.pages.dev

View logs

@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

CI note — pre-existing Server Edition failure is NOT from this PR

The Server Edition check is red, but it is a pre-existing build break on origin/main, unrelated to this change:

internal/teams/broker/oauth_connector.go:95: undefined: CredentialStore
internal/teams/broker/oauth_connector.go:184: undefined: UpstreamCredential
internal/teams/broker/oauth_connector_test.go:46: undefined: openTestDB
...
FAIL github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/broker [build failed]

CredentialStore / UpstreamCredential are referenced but never defined anywhere in internal/teams/ — introduced by #602 (spec 074, per-user OAuth broker). This PR touches only internal/security/scanner/, docs/, and docker/scanners/cisco/, none of which are server-edition-tagged. go build -tags server ./cmd/mcpproxy and go test -tags server ./internal/security/... are green locally on this branch.

Server Edition is not a required status check, so it does not gate merge. Flagging it separately for a follow-up fix (out of scope for MCP-2445).

Required checks status: Lint ✓, Build (ubuntu-latest) ✓, Build Frontend ✓, Validate PR title ✓, Verify OpenAPI ✓, swift-test/settings-parity skipping. Unit Tests completing. The scanner docker images (cisco/ramparts/snyk) build green, incl. cisco on linux/amd64+arm64 (validates the version pin).

@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 83.42857% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/security/scanner/source_resolver.go 81.19% 15 Missing and 7 partials ⚠️
internal/security/scanner/package_fetch.go 87.93% 5 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: mcp-2445-scanner-spec-parsing

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27539944945 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

codex review (gpt-5.5) → REQUEST_CHANGES (independently re-verified)

Blocking bug: runnerPackageSpec does not require the subcommand keyword for subcommand-style runners. For pnpm/yarn/pipx/bun, when dlx/run/x is absent the first positional is returned anyway: pnpm start → package "start", bun server.ts"server.ts". Combined with isPackageRunnerCommand now returning true for bare pnpm/yarn/bun, a normal non-runner launch is mis-classified and the scanner fetches/scans the wrong published package (false coverage + typosquat-fetch risk). Same flaw in the resolveNpxCache cache-resolve path.

Fix: only treat the first positional as a package spec when the runner subcommand keyword is present (pipx run/pnpm dlx/yarn dlx/bun x); isPackageRunnerCommand can't classify pnpm/yarn/bun by exe name alone. Add tests for pnpm start, bun server.ts.

@Dumbris Dumbris force-pushed the mcp-2445-scanner-spec-parsing branch from 9a7f874 to 0041dfd Compare June 15, 2026 09:22
@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

codex RE-REVIEW (gpt-5.5) → REQUEST_CHANGES (round 2 — the pnpm startstart case is handled, but related holes remain)

  1. -p/--package flag bypass (concrete blocker): runnerPackageSpec processes the package flag (-p <pkg>) before enforcing the dlx/x subcommand, so pnpm -p start / bun -p server.ts still produce a package spec without the required runner subcommand. Enforce the subcommand keyword first, then parse flags.
  2. Classification still by-name: isPackageRunnerCommand classifies bare pnpm/yarn/bun by command name alone, so pnpm start / bun server.ts stay on package-runner paths and rely on later parser failure rather than correct classification. Make classification argument-aware (a runner only when its subcommand is present).
  3. Cache version-pin fallback: when an exact pin is requested but not present in cache, it still falls back to a mismatched cached package — suppressing the published fetch of the requested version (false scanner coverage). Don't substitute a different version.

Add tests for pnpm -p start, bun -p server.ts, and the version-pin-miss case.

Dumbris added 3 commits June 15, 2026 13:00
…urce resolution

The scanner mis-parsed several package-runner launch commands and ignored
requested version pins, so it scanned the wrong source (or none).

- Add runnerPackageSpec(): a command-aware parser that understands subcommand
  runners (`pipx run X`, `pnpm dlx X`, `yarn dlx X`, `bun x X`), package-naming
  flags (`npx --package/-p X`, `uvx --from X`), and skips non-target value flags
  (`uvx --with <dep>`, `-p/--python <ver>`, `-c <cmd>`). Replaces firstPackageArg
  and the ad-hoc loops in resolveNpxCache/resolveUvxCache/npx+uvxTargetPackage.
- Enable pnpm/yarn in isPackageRunnerCommand so the pnpm fetch dispatch (a dead
  branch before) actually runs; wire bun/yarn through the npm fetch path too.
- Honor an exact version pin in the local caches: resolveNpxCache prefers the
  package.json-version match, findUvxArchiveDir prefers the .dist-info-version
  match, and the container npx locate script prefers the matching bucket — all
  falling back to newest/first when unpinned or unmatched.
- Bound the published-source fetch (download + extract) with a 90s timeout so a
  hung registry degrades to tool-definitions-only instead of stalling the scan.
- Pin cisco-ai-mcp-scanner==4.7.3 in the cisco scanner Dockerfile (reproducible,
  rug-pull guard).

TDD: table tests for the parser (pipx run / pnpm dlx / uvx --with / npx -p),
version-pinned cache selection (npx + uvx archive), and the container locate
script (run under /bin/sh). `go test ./internal/security/... -race` green.

Related MCP-2445
Codex re-review (MCP-2445): runnerPackageSpec returned the first positional
even when the runner's required subcommand keyword was absent, so with
pnpm/yarn/bun gated as package runners:

  pnpm start      -> "start"
  bun server.ts   -> "server.ts"
  pipx install x  -> "install"

The wrong token was then fetched/scanned = false coverage + typosquat risk.

Fix: for subcommand-style runners (pnpm/yarn `dlx`, `bun x`, `pipx run`), a
package spec is resolved ONLY when the keyword is present. A bare local
invocation (`pnpm start`, `bun server.ts`, `pipx install foo`) now yields ""
(no package) and degrades to tool-definitions-only instead of fetching a
bogus package. npx/uvx/bunx (no subcommand) are unchanged.

TDD: added pnpm start / pnpm run build / bun server.ts / bun run dev /
yarn build / pipx install cases (-> ""); pnpm dlx <pkg> / npx -y <pkg> /
uvx --from still resolve. Rebased onto origin/main (picks up #676
archive-extraction hardening + the teams/broker -> serveredition/broker move).

Related MCP-2445
1. Flag-bypass: runnerPackageSpec parsed package flags (-p/--package/--from)
   BEFORE enforcing the dlx/x/run subcommand, so `pnpm -p start` /
   `bun -p server.ts` still produced a spec. Now the required subcommand
   keyword is enforced FIRST (skipping only leading global flags); flag
   parsing happens only on the args that follow it.

2. Argument-aware classification: isPackageRunnerCommand classified bare
   pnpm/yarn/bun/pipx as runners by name alone. It now takes args and treats
   them as runners only when the ephemeral-run keyword is actually present
   (npx/uvx/bunx remain runners by name). Decided up front, not via a later
   parser failure.

3. Version-pin miss never substitutes: when an exact pin is requested but
   absent from a local cache, resolveNpxCache, findUvxArchiveDir, the uv
   tools dir (Strategy 2), and the container npx locate script all now
   resolve NOTHING instead of falling back to a mismatched cached version
   (false coverage). The published-source fetch then gets the pinned version.
   Also fixed cleanPkg to strip PEP 440 specifiers (pkg==1.0), not just @ver.

TDD: pnpm -p start / bun -p server.ts -> no spec; isPackageRunnerCommand
arg-aware table; npx + uvx-archive + uv-tools-dir pin-miss -> not resolved;
container locate pin-miss -> empty; exact-pin hits + unpinned still resolve.
go test ./internal/security/... -race green; golangci v2 0 issues.

Related MCP-2445
@Dumbris Dumbris force-pushed the mcp-2445-scanner-spec-parsing branch from 0041dfd to 41e9a59 Compare June 15, 2026 10:00
@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

codex RE-REVIEW (gpt-5.5) → REQUEST_CHANGES (round 3 — the 3 prior blockers are now FIXED ✅: subcommand-keyword-first parsing, argument-aware classification, host-side npx/uvx pin-miss returns error). One new in-scope blocker:

Container-side version match uses unescaped grep -EnpxContainerLocateScript (source_resolver.go:806) greps the version as an ERE, so . is a wildcard. A pin like pkg@1.0.0-alpha.1 matches cached 1.0.0-alpha-1, substituting the WRONG cached package despite the host-side pin-miss guard. Since MCP-2445 covers npx/container version-pin correctness, this is in scope. Fix: use fixed-string comparison (grep -F) or JSON-parse + exact string equality for the container cache path. Add a test with a regex-special version (e.g. 1.0.0-alpha.1 vs 1.0.0-alpha-1).

Codex round-3 (MCP-2445): npxContainerLocateScript embedded the requested
version inside a `grep -E` pattern, so regex metacharacters were active —
a pin like `1.0.0-alpha.1` matched a cached `1.0.0-alpha-1` ('.' as wildcard),
substituting the WRONG cached package despite the host-side pin-miss guard.

Fix: extract the package.json "version" VALUE (grep matches only the key; the
value is captured as "[^\"]*", never the pin), then compare it to the pin with
a LITERAL shell string test (`[ "$v" = '<pin>' ]`). No regex over the version.
The `if`-form keeps a no-match iteration's exit status 0 (clean empty result).

TDD: cached `1.0.0-alpha-1` + pin `1.0.0-alpha.1` -> no match (literal); exact
`1.0.0-alpha-1` pin still resolves; prior pin-hit/miss/unpinned cases unchanged.
go test ./internal/security/... -race green; golangci v2 0 issues.

Related MCP-2445

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

codex ACCEPT (gpt-5.5, reviewed in-checkout): subcommand-keyword-first parsing (pnpm -p start / bun -p server.ts do not resolve), argument-aware classification, host pin-miss errors instead of substituting, and container npx version match now LITERAL (test asserts 1.0.0-alpha.1 != 1.0.0-alpha-1). All 4 review rounds' findings closed; no regression.

@Dumbris Dumbris merged commit 9794dfe into main Jun 15, 2026
53 checks passed
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