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.
- package/package.json +1 -1
- package/src/auditor.js +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkgxray",
|
|
3
|
-
"version": "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(
|