🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

pkgxray

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pkgxray - npm Package Compare versions

Comparing version
1.0.3
to
1.0.4
+36
-0
bin/audit.js

@@ -52,2 +52,4 @@ #!/usr/bin/env node

" [--no-scan-results] [--timing] # and tool RESULTS are scanned for injection (use in host config)",
" pkgxray mcp-server # run pkgxray ITSELF as a stdio MCP server (for MCP hosts /",
" # the MCP Registry entry: `npx -y pkgxray mcp-server`) # exposes audit/guard/lockfile tools. cf. `mcp` above, which audits.",
"",

@@ -253,2 +255,18 @@ "Evidence JSON fields:",

async function main() {
// `pkgxray mcp-server` runs THIS package as a stdio MCP server — the same
// server the `pkgxray-mcp` bin exposes — so MCP hosts can launch it with
// `npx -y pkgxray mcp-server` (the registry entry) without a separate
// package. Intercepted before parseArgs so none of the audit machinery runs.
// NOTE: distinct from `pkgxray mcp <target>`, which AUDITS another server.
if (process.argv[2] === "mcp-server") {
if (process.argv.includes("--help") || process.argv.includes("-h")) {
process.stderr.write(
"Usage: pkgxray mcp-server # run pkgxray as a stdio MCP server (for MCP hosts)\n"
);
return;
}
require("./mcp-server.js").startStdioServer();
return;
}
const options = parseArgs(process.argv.slice(2));

@@ -290,2 +308,20 @@ if (options.help) {

process.stderr.write(`pkgxray: guard failed to complete (${error.message}); treating as ${verdict}\n`);
// Under --format json, a JSON consumer must still receive a parseable
// verdict on stdout — otherwise the degraded review is indistinguishable
// from a silent crash (empty stdout), which is exactly what batch scanners
// record as a "scan error". Emit a minimal, schema-tagged object.
if (options.format === "json") {
process.stdout.write(
`${JSON.stringify(
{
schemaVersion: 1,
decision: verdict,
reference: options.reference,
scanError: error.message
},
null,
2
)}\n`
);
}
process.exitCode = cfg.exitCodeForVerdict(verdict, config);

@@ -292,0 +328,0 @@ return;

+7
-3

@@ -830,4 +830,7 @@ #!/usr/bin/env node

// Exported for tests only. The wire behavior is unchanged whether or not these
// are consumed; they let the offline suite unit-test the config seam.
// `startStdioServer` is the same entry the `pkgxray-mcp` bin runs on direct
// invocation; it is exported so `pkgxray mcp-server` (bin/audit.js) can launch
// this server in-process without a separate package. The rest are exported for
// tests only — the wire behavior is unchanged whether or not they are consumed;
// they let the offline suite unit-test the config seam.
module.exports = {

@@ -837,3 +840,4 @@ applyConfigToGuardResult,

listTools,
CONFIG_TOOL_KEY
CONFIG_TOOL_KEY,
startStdioServer: attachStdin
};
{
"name": "pkgxray",
"version": "1.0.3",
"version": "1.0.4",
"mcpName": "io.github.adamsjack711-ux/pkgxray",
"description": "Zero-dep local CLI and MCP server that scans npm packages for supply-chain risk. OSV vuln pre-check, sandboxed quarantine, tarball-integrity verification, calibrated static heuristics, GitHub provenance cross-check.",

@@ -5,0 +6,0 @@ "license": "MIT",

@@ -73,4 +73,8 @@ <div align="center">

(no LLM in the verdict path, so injected text can't steer them), only
citable evidence is reported, and the zero-false-block calibration is
[regression-gated in CI](docs/benchmark.md).
citable evidence is reported, and the **zero-heuristic-false-block calibration
on the top-1000 most-downloaded packages** is
[regression-gated in CI](docs/benchmark.md). That claim is scoped to the
most-installed set — it is *not* a claim of zero false blocks on every package;
the newer MCP/agent-tooling ecosystem is over-blocked and being reconciled
per-case ([details](docs/benchmark.md#scope-of-the-claim-read-this-first)).

@@ -162,2 +166,6 @@ ## What it catches

pkgxray is published on the [MCP Registry](https://registry.modelcontextprotocol.io)
as `io.github.adamsjack711-ux/pkgxray`. Add it to any MCP client — locally
installed (`pkgxray-mcp`) or zero-install via `npx`:
```json

@@ -167,2 +175,6 @@ { "mcpServers": { "pkgxray": { "command": "pkgxray-mcp" } } }

```json
{ "mcpServers": { "pkgxray": { "command": "npx", "args": ["-y", "pkgxray", "mcp-server"] } } }
```
Gate installs with the [hookshot integration](examples/hookshot/) and wrap MCP

@@ -244,4 +256,5 @@ servers with [`pkgxray mcp-proxy`](docs/mcp.md#per-call-runtime-gate-pkgxray-mcp-proxy).

- **Known-vulnerable packages block at the OSV pre-check**, before download
- **Calibration** (precision, recall, the 0-false-block gate) is measured by a
committed [benchmark corpus](benchmark/) that fails CI when it regresses
- **Calibration** (precision, recall, the 0-heuristic-false-block gate on the
top-1000 most-downloaded — [scope](docs/benchmark.md#scope-of-the-claim-read-this-first))
is measured by a committed [benchmark corpus](benchmark/) that fails CI when it regresses

@@ -248,0 +261,0 @@ Full numbers: [docs/reference.md#performance](docs/reference.md#performance) ·

@@ -302,2 +302,11 @@ "use strict";

};
// The write stream MUST have an 'error' listener. Without one, a stream error
// — including the ERR_STREAM_DESTROYED that fires when `cleanup()` destroys the
// file while `response.pipe(file)` is still flowing (size-cap / redirect /
// timeout races), or an open-time EACCES/ENOSPC — is emitted as an unhandled
// 'error' event and takes the whole process down with exit 1 (the verdict is
// already printed, so a scan crashes AFTER succeeding). Route it to cleanup,
// which is idempotent, so the download rejects instead of crashing. Mirrors
// downloadFromCacheServer and quarantine.downloadFile.
file.on("error", cleanup);
const get = (currentUrl, hops) => {

@@ -304,0 +313,0 @@ if (hops > 5) return cleanup(new Error("Too many redirects"));

@@ -54,7 +54,13 @@ "use strict";

const DEFAULT_DOWNLOAD_MAX_REDIRECTS = 5;
// NOTE: `dist` / `build` are deliberately NOT skipped. In a *source repo* those
// are throwaway build output, but in a *published npm tarball* they are the
// shipped, executed code — `"main": "dist/index.js"` is ubiquitous, and many
// packages publish ONLY their compiled `dist/`. Skipping them meant the entire
// heuristic layer (de-obfuscation, exec/exfil correlation, artifact-diff) never
// ran on the code that actually runs at install/require time — a trivial evasion
// (drop the payload in `dist/`, point `main` at it). We keep skipping genuine
// non-code noise: VCS, vendored deps, framework caches, coverage, py caches.
const SKIP_DIRS = new Set([
".git",
"node_modules",
"dist",
"build",
"coverage",

@@ -1222,2 +1228,6 @@ ".next",

};
// Attach the error handler at creation, not inside the response callback: an
// open-time error (EACCES/ENOSPC) or a write-after-destroy race would
// otherwise be an unhandled 'error' event that crashes the process.
file.on("error", fail);
const succeed = () => {

@@ -1275,3 +1285,2 @@ if (settled) return;

file.on("finish", succeed);
file.on("error", fail);
}

@@ -1300,4 +1309,19 @@ );

]);
await normalizeTreePermissions(destination);
}
// Normalize owner perms across an extracted tree so the scanner can always read
// every file and traverse every directory. A package can ship a directory with
// no execute bit or a file with no read bit — by accident (pngjs ships lib/ as
// 0644) or on purpose, to hide code from the static walk so the scan aborts on
// EACCES and degrades to review instead of reading (and blocking) the payload.
// This is our throwaway, never-executed copy, so widening owner read/traverse is
// safe. `u+rX` adds read to everything and execute to DIRECTORIES only (never
// makes a regular file executable — `chmod -R` applies +X to a directory before
// it recurses, so a non-traversable dir gets fixed then descended into), so the
// file-mode signals we derive from the tar listing are unaffected.
async function normalizeTreePermissions(root) {
await run("chmod", ["-R", "u+rX", root]);
}
// Validate a `tar -tvzf` listing (array of non-empty lines). Throws

@@ -1550,5 +1574,17 @@ // "Tarball rejected: ..." on any unsafe/unparseable entry so extraction fails

if (!entry.isFile() || !looksTextLike(relativePath)) {
if (!entry.isFile()) {
continue;
}
let collectThisFile = looksTextLike(relativePath);
if (!collectThisFile && !entry.name.includes(".")) {
// Extensionless regular file (e.g. bare `install` / `preinstall` /
// `configure`): collect it only if it's a shebang script. These carry
// no text extension so looksTextLike skips them, yet a lifecycle hook
// can run them at install time. The peek is a bounded 2-byte read and
// only happens for the handful of extensionless files in a package.
collectThisFile = await startsWithShebang(fullPath);
}
if (!collectThisFile) {
continue;
}

@@ -1592,2 +1628,20 @@ // Use lstat (not stat) so a sneakily-replaced symlink between readdir

// True if the file's first two bytes are a shebang (`#!`). Used to rescue
// extensionless executable scripts that looksTextLike skips. Opens with a
// bounded 2-byte read and never follows a symlink (callers gate on a Dirent
// isFile() first, and the read targets a plain path).
async function startsWithShebang(fullPath) {
let handle;
try {
handle = await fsp.open(fullPath, "r");
const buf = Buffer.alloc(2);
const { bytesRead } = await handle.read(buf, 0, 2, 0);
return bytesRead === 2 && buf[0] === 0x23 && buf[1] === 0x21; // "#!"
} catch {
return false;
} finally {
if (handle) await handle.close();
}
}
function decisionForReport(report, policy) {

@@ -1652,2 +1706,3 @@ if (report.verdict === "block") {

extractTarball,
normalizeTreePermissions,
parseTarListingLine,

@@ -1654,0 +1709,0 @@ assertNoControlChars,

Sorry, the diff of this file is too big to display