@patchstack/connect 0.3.30 → 0.3.32
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/AGENT-INSTALL.md +180 -20
- package/README.md +17 -3
- package/dist/{chunk-LLKP5EJS.js → chunk-3G2I6QL6.js} +1 -1
- package/dist/chunk-3G2I6QL6.js.map +1 -0
- package/dist/cli.js +309 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +48 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +48 -18
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +2749 -1373
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +316 -0
- package/dist/protect.d.ts +134 -18
- package/dist/protect.edge.js +2776 -1412
- package/dist/protect.edge.js.map +3 -4
- package/dist/protect.js +2697 -1355
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-ZSP76JWQ.js → refresh-manifest-SWCPQ52Z.js} +49 -19
- package/dist/refresh-manifest-SWCPQ52Z.js.map +1 -0
- package/package.json +44 -11
- package/dist/chunk-LLKP5EJS.js.map +0 -1
- package/dist/refresh-manifest-ZSP76JWQ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as readFileSync14, writeFileSync as
|
|
4
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
5
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
6
|
|
|
6
7
|
// src/parsers/index.ts
|
|
7
8
|
import { access } from "fs/promises";
|
|
@@ -330,7 +331,7 @@ async function walkNodeModules(cwd) {
|
|
|
330
331
|
seen.set(await realpath(cwd), -1);
|
|
331
332
|
} catch {
|
|
332
333
|
}
|
|
333
|
-
return dedupeByShallowest(await walk(root, 0, seen));
|
|
334
|
+
return dedupeByShallowest(await walk(root, 0, seen, cwd));
|
|
334
335
|
}
|
|
335
336
|
function dedupeByShallowest(found) {
|
|
336
337
|
const best = /* @__PURE__ */ new Map();
|
|
@@ -341,7 +342,7 @@ function dedupeByShallowest(found) {
|
|
|
341
342
|
return [...best.values()].map(({ entry }) => entry);
|
|
342
343
|
}
|
|
343
344
|
var CONCURRENCY = 32;
|
|
344
|
-
async function walk(dir, depth, seen) {
|
|
345
|
+
async function walk(dir, depth, seen, root) {
|
|
345
346
|
let names;
|
|
346
347
|
try {
|
|
347
348
|
names = await readdir(dir);
|
|
@@ -353,7 +354,7 @@ async function walk(dir, depth, seen) {
|
|
|
353
354
|
const fullPath = path3.join(dir, name);
|
|
354
355
|
const resolved = await claim(fullPath, seen, depth);
|
|
355
356
|
if (resolved === null) return [];
|
|
356
|
-
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
|
|
357
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
|
|
357
358
|
if (name.startsWith("@")) {
|
|
358
359
|
let subNames;
|
|
359
360
|
try {
|
|
@@ -366,16 +367,16 @@ async function walk(dir, depth, seen) {
|
|
|
366
367
|
const scopedDir = path3.join(fullPath, sub);
|
|
367
368
|
const scopedReal = await claim(scopedDir, seen, depth);
|
|
368
369
|
if (scopedReal === null) return [];
|
|
369
|
-
return packageAndNested(scopedDir, scopedReal, depth, seen);
|
|
370
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen, root);
|
|
370
371
|
});
|
|
371
372
|
return scoped.flat();
|
|
372
373
|
}
|
|
373
|
-
return packageAndNested(fullPath, resolved, depth, seen);
|
|
374
|
+
return packageAndNested(fullPath, resolved, depth, seen, root);
|
|
374
375
|
});
|
|
375
376
|
return perEntry.flat();
|
|
376
377
|
}
|
|
377
|
-
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
378
|
-
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
378
|
+
async function packageAndNested(pkgDir, resolved, depth, seen, root) {
|
|
379
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
|
|
379
380
|
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
380
381
|
}
|
|
381
382
|
async function mapLimit(items, fn) {
|
|
@@ -390,7 +391,10 @@ async function mapLimit(items, fn) {
|
|
|
390
391
|
await Promise.all(workers);
|
|
391
392
|
return results;
|
|
392
393
|
}
|
|
393
|
-
|
|
394
|
+
function installLocation(root, pkgDir, impl = path3) {
|
|
395
|
+
return impl.relative(root, pkgDir).split(impl.sep).join("/");
|
|
396
|
+
}
|
|
397
|
+
async function readPackage(pkgDir, depth, root) {
|
|
394
398
|
let raw;
|
|
395
399
|
try {
|
|
396
400
|
raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
|
|
@@ -409,14 +413,14 @@ async function readPackage(pkgDir, depth) {
|
|
|
409
413
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
410
414
|
return null;
|
|
411
415
|
}
|
|
412
|
-
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
416
|
+
return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
|
|
413
417
|
}
|
|
414
|
-
async function walkNested(pkgDir, depth, seen) {
|
|
418
|
+
async function walkNested(pkgDir, depth, seen, root) {
|
|
415
419
|
const nested = path3.join(pkgDir, "node_modules");
|
|
416
420
|
if (await claim(nested, seen, depth + 1) === null) {
|
|
417
421
|
return [];
|
|
418
422
|
}
|
|
419
|
-
return walk(nested, depth + 1, seen);
|
|
423
|
+
return walk(nested, depth + 1, seen, root);
|
|
420
424
|
}
|
|
421
425
|
async function claim(dir, seen, depth) {
|
|
422
426
|
let resolved;
|
|
@@ -1059,21 +1063,33 @@ async function exists(filePath) {
|
|
|
1059
1063
|
}
|
|
1060
1064
|
|
|
1061
1065
|
// src/normalize.ts
|
|
1062
|
-
function buildWirePayload(manifest) {
|
|
1066
|
+
function buildWirePayload(manifest, options = {}) {
|
|
1067
|
+
const withPaths = options.installPaths === true;
|
|
1063
1068
|
const seen = /* @__PURE__ */ new Map();
|
|
1064
1069
|
const wirePackages = [];
|
|
1070
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
1071
|
+
let entriesWithoutPath = 0;
|
|
1065
1072
|
for (const entry of manifest.packages) {
|
|
1073
|
+
const identity = `${entry.name}@${entry.version}`;
|
|
1074
|
+
const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
|
|
1075
|
+
if (location === void 0) entriesWithoutPath++;
|
|
1076
|
+
const existing = byIdentity.get(identity);
|
|
1077
|
+
if (existing) {
|
|
1078
|
+
if (location !== void 0) addPath(existing, location);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1066
1081
|
const versions = seen.get(entry.name);
|
|
1067
1082
|
if (versions) {
|
|
1068
|
-
if (versions.has(entry.version)) {
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
1083
|
versions.add(entry.version);
|
|
1072
1084
|
} else {
|
|
1073
1085
|
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
1074
1086
|
}
|
|
1075
|
-
|
|
1087
|
+
const wire6 = { name: entry.name, version: entry.version };
|
|
1088
|
+
if (location !== void 0) addPath(wire6, location);
|
|
1089
|
+
byIdentity.set(identity, wire6);
|
|
1090
|
+
wirePackages.push(wire6);
|
|
1076
1091
|
}
|
|
1092
|
+
for (const wire6 of wirePackages) wire6.paths?.sort();
|
|
1077
1093
|
wirePackages.sort((a, b) => {
|
|
1078
1094
|
if (a.name === b.name) {
|
|
1079
1095
|
return compareVersions(a.version, b.version);
|
|
@@ -1087,7 +1103,18 @@ function buildWirePayload(manifest) {
|
|
|
1087
1103
|
}
|
|
1088
1104
|
}
|
|
1089
1105
|
return {
|
|
1090
|
-
payload: {
|
|
1106
|
+
payload: {
|
|
1107
|
+
ecosystem: manifest.ecosystem,
|
|
1108
|
+
packages: wirePackages,
|
|
1109
|
+
// Every entry had a location, so a missing path anywhere means the package is not installed there.
|
|
1110
|
+
// One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
|
|
1111
|
+
// no way to tell which entry was the incomplete one.
|
|
1112
|
+
//
|
|
1113
|
+
// No separate check for "locations were not requested": with them off every entry counts as without
|
|
1114
|
+
// one, so this is already false — which is the default, and the safe direction. An explicit
|
|
1115
|
+
// `withPaths &&` here was a guard no test could distinguish from its absence.
|
|
1116
|
+
installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
|
|
1117
|
+
},
|
|
1091
1118
|
stats: {
|
|
1092
1119
|
uniqueNames: seen.size,
|
|
1093
1120
|
duplicateNames,
|
|
@@ -1095,6 +1122,10 @@ function buildWirePayload(manifest) {
|
|
|
1095
1122
|
}
|
|
1096
1123
|
};
|
|
1097
1124
|
}
|
|
1125
|
+
function addPath(wire6, installPath) {
|
|
1126
|
+
if (wire6.paths === void 0) wire6.paths = [installPath];
|
|
1127
|
+
else if (!wire6.paths.includes(installPath)) wire6.paths.push(installPath);
|
|
1128
|
+
}
|
|
1098
1129
|
function compareVersions(a, b) {
|
|
1099
1130
|
if (a === b) {
|
|
1100
1131
|
return 0;
|
|
@@ -2053,38 +2084,83 @@ var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
|
|
|
2053
2084
|
var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
|
|
2054
2085
|
var REGION_CLOSE = "{/* #endregion patchstack */}";
|
|
2055
2086
|
var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
|
|
2087
|
+
function findJsxShellAnchor(source, tagName) {
|
|
2088
|
+
const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, "gm");
|
|
2089
|
+
let candidate;
|
|
2090
|
+
while ((candidate = candidates.exec(source)) !== null) {
|
|
2091
|
+
let quote = null;
|
|
2092
|
+
let escaped = false;
|
|
2093
|
+
let braces = 0;
|
|
2094
|
+
let blockComment = false;
|
|
2095
|
+
let lineComment = false;
|
|
2096
|
+
for (let index = candidates.lastIndex; index < source.length; index += 1) {
|
|
2097
|
+
const char = source[index];
|
|
2098
|
+
const next = source[index + 1];
|
|
2099
|
+
if (lineComment) {
|
|
2100
|
+
if (char === "\n") lineComment = false;
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (blockComment) {
|
|
2104
|
+
if (char === "*" && next === "/") {
|
|
2105
|
+
blockComment = false;
|
|
2106
|
+
index += 1;
|
|
2107
|
+
}
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
if (quote !== null) {
|
|
2111
|
+
if (escaped) {
|
|
2112
|
+
escaped = false;
|
|
2113
|
+
} else if (char === "\\") {
|
|
2114
|
+
escaped = true;
|
|
2115
|
+
} else if (char === quote) {
|
|
2116
|
+
quote = null;
|
|
2117
|
+
}
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2121
|
+
quote = char;
|
|
2122
|
+
} else if (braces > 0 && char === "/" && next === "*") {
|
|
2123
|
+
blockComment = true;
|
|
2124
|
+
index += 1;
|
|
2125
|
+
} else if (braces > 0 && char === "/" && next === "/") {
|
|
2126
|
+
lineComment = true;
|
|
2127
|
+
index += 1;
|
|
2128
|
+
} else if (char === "{") {
|
|
2129
|
+
braces += 1;
|
|
2130
|
+
} else if (char === "}") {
|
|
2131
|
+
if (braces === 0) break;
|
|
2132
|
+
braces -= 1;
|
|
2133
|
+
} else if (char === ">" && braces === 0) {
|
|
2134
|
+
const openingTag = source.slice(candidate.index, index + 1);
|
|
2135
|
+
if (/\/\s*>$/.test(openingTag)) break;
|
|
2136
|
+
return { end: index + 1, indent: candidate[1] ?? "" };
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2056
2142
|
function ensureMarkerInJsxShell(source, framework) {
|
|
2057
2143
|
if (!hasJsxShell(framework)) {
|
|
2058
2144
|
return { source, action: "unsupported" };
|
|
2059
2145
|
}
|
|
2060
2146
|
const stripped = source.replace(REGION_RE, "");
|
|
2061
2147
|
if (stripped.includes(PROD_MARKER_GLOBAL)) {
|
|
2062
|
-
return { source, action: "manual" };
|
|
2148
|
+
return { source: stripped, action: "manual" };
|
|
2063
2149
|
}
|
|
2064
2150
|
const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
action: "added"
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
|
-
for (const anchor of [/^([ \t]*)<head>[ \t]*$/m, /^([ \t]*)<body>[ \t]*$/m]) {
|
|
2075
|
-
const match = stripped.match(anchor);
|
|
2076
|
-
if (match?.index === void 0) {
|
|
2077
|
-
continue;
|
|
2078
|
-
}
|
|
2079
|
-
const end = match.index + match[0].length;
|
|
2080
|
-
const indent = `${match[1] ?? ""} `;
|
|
2151
|
+
for (const tagName of ["head", "body"]) {
|
|
2152
|
+
const anchor = findJsxShellAnchor(stripped, tagName);
|
|
2153
|
+
if (anchor === null) continue;
|
|
2154
|
+
const indent = `${anchor.indent} `;
|
|
2155
|
+
const remainder = stripped.slice(anchor.end);
|
|
2156
|
+
const separator = remainder.startsWith("\n") || remainder.startsWith("\r") ? "" : "\n";
|
|
2081
2157
|
return {
|
|
2082
|
-
source: `${stripped.slice(0, end)}
|
|
2083
|
-
${block(indent)}${
|
|
2158
|
+
source: `${stripped.slice(0, anchor.end)}
|
|
2159
|
+
${block(indent)}${separator}${remainder}`,
|
|
2084
2160
|
action: "added"
|
|
2085
2161
|
};
|
|
2086
2162
|
}
|
|
2087
|
-
return { source, action: "no-anchor" };
|
|
2163
|
+
return { source: stripped, action: "no-anchor" };
|
|
2088
2164
|
}
|
|
2089
2165
|
function ensureSourceMarker(cwd, shell, framework) {
|
|
2090
2166
|
if (shell === null) {
|
|
@@ -2093,7 +2169,7 @@ function ensureSourceMarker(cwd, shell, framework) {
|
|
|
2093
2169
|
const file = path7.resolve(cwd, shell);
|
|
2094
2170
|
const before = readFileSync(file, "utf8");
|
|
2095
2171
|
const result = ensureMarkerInJsxShell(before, framework);
|
|
2096
|
-
if (result.source !== before
|
|
2172
|
+
if (result.source !== before) {
|
|
2097
2173
|
writeFileSync(file, result.source);
|
|
2098
2174
|
}
|
|
2099
2175
|
return { ...result, shell };
|
|
@@ -4078,6 +4154,9 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
4078
4154
|
return waitForApproval(config, started.pending, deps);
|
|
4079
4155
|
}
|
|
4080
4156
|
|
|
4157
|
+
// src/map-command.ts
|
|
4158
|
+
import { writeFileSync as writeFileSync8 } from "fs";
|
|
4159
|
+
|
|
4081
4160
|
// src/map/ts-loader.ts
|
|
4082
4161
|
import { createRequire } from "module";
|
|
4083
4162
|
import { pathToFileURL } from "url";
|
|
@@ -4485,7 +4564,30 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
4485
4564
|
"temp",
|
|
4486
4565
|
"__pycache__"
|
|
4487
4566
|
]);
|
|
4488
|
-
|
|
4567
|
+
var SKIP_IS_DEFINITIONAL = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
4568
|
+
var PROBE_BUDGET = 4096;
|
|
4569
|
+
function probeForSource(dir, budget) {
|
|
4570
|
+
let entries;
|
|
4571
|
+
try {
|
|
4572
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
4573
|
+
} catch {
|
|
4574
|
+
return "unknown";
|
|
4575
|
+
}
|
|
4576
|
+
const dirs = [];
|
|
4577
|
+
for (const e of entries) {
|
|
4578
|
+
if (budget.left-- <= 0) return "unknown";
|
|
4579
|
+
if (e.isDirectory()) {
|
|
4580
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join13(dir, e.name));
|
|
4581
|
+
} else if (isSourceFile(e.name)) return true;
|
|
4582
|
+
else if (e.isSymbolicLink()) return "unknown";
|
|
4583
|
+
}
|
|
4584
|
+
for (const sub of dirs) {
|
|
4585
|
+
const found = probeForSource(sub, budget);
|
|
4586
|
+
if (found !== false) return found;
|
|
4587
|
+
}
|
|
4588
|
+
return false;
|
|
4589
|
+
}
|
|
4590
|
+
function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
|
|
4489
4591
|
let key;
|
|
4490
4592
|
try {
|
|
4491
4593
|
key = realpathSync(dir);
|
|
@@ -4508,9 +4610,18 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4508
4610
|
}
|
|
4509
4611
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
4510
4612
|
for (const e of entries) {
|
|
4511
|
-
if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".")
|
|
4613
|
+
if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") {
|
|
4614
|
+
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4615
|
+
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4616
|
+
else if (probeable && e.isDirectory()) {
|
|
4617
|
+
const held = probeForSource(join13(dir, e.name), probeBudget);
|
|
4618
|
+
if (held === true) stats.skippedWithSource.push(join13(dir, e.name));
|
|
4619
|
+
else if (held === "unknown") stats.skippedUnknown++;
|
|
4620
|
+
}
|
|
4621
|
+
continue;
|
|
4622
|
+
}
|
|
4512
4623
|
const full = join13(dir, e.name);
|
|
4513
|
-
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
|
|
4624
|
+
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
4514
4625
|
else if (e.isSymbolicLink()) {
|
|
4515
4626
|
let st, real;
|
|
4516
4627
|
try {
|
|
@@ -4524,7 +4635,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4524
4635
|
if (st.isDirectory() || isSourceFile(e.name)) stats.unwalked++;
|
|
4525
4636
|
continue;
|
|
4526
4637
|
}
|
|
4527
|
-
if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
|
|
4638
|
+
if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
4528
4639
|
else if (st.isFile() && isSourceFile(e.name)) {
|
|
4529
4640
|
out.push(full);
|
|
4530
4641
|
stats.discovered++;
|
|
@@ -4847,12 +4958,16 @@ var FS_PACKAGES = ["fs-extra", "graceful-fs", "memfs"];
|
|
|
4847
4958
|
var isFsPackage = (pkg) => /^node:fs(\/promises)?$/.test(pkg) || FS_PACKAGES.includes(pkg);
|
|
4848
4959
|
var EXEC_PACKAGES = ["execa", "cross-spawn", "shelljs", "zx"];
|
|
4849
4960
|
var isExecPackage = (pkg) => pkg === "node:child_process" || EXEC_PACKAGES.includes(pkg);
|
|
4961
|
+
var DESERIALIZE_CALLS = /^unserialize$/;
|
|
4962
|
+
var DESERIALIZE_PACKAGES = ["node-serialize"];
|
|
4963
|
+
var isDeserializePackage = (pkg) => DESERIALIZE_PACKAGES.includes(pkg);
|
|
4850
4964
|
function recognizedSinkKinds(pkg) {
|
|
4851
4965
|
const kinds = [];
|
|
4852
4966
|
if (isDbPackage(pkg)) kinds.push("db");
|
|
4853
4967
|
if (isFsPackage(pkg)) kinds.push("fs");
|
|
4854
4968
|
if (isExecPackage(pkg)) kinds.push("exec");
|
|
4855
4969
|
if (isHttpPackage(pkg)) kinds.push("http");
|
|
4970
|
+
if (isDeserializePackage(pkg)) kinds.push("eval");
|
|
4856
4971
|
return kinds;
|
|
4857
4972
|
}
|
|
4858
4973
|
function collectLocalSinks(sf, ts, bindings, ctx) {
|
|
@@ -4966,6 +5081,9 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
4966
5081
|
if (EXEC_CALLS.test(method) && b.pkg && isExecPackage(b.pkg)) {
|
|
4967
5082
|
push({ kind: "exec", package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
4968
5083
|
}
|
|
5084
|
+
if (DESERIALIZE_CALLS.test(method) && b.pkg && isDeserializePackage(b.pkg)) {
|
|
5085
|
+
push({ kind: "eval", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
5086
|
+
}
|
|
4969
5087
|
if (HTTP_MEMBER_METHODS.has(method)) {
|
|
4970
5088
|
if (b.pkg && isHttpPackage(b.pkg)) {
|
|
4971
5089
|
push({ kind: "http", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
|
|
@@ -4993,6 +5111,9 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
4993
5111
|
push({ kind: "exec", package: pkg, op: name, attribution: "import", ...spanOf(n) });
|
|
4994
5112
|
}
|
|
4995
5113
|
if (name === "eval" && trueGlobal) push({ kind: "eval", op: "eval", attribution: "global", ...spanOf(n) });
|
|
5114
|
+
if (DESERIALIZE_CALLS.test(name) && pkg && isDeserializePackage(pkg)) {
|
|
5115
|
+
push({ kind: "eval", package: pkg, op: name, attribution: "import", ...spanOf(n) });
|
|
5116
|
+
}
|
|
4996
5117
|
}
|
|
4997
5118
|
}
|
|
4998
5119
|
if (ts.isNewExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "Function" && !bindings.locals.has("Function") && !isShadowedByEnclosingBinding(n, "Function", ts)) {
|
|
@@ -5069,6 +5190,7 @@ var CANDIDATE_FAMILIES = {
|
|
|
5069
5190
|
};
|
|
5070
5191
|
function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
5071
5192
|
if (sinkKind === "eval" && method === "Function") return index === total - 1 ? "code" : "args";
|
|
5193
|
+
if (sinkKind === "eval" && method === "unserialize") return index === 0 ? "code" : "unknown";
|
|
5072
5194
|
const table = method ? ARGUMENT_ROLES[sinkKind]?.[method] : void 0;
|
|
5073
5195
|
return table?.[index] ?? "unknown";
|
|
5074
5196
|
}
|
|
@@ -5349,7 +5471,7 @@ function zodShape(node, ts) {
|
|
|
5349
5471
|
}
|
|
5350
5472
|
return shape;
|
|
5351
5473
|
}
|
|
5352
|
-
var REQ_SOURCES = ["body", "query", "params"];
|
|
5474
|
+
var REQ_SOURCES = ["body", "query", "params", "headers", "cookies", "files"];
|
|
5353
5475
|
function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
5354
5476
|
if (!body) return [];
|
|
5355
5477
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -5386,6 +5508,12 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
5386
5508
|
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
|
|
5387
5509
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
5388
5510
|
}
|
|
5511
|
+
if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
|
|
5512
|
+
record(n.argumentExpression.text, sourceOfExpr(n.expression));
|
|
5513
|
+
}
|
|
5514
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) {
|
|
5515
|
+
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
5516
|
+
}
|
|
5389
5517
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
5390
5518
|
const init = unwrap(n.initializer);
|
|
5391
5519
|
if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name.text, bodyReadSource(n.initializer));
|
|
@@ -5410,9 +5538,8 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
5410
5538
|
function sourceOfExpr(e) {
|
|
5411
5539
|
if (isPayloadExpr(e)) return "server-fn-data";
|
|
5412
5540
|
if (ts.isPropertyAccessExpression(e)) {
|
|
5413
|
-
|
|
5414
|
-
if (e.name.text
|
|
5415
|
-
if (e.name.text === "body") return "body";
|
|
5541
|
+
const named = namespaceSource(e.name.text);
|
|
5542
|
+
if (REQ_SOURCES.includes(e.name.text)) return named;
|
|
5416
5543
|
}
|
|
5417
5544
|
if (ts.isIdentifier(e)) {
|
|
5418
5545
|
const recorded = sourceNames.get(e.text);
|
|
@@ -5423,6 +5550,9 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
5423
5550
|
function namespaceSource(key) {
|
|
5424
5551
|
if (key === "query") return "query";
|
|
5425
5552
|
if (key === "params") return "route-param";
|
|
5553
|
+
if (key === "headers") return "header";
|
|
5554
|
+
if (key === "cookies") return "cookie";
|
|
5555
|
+
if (key === "files") return "file";
|
|
5426
5556
|
return "body";
|
|
5427
5557
|
}
|
|
5428
5558
|
function bodyReadSource(init) {
|
|
@@ -5437,6 +5567,9 @@ function spaceOfKey(key) {
|
|
|
5437
5567
|
if (key === "params") return "route-param";
|
|
5438
5568
|
if (key === "body") return "post";
|
|
5439
5569
|
if (key === "data" || key === "input" || key === "payload") return "post";
|
|
5570
|
+
if (key === "headers") return "server";
|
|
5571
|
+
if (key === "cookies") return "cookie";
|
|
5572
|
+
if (key === "files") return "files";
|
|
5440
5573
|
return void 0;
|
|
5441
5574
|
}
|
|
5442
5575
|
function linkedFlows(body, params, inputs, sinks, ts) {
|
|
@@ -6216,7 +6349,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6216
6349
|
} catch {
|
|
6217
6350
|
}
|
|
6218
6351
|
const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
|
|
6219
|
-
const stats = { discovered: 0, unwalked: 0 };
|
|
6352
|
+
const stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 };
|
|
6220
6353
|
const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
|
|
6221
6354
|
const imports = createImportInventory(readPathAliases(cwd));
|
|
6222
6355
|
const invocations = createInvocationInventory();
|
|
@@ -6321,19 +6454,31 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6321
6454
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
6322
6455
|
const importList = imports.list();
|
|
6323
6456
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6457
|
+
const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
|
|
6324
6458
|
const importCoverageGaps = {
|
|
6325
6459
|
unreadableFiles: failed.length,
|
|
6326
6460
|
unscannableFiles: importScanFailures,
|
|
6327
6461
|
unwalkedPaths: stats.unwalked,
|
|
6328
|
-
unresolvableImports
|
|
6462
|
+
unresolvableImports,
|
|
6463
|
+
// A directory skipped by NAME that turned out to hold source. It moved no other counter, so before
|
|
6464
|
+
// this an app whose server lived under `vendor/` was indistinguishable from one with no such
|
|
6465
|
+
// directory — and the inventory still claimed to be complete.
|
|
6466
|
+
skippedDirsWithSource,
|
|
6467
|
+
skippedDirsUnsettled: stats.skippedUnknown
|
|
6329
6468
|
};
|
|
6330
|
-
const environmentalGaps = failed.length + importScanFailures + stats.unwalked;
|
|
6469
|
+
const environmentalGaps = failed.length + importScanFailures + stats.unwalked + stats.skippedWithSource.length + stats.skippedUnknown;
|
|
6331
6470
|
const importsComplete = environmentalGaps === 0 && unresolvableImports === 0;
|
|
6332
6471
|
notes.push("`imports` lists every package the app imports, from ALL source files \u2014 not only files holding an entry point. Absence of a SINK for a package is never evidence; absence of the PACKAGE is evidence only when coverage.importsComplete is true.");
|
|
6333
6472
|
notes.push(`${unmodelled} of ${importList.length} imported package(s) have no recognized sink family (recognizedSinkKinds: []). The extractor models a small set of API families, so for those packages it cannot tell whether input reaches them: a vulnerability in one must stay "needs review" and can never be closed as unreachable using this map.`);
|
|
6334
6473
|
if (environmentalGaps > 0) {
|
|
6335
6474
|
notes.push(`The import inventory is INCOMPLETE: ${failed.length} file(s) could not be read, ${importScanFailures} could not be scanned, and ${stats.unwalked} path(s) could not be walked at all (unreadable directory, broken link, or a symlink leaving the project). A package may therefore be imported without appearing in \`imports\`. Do not read a package's absence as "not imported" while coverage.importsComplete is false.`);
|
|
6336
6475
|
}
|
|
6476
|
+
if (stats.skippedWithSource.length > 0) {
|
|
6477
|
+
notes.push(`${stats.skippedWithSource.length} director(ies) skipped by name hold source that was NOT analyzed: ${skippedDirsWithSource.slice(0, 10).join(", ")}${skippedDirsWithSource.length > 10 ? ", \u2026" : ""}. The walk excludes these names because they are usually generated or vendored, which is a guess about where app source lives \u2014 and here the guess was wrong. Code in them can import, and serve, anything. Point the scan at the directory, or move the app source out of it, before reading any package's absence as evidence.`);
|
|
6478
|
+
}
|
|
6479
|
+
if (stats.skippedUnknown > 0) {
|
|
6480
|
+
notes.push(`${stats.skippedUnknown} skipped director(ies) could not be settled either way \u2014 the probe for source hit its budget or the directory could not be read. Counted against completeness deliberately: "we stopped looking" is not the same answer as "nothing there".`);
|
|
6481
|
+
}
|
|
6337
6482
|
if (unresolvableImports > 0) {
|
|
6338
6483
|
notes.push(`The import inventory is INCOMPLETE for a reason no re-run can fix: ${unresolvableImports} import(s) do not name a resolvable module \u2014 either the specifier is computed at runtime (\`require(REGISTRY[kind])\`) or the loader itself was aliased (\`const r = require\`), so which package is loaded is not knowable from the source. Those imports appear NOWHERE in \`imports\` \u2014 not as an unresolved entry, simply absent \u2014 so a package's absence is not evidence of it being unused. This is a property of the application's code, not a scan failure. Reported conservatively: an aliased loader counts even if every call through it uses a literal.`);
|
|
6339
6484
|
}
|
|
@@ -6416,8 +6561,88 @@ async function buildInputMap(cwd, options = {}) {
|
|
|
6416
6561
|
return { map };
|
|
6417
6562
|
}
|
|
6418
6563
|
|
|
6564
|
+
// src/flags.ts
|
|
6565
|
+
function getStringFlag(flags, name) {
|
|
6566
|
+
const value = flags.get(name);
|
|
6567
|
+
return typeof value === "string" ? value : void 0;
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
// src/map-command.ts
|
|
6571
|
+
async function runMap(flags) {
|
|
6572
|
+
const cwd = getStringFlag(flags, "dir") ?? process.cwd();
|
|
6573
|
+
const { map, error } = await buildInputMap(cwd, {
|
|
6574
|
+
followSymlinks: flags.get("follow-symlinks") === true
|
|
6575
|
+
});
|
|
6576
|
+
if (!map) {
|
|
6577
|
+
console.error(`patchstack: ${error}`);
|
|
6578
|
+
return 1;
|
|
6579
|
+
}
|
|
6580
|
+
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
|
|
6581
|
+
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
|
|
6582
|
+
const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
|
|
6583
|
+
const c = map.coverage;
|
|
6584
|
+
console.error(
|
|
6585
|
+
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
|
|
6586
|
+
);
|
|
6587
|
+
console.error(
|
|
6588
|
+
// All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
|
|
6589
|
+
// simply contain no server entry point (most of a project is client code). Only `skipped` is a
|
|
6590
|
+
// failure to analyse.
|
|
6591
|
+
`patchstack: ${c.filesDiscovered} file(s) found \u2014 ${c.filesParsed} analysed, ${c.filesPreFiltered} skipped (no server entry point)` + (c.filesSkipped ? `, ${c.filesSkipped} could not be analysed` : "") + `. DETECTED surface only \u2014 static analysis is best-effort; every flow carries the tier it was established at ("exact-local" and "transformed-local" are proven; "imported", "heuristic" and "unknown" are not).`
|
|
6592
|
+
);
|
|
6593
|
+
const invoked = map.apiInvocations ?? [];
|
|
6594
|
+
if (invoked.length > 0) {
|
|
6595
|
+
const c2 = map.coverage;
|
|
6596
|
+
const dependency = c2.callsDependency ?? 0;
|
|
6597
|
+
const ambiguous = c2.callsAmbiguous ?? 0;
|
|
6598
|
+
const denominator = dependency + ambiguous;
|
|
6599
|
+
const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
|
|
6600
|
+
console.error(
|
|
6601
|
+
`patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
|
|
6602
|
+
);
|
|
6603
|
+
}
|
|
6604
|
+
const imported = map.imports ?? [];
|
|
6605
|
+
if (imported.length > 0) {
|
|
6606
|
+
const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6607
|
+
console.error(
|
|
6608
|
+
`patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
|
|
6609
|
+
);
|
|
6610
|
+
}
|
|
6611
|
+
const json = JSON.stringify(map, null, 2);
|
|
6612
|
+
const out = getStringFlag(flags, "out");
|
|
6613
|
+
if (out) {
|
|
6614
|
+
writeFileSync8(out, json);
|
|
6615
|
+
console.error(`patchstack: wrote ${out}`);
|
|
6616
|
+
} else if (flags.get("upload") !== true) {
|
|
6617
|
+
console.log(json);
|
|
6618
|
+
}
|
|
6619
|
+
if (flags.get("upload") === true) {
|
|
6620
|
+
if (map.endpoints.length === 0) {
|
|
6621
|
+
console.error(
|
|
6622
|
+
"patchstack: no server entry points were recognized \u2014 uploading the import inventory and coverage notes anyway, so a vulnerability can still be judged imported or not. Nothing here can decide whether a request reaches it."
|
|
6623
|
+
);
|
|
6624
|
+
}
|
|
6625
|
+
const config = await resolveConfig({
|
|
6626
|
+
cwd,
|
|
6627
|
+
cliSiteUuid: getStringFlag(flags, "site-uuid"),
|
|
6628
|
+
cliEndpoint: getStringFlag(flags, "endpoint")
|
|
6629
|
+
});
|
|
6630
|
+
const outcome = await postInputMap(config, map);
|
|
6631
|
+
if (outcome.result === "stored") {
|
|
6632
|
+
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
6633
|
+
} else if (outcome.result === "unchanged") {
|
|
6634
|
+
console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
|
|
6635
|
+
} else if (outcome.result === "skipped") {
|
|
6636
|
+
console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
|
|
6637
|
+
} else {
|
|
6638
|
+
console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
return 0;
|
|
6642
|
+
}
|
|
6643
|
+
|
|
6419
6644
|
// src/setup.ts
|
|
6420
|
-
import { readFileSync as readFileSync13, writeFileSync as
|
|
6645
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
6421
6646
|
import path11 from "path";
|
|
6422
6647
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
6423
6648
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
@@ -6486,7 +6711,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
6486
6711
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
6487
6712
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
6488
6713
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
6489
|
-
|
|
6714
|
+
writeFileSync9(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
6490
6715
|
if (build === void 0 || build.trim().length === 0) {
|
|
6491
6716
|
return {
|
|
6492
6717
|
changed: true,
|
|
@@ -6583,10 +6808,19 @@ Usage:
|
|
|
6583
6808
|
the old one must be updated. Not usable in CI
|
|
6584
6809
|
patchstack-connect help Print this message
|
|
6585
6810
|
|
|
6811
|
+
Global:
|
|
6812
|
+
--version Print the installed version of this package and exit
|
|
6813
|
+
--help Print this help and exit
|
|
6814
|
+
|
|
6586
6815
|
Options (for scan, setup, status, and uninstall):
|
|
6587
6816
|
--site-uuid <uuid> Override the configured site UUID
|
|
6588
6817
|
--endpoint <url> Override the API endpoint
|
|
6589
6818
|
--dry-run (scan only) Show the payload without posting
|
|
6819
|
+
--install-paths (scan only) Also send where each package is installed
|
|
6820
|
+
in the dependency tree (node_modules/lodash, and the
|
|
6821
|
+
same under a workspace that pins its own copy), so an
|
|
6822
|
+
advisory can be matched to the copy your code actually
|
|
6823
|
+
loads. Off by default; never source file paths
|
|
6590
6824
|
|
|
6591
6825
|
Options (for mark-build):
|
|
6592
6826
|
--dir <path> Build output directory (default: auto-detect
|
|
@@ -6651,10 +6885,6 @@ function parseArgs(argv) {
|
|
|
6651
6885
|
flags
|
|
6652
6886
|
};
|
|
6653
6887
|
}
|
|
6654
|
-
function getStringFlag(flags, name) {
|
|
6655
|
-
const value = flags.get(name);
|
|
6656
|
-
return typeof value === "string" ? value : void 0;
|
|
6657
|
-
}
|
|
6658
6888
|
async function runInit(args) {
|
|
6659
6889
|
const uuid = args.positional[0];
|
|
6660
6890
|
if (!uuid) {
|
|
@@ -6752,77 +6982,6 @@ async function runLogin(args) {
|
|
|
6752
6982
|
`);
|
|
6753
6983
|
return 1;
|
|
6754
6984
|
}
|
|
6755
|
-
async function runMap(args) {
|
|
6756
|
-
const cwd = getStringFlag(args.flags, "dir") ?? process.cwd();
|
|
6757
|
-
const { map, error } = await buildInputMap(cwd, {
|
|
6758
|
-
followSymlinks: args.flags.get("follow-symlinks") === true
|
|
6759
|
-
});
|
|
6760
|
-
if (!map) {
|
|
6761
|
-
console.error(`patchstack: ${error}`);
|
|
6762
|
-
return 1;
|
|
6763
|
-
}
|
|
6764
|
-
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
|
|
6765
|
-
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
|
|
6766
|
-
const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
|
|
6767
|
-
const c = map.coverage;
|
|
6768
|
-
console.error(
|
|
6769
|
-
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
|
|
6770
|
-
);
|
|
6771
|
-
console.error(
|
|
6772
|
-
// All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
|
|
6773
|
-
// simply contain no server entry point (most of a project is client code). Only `skipped` is a
|
|
6774
|
-
// failure to analyse.
|
|
6775
|
-
`patchstack: ${c.filesDiscovered} file(s) found \u2014 ${c.filesParsed} analysed, ${c.filesPreFiltered} skipped (no server entry point)` + (c.filesSkipped ? `, ${c.filesSkipped} could not be analysed` : "") + `. DETECTED surface only \u2014 static analysis is best-effort; every flow carries the tier it was established at ("exact-local" and "transformed-local" are proven; "imported", "heuristic" and "unknown" are not).`
|
|
6776
|
-
);
|
|
6777
|
-
const invoked = map.apiInvocations ?? [];
|
|
6778
|
-
if (invoked.length > 0) {
|
|
6779
|
-
const c2 = map.coverage;
|
|
6780
|
-
const dependency = c2.callsDependency ?? 0;
|
|
6781
|
-
const ambiguous = c2.callsAmbiguous ?? 0;
|
|
6782
|
-
const denominator = dependency + ambiguous;
|
|
6783
|
-
const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
|
|
6784
|
-
console.error(
|
|
6785
|
-
`patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
|
|
6786
|
-
);
|
|
6787
|
-
}
|
|
6788
|
-
const imported = map.imports ?? [];
|
|
6789
|
-
if (imported.length > 0) {
|
|
6790
|
-
const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6791
|
-
console.error(
|
|
6792
|
-
`patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
|
|
6793
|
-
);
|
|
6794
|
-
}
|
|
6795
|
-
const json = JSON.stringify(map, null, 2);
|
|
6796
|
-
const out = getStringFlag(args.flags, "out");
|
|
6797
|
-
if (out) {
|
|
6798
|
-
writeFileSync9(out, json);
|
|
6799
|
-
console.error(`patchstack: wrote ${out}`);
|
|
6800
|
-
} else if (args.flags.get("upload") !== true) {
|
|
6801
|
-
console.log(json);
|
|
6802
|
-
}
|
|
6803
|
-
if (args.flags.get("upload") === true) {
|
|
6804
|
-
if (map.endpoints.length === 0) {
|
|
6805
|
-
console.error("patchstack: nothing to upload \u2014 no server entry points were detected.");
|
|
6806
|
-
return 0;
|
|
6807
|
-
}
|
|
6808
|
-
const config = await resolveConfig({
|
|
6809
|
-
cwd,
|
|
6810
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
6811
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
6812
|
-
});
|
|
6813
|
-
const outcome = await postInputMap(config, map);
|
|
6814
|
-
if (outcome.result === "stored") {
|
|
6815
|
-
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
6816
|
-
} else if (outcome.result === "unchanged") {
|
|
6817
|
-
console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
|
|
6818
|
-
} else if (outcome.result === "skipped") {
|
|
6819
|
-
console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
|
|
6820
|
-
} else {
|
|
6821
|
-
console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
|
|
6822
|
-
}
|
|
6823
|
-
}
|
|
6824
|
-
return 0;
|
|
6825
|
-
}
|
|
6826
6985
|
async function runScan(args, options = {}) {
|
|
6827
6986
|
const dryRun = args.flags.get("dry-run") === true;
|
|
6828
6987
|
const config = await resolveConfig({
|
|
@@ -6834,10 +6993,17 @@ async function runScan(args, options = {}) {
|
|
|
6834
6993
|
for (const warning of manifest.warnings ?? []) {
|
|
6835
6994
|
console.warn(`patchstack: ${warning}`);
|
|
6836
6995
|
}
|
|
6837
|
-
const
|
|
6996
|
+
const installPaths = args.flags.get("install-paths") === true;
|
|
6997
|
+
const { payload, stats } = buildWirePayload(manifest, { installPaths });
|
|
6838
6998
|
console.log(
|
|
6839
6999
|
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
6840
7000
|
);
|
|
7001
|
+
if (installPaths) {
|
|
7002
|
+
const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
|
|
7003
|
+
console.log(
|
|
7004
|
+
payload.installPathsComplete ? `Including each package's install location in the dependency tree (--install-paths), for all ${located}.` : `Including install locations (--install-paths), for ${located} of ${payload.packages.length} \u2014 this lockfile format does not record them for the rest, which will be reported as "not recorded" rather than "not installed there".`
|
|
7005
|
+
);
|
|
7006
|
+
}
|
|
6841
7007
|
console.log(
|
|
6842
7008
|
`Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
|
|
6843
7009
|
);
|
|
@@ -7330,7 +7496,7 @@ async function runMarkBuild(args) {
|
|
|
7330
7496
|
after = ensured.html;
|
|
7331
7497
|
}
|
|
7332
7498
|
if (after !== before) {
|
|
7333
|
-
|
|
7499
|
+
writeFileSync10(file, after);
|
|
7334
7500
|
marked += 1;
|
|
7335
7501
|
}
|
|
7336
7502
|
}
|
|
@@ -7340,8 +7506,21 @@ async function runMarkBuild(args) {
|
|
|
7340
7506
|
);
|
|
7341
7507
|
return 0;
|
|
7342
7508
|
}
|
|
7509
|
+
function packageVersion() {
|
|
7510
|
+
try {
|
|
7511
|
+
const require2 = createRequire2(import.meta.url);
|
|
7512
|
+
const manifest = require2("../package.json");
|
|
7513
|
+
return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
|
|
7514
|
+
} catch {
|
|
7515
|
+
return "unknown";
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7343
7518
|
async function main() {
|
|
7344
7519
|
const args = parseArgs(process.argv);
|
|
7520
|
+
if (args.flags.has("version") || args.command === "version") {
|
|
7521
|
+
console.log(packageVersion());
|
|
7522
|
+
return 0;
|
|
7523
|
+
}
|
|
7345
7524
|
if (args.flags.has("help") || args.command === "help" || args.command === null) {
|
|
7346
7525
|
console.log(HELP);
|
|
7347
7526
|
return 0;
|
|
@@ -7368,7 +7547,7 @@ async function main() {
|
|
|
7368
7547
|
case "setup":
|
|
7369
7548
|
return runSetup(args);
|
|
7370
7549
|
case "map":
|
|
7371
|
-
return runMap(args);
|
|
7550
|
+
return runMap(args.flags);
|
|
7372
7551
|
case "login":
|
|
7373
7552
|
return runLogin(args);
|
|
7374
7553
|
default:
|