pkgxray 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/auditor.js +65 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkgxray",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
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",
package/src/auditor.js CHANGED
@@ -175,6 +175,7 @@ function auditEvidence(input) {
175
175
 
176
176
  const verdict = decideVerdict(findings, evidence);
177
177
  const grading = gradeEvidence(findings, evidence);
178
+ const riskBands = computeRiskBands(findings);
178
179
  return {
179
180
  verdict,
180
181
  grade: grading.grade,
@@ -182,10 +183,58 @@ function auditEvidence(input) {
182
183
  parameters: grading.parameters,
183
184
  summary: summarizeVerdict(verdict, findings),
184
185
  packageName: evidence.packageName || null,
186
+ riskBands,
185
187
  findings: findings.sort(compareFindings)
186
188
  };
187
189
  }
188
190
 
191
+ // Maps the granular finding categories the auditor produces into a smaller
192
+ // set of human-readable "bands" so the verdict explainer can say things like
193
+ // "review because: lifecycle-script + dynamic-eval" instead of dumping the
194
+ // raw category list.
195
+ const BAND_DEFINITIONS = [
196
+ { band: "prompt-injection", label: "prompt-injection", categories: ["injection-attempt"], rationale: "README/docs contain text aimed at instructing an LLM auditor." },
197
+ { band: "credential-access", label: "credential-access", categories: ["credential-access"], rationale: "Reads a path to a credential / wallet / key store near a filesystem read." },
198
+ { band: "persistence", label: "persistence", categories: ["persistence"], rationale: "Writes to a shell rc, crontab, launchagent, systemd unit, or Windows Run key." },
199
+ { band: "exfiltration", label: "network-exfiltration", categories: ["network-exfil-or-loader"], rationale: "Code reaches a hardcoded public IP / shortener / webhook from a file that also has exec or net capability." },
200
+ { band: "obfuscation", label: "obfuscation", categories: ["obfuscation"], rationale: "Large encoded blob co-located with an execution primitive — classic malware shape." },
201
+ { band: "known-vulnerability", label: "known-vulnerability", categories: ["known-vulnerability"], rationale: "OSV reports this package/version as affected by a published vulnerability." },
202
+ { band: "lifecycle-script", label: "lifecycle-script", categories: ["install-hook"], rationale: "Runs a script at install time with the installing user's privileges." },
203
+ { band: "dynamic-eval", label: "dynamic-eval", categories: ["code-execution"], severityMin: "medium", rationale: "Uses eval / new Function / vm — can execute strings as code at runtime." },
204
+ { band: "bulk-env", label: "bulk-env-access", categories: ["environment-access"], rationale: "Reads the entire process environment in bulk; risky paired with network." },
205
+ { band: "clipboard", label: "clipboard-access", categories: ["data-access"], rationale: "Reads or writes the system clipboard — can expose copied secrets." },
206
+ { band: "incomplete-evidence", label: "incomplete-evidence", categories: ["missing-evidence", "missing-package-json", "package-metadata"], rationale: "Source or package.json was missing or unparseable — cannot rule the package safe." },
207
+ { band: "missing-metadata", label: "missing-metadata", categories: ["missing-metadata", "supply-chain-signal"], rationale: "Provenance metadata (npm registry / GitHub) absent or weak; cross-checks skipped." }
208
+ ];
209
+
210
+ const SEVERITY_RANK = { info: 0, low: 1, medium: 2, high: 3 };
211
+
212
+ function computeRiskBands(findings) {
213
+ const result = [];
214
+ for (const def of BAND_DEFINITIONS) {
215
+ const matched = findings.filter((finding) => {
216
+ if (!def.categories.includes(finding.category)) return false;
217
+ if (def.severityMin && SEVERITY_RANK[finding.severity] < SEVERITY_RANK[def.severityMin]) return false;
218
+ return true;
219
+ });
220
+ if (matched.length === 0) continue;
221
+ const severity = matched.reduce(
222
+ (max, f) => (SEVERITY_RANK[f.severity] > SEVERITY_RANK[max] ? f.severity : max),
223
+ "info"
224
+ );
225
+ const examples = matched.slice(0, 3).map((f) => f.file);
226
+ result.push({
227
+ band: def.band,
228
+ label: def.label,
229
+ severity,
230
+ count: matched.length,
231
+ examples,
232
+ rationale: def.rationale
233
+ });
234
+ }
235
+ return result.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
236
+ }
237
+
189
238
  function auditMetadata(evidence, findings) {
190
239
  const packageJson = findPackageJson(evidence.sourceFiles);
191
240
  if (packageJson) {
@@ -773,6 +822,22 @@ function renderMarkdown(report) {
773
822
  lines.push(`Package: \`${report.packageName}\``, "");
774
823
  }
775
824
 
825
+ if (report.riskBands && report.riskBands.length > 0) {
826
+ const verb = report.verdict === "block"
827
+ ? "Block because"
828
+ : report.verdict === "review"
829
+ ? "Review because"
830
+ : "Notes";
831
+ lines.push(`${verb}:`);
832
+ for (const band of report.riskBands) {
833
+ const examples = band.examples && band.examples.length > 0
834
+ ? ` (${band.examples.slice(0, 2).map((e) => `\`${e}\``).join(", ")}${band.count > band.examples.length ? `, +${band.count - band.examples.length} more` : ""})`
835
+ : "";
836
+ lines.push(`- **${band.severity.toUpperCase()} ${band.label}** — ${band.rationale}${examples}`);
837
+ }
838
+ lines.push("");
839
+ }
840
+
776
841
  lines.push("Parameter grades:");
777
842
  for (const [name, parameter] of Object.entries(report.parameters)) {
778
843
  lines.push(