Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/features/security-scanner-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,24 @@ Scanners communicate results via SARIF 2.1.0. Exit code `0` indicates "scan comp
2. Package cache (npx/uvx/pipx/bunx) — PREFERRED for package runners
3. WorkingDir (from server config)
4. Arg-scan fallback — accepts only directories containing source markers
5. Tool definitions only — last resort (HTTP / SSE / unresolvable)
5. Published package fetch (npx/uvx) — download & unpack real source, no execution
6. Tool definitions only — last resort (HTTP / SSE / unresolvable)
```

The resolved method and path are recorded on the scan job and visible via both the text and JSON report. See [Security Commands → scan](/cli/security-commands#security-scan) for more.
The resolved method and path are recorded on the scan job and visible via both the text and JSON report. The `source_method` field reports how source was obtained: `docker_extract`, `npx_cache`, `uvx_cache`, `working_dir`, `npm_pack`, `pip_download`, `url`, or `tool_definitions_only`. See [Security Commands → scan](/cli/security-commands#security-scan) for more.

#### Published package fetch (npx / uvx)

Package-runner servers (`npx`, `uvx`) are the **primary** quarantine/scan target, but a server quarantined on add has never run locally — so the local package cache (step 2) misses and, before this fallback existed, the scan degraded to **tool definitions only** (no real source-level analysis). Step 5 closes that gap by downloading the *published* package source so the AI and supply-chain scanners run against real code.

**The source is fetched but never executed.** A scanner must not run the untrusted code it is scanning. The fetch only ever downloads and unpacks archives:

- **npm (`npx`)** — `npm pack <pkg>@<version> --ignore-scripts` downloads the published tarball without running any lifecycle scripts (`install`/`postinstall`), then it is extracted (`source_method=npm_pack`).
- **PyPI (`uvx`)** — `uv pip download <pkg>==<version> --no-deps --only-binary=:all:` (falling back to `pip download`) fetches **only a prebuilt wheel**, which is unpacked without building or running `setup.py` (`source_method=pip_download`). `--only-binary=:all:` is mandatory: downloading an sdist would invoke the package's PEP 517 build backend (`setup.py egg_info`) to resolve metadata, executing the untrusted code. A package that ships **no wheel** therefore fails the fetch and falls back to tool definitions only — sdists are never built or extracted.

Extraction is hardened against path traversal (zip-slip), symlink escape, and decompression bombs (bounded file count and total size). If the toolchain is missing, the host is offline, or the fetch fails, resolution falls through to **tool definitions only** with no regression.

This fallback is **enabled by default**. Air-gapped deployments that must forbid the scanner's network egress can disable it with `security.scanner_fetch_package_source: false` — package-runner servers without local source then scan tool definitions only.

## SARIF normalization

Expand Down
22 changes: 22 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1768,4 +1768,26 @@ type SecurityConfig struct {
// is small. The preferred fix remains replacing snap docker with a
// distro-packaged docker.
ScannerDisableNoNewPrivileges bool `json:"scanner_disable_no_new_privileges,omitempty" mapstructure:"scanner-disable-no-new-privileges"`

// ScannerFetchPackageSource controls whether the scanner fetches the
// PUBLISHED source of package-runner servers (npx/uvx) — without executing
// it — when no local source is available (no Docker container, no local
// package cache, no working_dir). This is the primary quarantine/scan
// target: a quarantined-on-add server is never run locally, so without this
// the scan degrades to tool-definitions-only (no real source-level
// analysis). See MCP-2206.
//
// Fetching uses `npm pack --ignore-scripts` (npm) and `uv pip download` /
// `pip download` with `--only-binary=:all:` (Python), which only download +
// unpack archives and NEVER run install, build, or setup.py — a scanner must
// not execute the untrusted code it is scanning. The Python
// `--only-binary=:all:` flag is required because downloading an sdist would
// invoke its build backend (setup.py); packages with no wheel fall back to
// tool-definitions-only instead. Extraction is hardened against path
// traversal and decompression bombs.
//
// Default (nil) is ENABLED. Set to false on air-gapped deployments to
// forbid the scanner's network egress; such servers then fall back to the
// tool-definitions-only scan with no regression.
ScannerFetchPackageSource *bool `json:"scanner_fetch_package_source,omitempty" mapstructure:"scanner-fetch-package-source"`
}
Loading
Loading