pkgxray 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -44
- package/bin/audit.js +3 -112
- package/bin/mcp-server.js +2 -78
- package/package.json +3 -20
- package/src/auditor.js +191 -112
- package/src/providers/anthropic.js +0 -64
- package/src/providers/gemini.js +0 -66
- package/src/providers/index.js +0 -40
- package/src/providers/openai.js +0 -75
- package/src/reasoner.js +0 -265
package/README.md
CHANGED
|
@@ -96,11 +96,10 @@ Use the stdio server from any MCP-capable agent:
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
The server exposes
|
|
99
|
+
The server exposes two tools:
|
|
100
100
|
|
|
101
|
-
- `audit_agent_extension_supply_chain` —
|
|
101
|
+
- `audit_agent_extension_supply_chain` — static heuristics on supplied evidence
|
|
102
102
|
- `guard_agent_extension_install` — stage, vuln-check, audit a real package
|
|
103
|
-
- `reason_about_extension_supply_chain` — Claude-powered authoritative verdict (requires `ANTHROPIC_API_KEY`)
|
|
104
103
|
|
|
105
104
|
Tool arguments:
|
|
106
105
|
|
|
@@ -114,50 +113,22 @@ Tool arguments:
|
|
|
114
113
|
`guard_agent_extension_install` accepts `reference`, optional `quarantineRoot`,
|
|
115
114
|
optional `promoteTo`, `policy`, `force`, and `outputFormat`.
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
`audit_agent_extension_supply_chain`, plus optional `model` (default
|
|
119
|
-
`claude-opus-4-7`) and `maxFiles` (default 200). It returns a JSON verdict per
|
|
120
|
-
the prompt's schema (`verdict`, `summary`, `findings`, `evidenceGaps`,
|
|
121
|
-
`promotable`) plus `usage` and `latencyMs`.
|
|
116
|
+
## Static heuristics — calibration
|
|
122
117
|
|
|
123
|
-
|
|
118
|
+
The heuristics are calibrated to keep legitimate packages out of `block`. Real
|
|
119
|
+
malicious patterns that gate the verdict:
|
|
124
120
|
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
- **block** (HIGH) — prompt-injection text in README/docs, credential reads in
|
|
122
|
+
proximity to a filesystem-read primitive, persistence writes to shell rc /
|
|
123
|
+
cron / launchagents, dynamic exec + hardcoded IP/shortener/webhook target,
|
|
124
|
+
bulk `process.env` harvest in the same file as outbound network.
|
|
125
|
+
- **review** (MEDIUM) — install / postinstall / prepare lifecycle scripts,
|
|
126
|
+
dynamic eval / new Function / vm, clipboard read/write, missing
|
|
127
|
+
package.json, missing entrypoint source.
|
|
128
|
+
- **info** — child_process / fetch / network in isolation. Common in build
|
|
129
|
+
tools and CLIs; recorded but does not gate the verdict.
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
# Anthropic (default)
|
|
130
|
-
export ANTHROPIC_API_KEY=sk-ant-...
|
|
131
|
-
npm install -g @anthropic-ai/sdk
|
|
132
|
-
pkgxray --reason --file evidence.json
|
|
133
|
-
pkgxray guard npm:some-pkg --reason --format json
|
|
134
|
-
|
|
135
|
-
# OpenAI
|
|
136
|
-
export OPENAI_API_KEY=sk-...
|
|
137
|
-
npm install -g openai
|
|
138
|
-
pkgxray guard npm:some-pkg --reason --reason-provider openai
|
|
139
|
-
|
|
140
|
-
# Gemini
|
|
141
|
-
export GEMINI_API_KEY=...
|
|
142
|
-
npm install -g @google/generative-ai
|
|
143
|
-
pkgxray guard npm:some-pkg --reason --reason-model gemini-2.5-pro
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Provider is selected by `--reason-provider <anthropic|openai|gemini>` or
|
|
147
|
-
auto-detected from the model prefix (`claude-*` → anthropic, `gpt-*`/`o*` →
|
|
148
|
-
openai, `gemini-*` → gemini). Defaults: `claude-opus-4-7`, `gpt-5`,
|
|
149
|
-
`gemini-2.5-pro` — overridable with `--reason-model`.
|
|
150
|
-
|
|
151
|
-
The Anthropic path uses adaptive thinking, `effort: "high"`, and caches the
|
|
152
|
-
system prompt for 5-minute TTL (~90% cheaper on prompt tokens for repeated
|
|
153
|
-
calls in the window). OpenAI uses strict structured outputs against the same
|
|
154
|
-
JSON Schema. Gemini uses JSON-mode responses.
|
|
155
|
-
|
|
156
|
-
Source files are capped at 200 files / 32 KB each / 500 KB total before
|
|
157
|
-
sending — override with `--reason-max-files`.
|
|
158
|
-
|
|
159
|
-
The reasoning verdict supersedes the static decision when `--reason` is used.
|
|
160
|
-
Exit codes: `0` = safe, `2` = block, `3` = review.
|
|
131
|
+
`.d.ts`, `.map`, `.min.js`, and `.lock` files are skipped entirely.
|
|
161
132
|
|
|
162
133
|
## Browser Extension
|
|
163
134
|
|
package/bin/audit.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
const { auditEvidence, renderMarkdown } = require("../src/auditor");
|
|
6
6
|
const { guardExtension } = require("../src/quarantine");
|
|
7
|
-
const { reasonAbout } = require("../src/reasoner");
|
|
8
7
|
|
|
9
8
|
function printUsage() {
|
|
10
9
|
process.stderr.write(
|
|
@@ -13,17 +12,10 @@ function printUsage() {
|
|
|
13
12
|
" pkgxray < evidence.json",
|
|
14
13
|
" pkgxray --format json < evidence.json",
|
|
15
14
|
" pkgxray --file evidence.json --format markdown",
|
|
16
|
-
" pkgxray --
|
|
17
|
-
" pkgxray guard <npm-package|npm:name@version|./path> [--reason] [--promote-to dir] [--no-source-scan]",
|
|
15
|
+
" pkgxray guard <npm-package|npm:name@version|./path> [--promote-to dir] [--no-source-scan]",
|
|
18
16
|
"",
|
|
19
17
|
"Evidence JSON fields:",
|
|
20
18
|
" packageName, npmMetadata, githubMetadata, webPresence, sourceFiles",
|
|
21
|
-
"",
|
|
22
|
-
"--reason consults an LLM as an authoritative verdict on top of the static",
|
|
23
|
-
"heuristics. Provider auto-detected from --reason-model, or pass",
|
|
24
|
-
"--reason-provider <anthropic|openai|gemini>. Defaults: anthropic +",
|
|
25
|
-
"claude-opus-4-7. Each provider needs its own env key (ANTHROPIC_API_KEY,",
|
|
26
|
-
"OPENAI_API_KEY, GEMINI_API_KEY) and SDK installed.",
|
|
27
19
|
""
|
|
28
20
|
].join("\n")
|
|
29
21
|
);
|
|
@@ -57,14 +49,6 @@ function parseArgs(argv) {
|
|
|
57
49
|
options.sourceScan = false;
|
|
58
50
|
} else if (arg === "--no-vulnerability-check") {
|
|
59
51
|
options.vulnerabilityCheck = false;
|
|
60
|
-
} else if (arg === "--reason") {
|
|
61
|
-
options.reason = true;
|
|
62
|
-
} else if (arg === "--reason-model") {
|
|
63
|
-
options.reasonModel = argv[++i];
|
|
64
|
-
} else if (arg === "--reason-provider") {
|
|
65
|
-
options.reasonProvider = argv[++i];
|
|
66
|
-
} else if (arg === "--reason-max-files") {
|
|
67
|
-
options.reasonMaxFiles = Number(argv[++i]);
|
|
68
52
|
} else {
|
|
69
53
|
throw new Error(`Unknown argument: ${arg}`);
|
|
70
54
|
}
|
|
@@ -82,26 +66,6 @@ function readInput(file) {
|
|
|
82
66
|
return fs.readFileSync(0, "utf8");
|
|
83
67
|
}
|
|
84
68
|
|
|
85
|
-
async function maybeReason(evidence, options) {
|
|
86
|
-
if (!options.reason) return null;
|
|
87
|
-
try {
|
|
88
|
-
return await reasonAbout(evidence, {
|
|
89
|
-
provider: options.reasonProvider,
|
|
90
|
-
model: options.reasonModel,
|
|
91
|
-
maxFiles: options.reasonMaxFiles
|
|
92
|
-
});
|
|
93
|
-
} catch (error) {
|
|
94
|
-
return { error: { code: error.code || "REASONER_ERROR", message: error.message } };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function reasoningExitCode(reasoning) {
|
|
99
|
-
if (!reasoning || reasoning.error) return null;
|
|
100
|
-
if (reasoning.verdict === "block") return 2;
|
|
101
|
-
if (reasoning.verdict === "review") return 3;
|
|
102
|
-
return 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
69
|
async function main() {
|
|
106
70
|
const options = parseArgs(process.argv.slice(2));
|
|
107
71
|
if (options.help) {
|
|
@@ -114,24 +78,6 @@ async function main() {
|
|
|
114
78
|
throw new Error("guard requires an extension reference");
|
|
115
79
|
}
|
|
116
80
|
const result = await guardExtension(options.reference, options);
|
|
117
|
-
if (options.reason) {
|
|
118
|
-
const evidenceForReason = {
|
|
119
|
-
packageName: result.resolved && result.resolved.packageName,
|
|
120
|
-
npmMetadata: result.resolved && result.resolved.npmMetadata,
|
|
121
|
-
githubMetadata: null,
|
|
122
|
-
webPresence: null,
|
|
123
|
-
sourceFiles: result.sourceFiles || {}
|
|
124
|
-
};
|
|
125
|
-
const reasoning = await maybeReason(evidenceForReason, options);
|
|
126
|
-
result.reasoning = reasoning;
|
|
127
|
-
if (reasoning && !reasoning.error && reasoning.verdict) {
|
|
128
|
-
result.decision = reasoning.verdict === "block"
|
|
129
|
-
? "block"
|
|
130
|
-
: reasoning.verdict === "safe"
|
|
131
|
-
? "allow"
|
|
132
|
-
: "review";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
81
|
if (options.format === "json") {
|
|
136
82
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
137
83
|
} else {
|
|
@@ -148,22 +94,14 @@ async function main() {
|
|
|
148
94
|
|
|
149
95
|
const evidence = JSON.parse(raw);
|
|
150
96
|
const report = auditEvidence(evidence);
|
|
151
|
-
const reasoning = await maybeReason(evidence, options);
|
|
152
|
-
|
|
153
|
-
const payload = reasoning ? { report, reasoning } : report;
|
|
154
97
|
|
|
155
98
|
if (options.format === "json") {
|
|
156
|
-
process.stdout.write(`${JSON.stringify(
|
|
157
|
-
} else if (reasoning) {
|
|
158
|
-
process.stdout.write(`${renderMarkdown(report)}\n\n---\n\n${renderReasoningMarkdown(reasoning)}\n`);
|
|
99
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
159
100
|
} else {
|
|
160
101
|
process.stdout.write(`${renderMarkdown(report)}\n`);
|
|
161
102
|
}
|
|
162
103
|
|
|
163
|
-
|
|
164
|
-
if (exitFromReason !== null) {
|
|
165
|
-
process.exitCode = exitFromReason;
|
|
166
|
-
}
|
|
104
|
+
process.exitCode = report.verdict === "block" ? 2 : report.verdict === "review" ? 3 : 0;
|
|
167
105
|
}
|
|
168
106
|
|
|
169
107
|
function renderGuardMarkdown(result) {
|
|
@@ -179,53 +117,6 @@ function renderGuardMarkdown(result) {
|
|
|
179
117
|
}
|
|
180
118
|
|
|
181
119
|
lines.push(renderMarkdown(result.report));
|
|
182
|
-
|
|
183
|
-
if (result.reasoning) {
|
|
184
|
-
lines.push("", "---", "", renderReasoningMarkdown(result.reasoning));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return lines.join("\n");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function renderReasoningMarkdown(reasoning) {
|
|
191
|
-
if (reasoning.error) {
|
|
192
|
-
return `Reasoning: **unavailable** (${reasoning.error.code}: ${reasoning.error.message})`;
|
|
193
|
-
}
|
|
194
|
-
const lines = [
|
|
195
|
-
`Reasoning verdict: **${(reasoning.verdict || "?").toUpperCase()}**`,
|
|
196
|
-
`Provider: \`${reasoning.provider || "?"}\` · Model: \`${reasoning.model}\` · latency: ${reasoning.latencyMs} ms`,
|
|
197
|
-
"",
|
|
198
|
-
reasoning.summary || "",
|
|
199
|
-
""
|
|
200
|
-
];
|
|
201
|
-
if (reasoning.usage) {
|
|
202
|
-
const u = reasoning.usage;
|
|
203
|
-
const parts = [
|
|
204
|
-
`in=${u.input_tokens ?? "?"}`,
|
|
205
|
-
`out=${u.output_tokens ?? "?"}`,
|
|
206
|
-
`cache_read=${u.cache_read_input_tokens ?? 0}`,
|
|
207
|
-
`cache_write=${u.cache_creation_input_tokens ?? 0}`
|
|
208
|
-
];
|
|
209
|
-
lines.push(`Tokens: ${parts.join(" · ")}`, "");
|
|
210
|
-
}
|
|
211
|
-
if (reasoning.findings && reasoning.findings.length > 0) {
|
|
212
|
-
lines.push("Findings:");
|
|
213
|
-
for (const finding of reasoning.findings) {
|
|
214
|
-
lines.push(
|
|
215
|
-
`- **${finding.severity.toUpperCase()} - ${finding.category}**: ${finding.reasoning}`,
|
|
216
|
-
` Evidence: \`${finding.evidence}\``
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
lines.push("");
|
|
220
|
-
} else {
|
|
221
|
-
lines.push("Findings: none reported.", "");
|
|
222
|
-
}
|
|
223
|
-
if (reasoning.evidenceGaps && reasoning.evidenceGaps.length > 0) {
|
|
224
|
-
lines.push("Evidence gaps:");
|
|
225
|
-
for (const gap of reasoning.evidenceGaps) {
|
|
226
|
-
lines.push(`- ${gap}`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
120
|
return lines.join("\n");
|
|
230
121
|
}
|
|
231
122
|
|
package/bin/mcp-server.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
const { auditEvidence, renderMarkdown } = require("../src/auditor");
|
|
5
5
|
const { guardExtension } = require("../src/quarantine");
|
|
6
|
-
const { reasonAbout } = require("../src/reasoner");
|
|
7
6
|
|
|
8
7
|
const TOOL_NAME = "audit_agent_extension_supply_chain";
|
|
9
8
|
const GUARD_TOOL_NAME = "guard_agent_extension_install";
|
|
10
|
-
const REASON_TOOL_NAME = "reason_about_extension_supply_chain";
|
|
11
9
|
let buffer = "";
|
|
12
10
|
|
|
13
11
|
function send(message) {
|
|
@@ -115,59 +113,6 @@ function guardToolDefinition() {
|
|
|
115
113
|
};
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
function reasonToolDefinition() {
|
|
119
|
-
return {
|
|
120
|
-
name: REASON_TOOL_NAME,
|
|
121
|
-
description:
|
|
122
|
-
"Consult an LLM as an authoritative reasoning layer over supplied evidence. Supports Anthropic, OpenAI, and Gemini providers; defaults to Anthropic + claude-opus-4-7. Returns a structured JSON verdict (verdict, summary, findings, evidenceGaps, promotable). Requires the matching env key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY) and SDK installed.",
|
|
123
|
-
inputSchema: {
|
|
124
|
-
type: "object",
|
|
125
|
-
additionalProperties: false,
|
|
126
|
-
properties: {
|
|
127
|
-
packageName: { type: "string" },
|
|
128
|
-
npmMetadata: {},
|
|
129
|
-
githubMetadata: {},
|
|
130
|
-
webPresence: {},
|
|
131
|
-
sourceFiles: {
|
|
132
|
-
description:
|
|
133
|
-
"Map of file path to source text, or an array of objects with path/name and content/text.",
|
|
134
|
-
anyOf: [
|
|
135
|
-
{ type: "object", additionalProperties: { type: "string" } },
|
|
136
|
-
{
|
|
137
|
-
type: "array",
|
|
138
|
-
items: {
|
|
139
|
-
type: "object",
|
|
140
|
-
additionalProperties: true,
|
|
141
|
-
properties: {
|
|
142
|
-
path: { type: "string" },
|
|
143
|
-
name: { type: "string" },
|
|
144
|
-
content: { type: "string" },
|
|
145
|
-
text: { type: "string" }
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
},
|
|
151
|
-
provider: {
|
|
152
|
-
type: "string",
|
|
153
|
-
enum: ["anthropic", "openai", "gemini"],
|
|
154
|
-
description: "LLM provider. Defaults to anthropic, or auto-detected from model."
|
|
155
|
-
},
|
|
156
|
-
model: {
|
|
157
|
-
type: "string",
|
|
158
|
-
description:
|
|
159
|
-
"Model ID. Defaults per provider: anthropic=claude-opus-4-7, openai=gpt-5, gemini=gemini-2.5-pro."
|
|
160
|
-
},
|
|
161
|
-
maxFiles: {
|
|
162
|
-
type: "integer",
|
|
163
|
-
description: "Cap on source files sent to the model. Default 200."
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
required: ["sourceFiles"]
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
116
|
function handleRequest(request) {
|
|
172
117
|
const { id, method, params } = request;
|
|
173
118
|
|
|
@@ -195,14 +140,14 @@ function handleRequest(request) {
|
|
|
195
140
|
jsonrpc: "2.0",
|
|
196
141
|
id,
|
|
197
142
|
result: {
|
|
198
|
-
tools: [toolDefinition(), guardToolDefinition()
|
|
143
|
+
tools: [toolDefinition(), guardToolDefinition()]
|
|
199
144
|
}
|
|
200
145
|
};
|
|
201
146
|
}
|
|
202
147
|
|
|
203
148
|
if (method === "tools/call") {
|
|
204
149
|
const name = params && params.name;
|
|
205
|
-
if (name !== TOOL_NAME && name !== GUARD_TOOL_NAME
|
|
150
|
+
if (name !== TOOL_NAME && name !== GUARD_TOOL_NAME) {
|
|
206
151
|
return {
|
|
207
152
|
jsonrpc: "2.0",
|
|
208
153
|
id,
|
|
@@ -215,27 +160,6 @@ function handleRequest(request) {
|
|
|
215
160
|
|
|
216
161
|
const args = (params && params.arguments) || {};
|
|
217
162
|
|
|
218
|
-
if (name === REASON_TOOL_NAME) {
|
|
219
|
-
return reasonAbout(args, {
|
|
220
|
-
provider: args.provider,
|
|
221
|
-
model: args.model,
|
|
222
|
-
maxFiles: args.maxFiles
|
|
223
|
-
})
|
|
224
|
-
.then((reasoning) => ({
|
|
225
|
-
jsonrpc: "2.0",
|
|
226
|
-
id,
|
|
227
|
-
result: {
|
|
228
|
-
content: textContent(JSON.stringify(reasoning, null, 2)),
|
|
229
|
-
structuredContent: reasoning
|
|
230
|
-
}
|
|
231
|
-
}))
|
|
232
|
-
.catch((error) => ({
|
|
233
|
-
jsonrpc: "2.0",
|
|
234
|
-
id,
|
|
235
|
-
error: { code: -32603, message: `${error.code || "REASONER_ERROR"}: ${error.message}` }
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
163
|
if (name === GUARD_TOOL_NAME) {
|
|
240
164
|
return guardExtension(args.reference, args).then((guardResult) => {
|
|
241
165
|
const text =
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkgxray",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Zero-dep local CLI and MCP server that scans npm packages and AI-agent extensions for supply-chain risk. OSV vuln pre-check, sandboxed quarantine, tarball-integrity verification, calibrated static heuristics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jack Adams-Lovell",
|
|
7
7
|
"type": "commonjs",
|
|
@@ -34,26 +34,9 @@
|
|
|
34
34
|
"security",
|
|
35
35
|
"supply-chain",
|
|
36
36
|
"npm-audit",
|
|
37
|
-
"agent-extension"
|
|
38
|
-
"ai-safety",
|
|
39
|
-
"claude",
|
|
40
|
-
"openai",
|
|
41
|
-
"gemini"
|
|
37
|
+
"agent-extension"
|
|
42
38
|
],
|
|
43
39
|
"engines": {
|
|
44
40
|
"node": ">=18"
|
|
45
|
-
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"@anthropic-ai/sdk": ">=0.40.0",
|
|
48
|
-
"openai": ">=4.0.0",
|
|
49
|
-
"@google/generative-ai": ">=0.20.0"
|
|
50
|
-
},
|
|
51
|
-
"peerDependenciesMeta": {
|
|
52
|
-
"@anthropic-ai/sdk": { "optional": true },
|
|
53
|
-
"openai": { "optional": true },
|
|
54
|
-
"@google/generative-ai": { "optional": true }
|
|
55
|
-
},
|
|
56
|
-
"devDependencies": {
|
|
57
|
-
"@anthropic-ai/sdk": "^0.105.0"
|
|
58
41
|
}
|
|
59
42
|
}
|