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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkgxray",
3
- "version": "0.8.0",
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
- // Files in the npm tarball but NOT in the github repo at the matching
168
- // ref. If the package has a build script, root-level JS at non-source
169
- // paths is probably bundled / generated and we can't reliably catch
170
- // tampering theredemote to silent. We DO still surface extras in
171
- // paths that look like source trees (`src/`, `lib/`, `tests/`,
172
- // `scripts/`) since those should be 1:1 even with a build step.
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 signalgithub 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
- const category = isBuildOutput(rel)
175
- ? hasBuildScript ? "expected-build-output" : "extra-build-output"
176
- : isSourceFile(rel)
177
- ? hasBuildScript && !inLikelySourceDir
178
- ? "expected-build-output"
179
- : "extra-source"
180
- : "extra-other";
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: path.basename(parsed.path)
251
+ packageName,
252
+ version,
253
+ npmMetadata
231
254
  };
232
255
  }
233
256