pkgxray 0.8.0 → 0.8.1
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/diff.js +30 -13
- package/src/github.js +3 -1
- package/src/quarantine.js +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkgxray",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jack Adams-Lovell",
|
package/src/diff.js
CHANGED
|
@@ -156,6 +156,18 @@ async function diffNpmVsGithub({ npmStagedPath, githubStagedPath, subdir, hasBui
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Pre-compute the set of directories that EXIST in github. We use this to
|
|
160
|
+
// decide whether an extra file is in a "real source dir" (sibling source
|
|
161
|
+
// files exist in github) or in a path github doesn't have at all (more
|
|
162
|
+
// likely build output).
|
|
163
|
+
const ghDirs = new Set();
|
|
164
|
+
for (const ghPath of ghTree.keys()) {
|
|
165
|
+
const parts = ghPath.split("/");
|
|
166
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
167
|
+
ghDirs.add(parts.slice(0, i).join("/"));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
const extraInNpm = [];
|
|
160
172
|
const mismatched = [];
|
|
161
173
|
const matched = [];
|
|
@@ -164,20 +176,25 @@ async function diffNpmVsGithub({ npmStagedPath, githubStagedPath, subdir, hasBui
|
|
|
164
176
|
if (isAlwaysIgnored(rel)) continue;
|
|
165
177
|
const ghEntry = ghTree.get(rel);
|
|
166
178
|
if (!ghEntry) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
179
|
+
const parentDir = rel.includes("/") ? rel.split("/").slice(0, -1).join("/") : "";
|
|
180
|
+
const parentExistsInGh = parentDir === "" || ghDirs.has(parentDir);
|
|
181
|
+
// An extra file inside a directory that exists in github is the strong
|
|
182
|
+
// ATO signal — github has the dir, the attacker just dropped one more
|
|
183
|
+
// file in it. An extra file at a path github doesn't have at all is
|
|
184
|
+
// more likely build output the repo never committed.
|
|
173
185
|
const inLikelySourceDir = /^(?:src|tests?|scripts|spec)\//.test(rel);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
let category;
|
|
187
|
+
if (parentExistsInGh && isSourceFile(rel)) {
|
|
188
|
+
category = "extra-source";
|
|
189
|
+
} else if (isBuildOutput(rel)) {
|
|
190
|
+
category = hasBuildScript ? "expected-build-output" : "extra-build-output";
|
|
191
|
+
} else if (isSourceFile(rel)) {
|
|
192
|
+
category = hasBuildScript && !inLikelySourceDir
|
|
193
|
+
? "expected-build-output"
|
|
194
|
+
: "extra-source";
|
|
195
|
+
} else {
|
|
196
|
+
category = "extra-other";
|
|
197
|
+
}
|
|
181
198
|
extraInNpm.push({ path: rel, category, size: npmEntry.size });
|
|
182
199
|
continue;
|
|
183
200
|
}
|
package/src/github.js
CHANGED
|
@@ -49,7 +49,9 @@ function parseGithubRepo(repository) {
|
|
|
49
49
|
/^github:([^/]+)\/(.+)$/,
|
|
50
50
|
/^(?:https?|git):\/\/github\.com\/([^/]+)\/([^/?#]+)/,
|
|
51
51
|
/^git@github\.com:([^/]+)\/([^/?#]+)/,
|
|
52
|
-
/^ssh:\/\/git@github\.com\/([^/]+)\/([^/?#]+)
|
|
52
|
+
/^ssh:\/\/git@github\.com\/([^/]+)\/([^/?#]+)/,
|
|
53
|
+
// npm shorthand: bare "owner/repo" defaults to GitHub
|
|
54
|
+
/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/
|
|
53
55
|
];
|
|
54
56
|
for (const pattern of patterns) {
|
|
55
57
|
const match = cleaned.match(pattern);
|
package/src/quarantine.js
CHANGED
|
@@ -111,7 +111,7 @@ async function guardExtension(reference, options = {}) {
|
|
|
111
111
|
let npmVsGithubDiff = null;
|
|
112
112
|
if (
|
|
113
113
|
options.githubDiff !== false &&
|
|
114
|
-
resolved.type === "npm" &&
|
|
114
|
+
(resolved.type === "npm" || resolved.type === "local") &&
|
|
115
115
|
githubMetadata && githubMetadata.found &&
|
|
116
116
|
vulnerabilities.length === 0 &&
|
|
117
117
|
Object.keys(sourceFiles).length > 0
|
|
@@ -224,10 +224,33 @@ async function stageReference(reference, stagedPath, options) {
|
|
|
224
224
|
const parsed = parseReference(reference);
|
|
225
225
|
if (parsed.type === "local") {
|
|
226
226
|
await copyLocalPath(parsed.path, stagedPath);
|
|
227
|
+
// Populate npmMetadata from the staged package.json so downstream phases
|
|
228
|
+
// (github metadata cross-check, npm-vs-github diff) can work on local
|
|
229
|
+
// packages too.
|
|
230
|
+
let npmMetadata = null;
|
|
231
|
+
let packageName = path.basename(parsed.path);
|
|
232
|
+
let version = null;
|
|
233
|
+
try {
|
|
234
|
+
const pkg = JSON.parse(await fsp.readFile(path.join(stagedPath, "package.json"), "utf8"));
|
|
235
|
+
packageName = pkg.name || packageName;
|
|
236
|
+
version = pkg.version || null;
|
|
237
|
+
if (pkg.repository) {
|
|
238
|
+
npmMetadata = {
|
|
239
|
+
name: pkg.name || packageName,
|
|
240
|
+
version: pkg.version || null,
|
|
241
|
+
repository: pkg.repository,
|
|
242
|
+
maintainers: []
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// no package.json or unparseable — fine, just no metadata
|
|
247
|
+
}
|
|
227
248
|
return {
|
|
228
249
|
type: "local",
|
|
229
250
|
source: parsed.path,
|
|
230
|
-
packageName
|
|
251
|
+
packageName,
|
|
252
|
+
version,
|
|
253
|
+
npmMetadata
|
|
231
254
|
};
|
|
232
255
|
}
|
|
233
256
|
|