@patchstack/connect 0.5.19 → 0.5.20

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/dist/cli.js CHANGED
@@ -8391,6 +8391,234 @@ function pathFromTainted(node, ts, rootPath, trailing = []) {
8391
8391
  return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request, ...accessor, ...reassigned };
8392
8392
  }
8393
8393
 
8394
+ // src/map/budget.ts
8395
+ var INGEST_LIMITS = {
8396
+ bytes: 2 * 1024 * 1024,
8397
+ nodes: 5e4,
8398
+ depth: 12,
8399
+ endpoints: 2e3,
8400
+ fileLength: 512,
8401
+ inputsPerEndpoint: 500,
8402
+ sinksPerEndpoint: 500,
8403
+ flowsPerEndpoint: 2e3,
8404
+ dependencyInputFlowsPerEndpoint: 100
8405
+ };
8406
+ var RESERVE = { nodes: 8, bytes: 256 };
8407
+ function nodeCount(value) {
8408
+ if (value === null || typeof value !== "object") return 0;
8409
+ let n = 0;
8410
+ for (const item of Object.values(value)) n += 1 + nodeCount(item);
8411
+ return n;
8412
+ }
8413
+ function depthOf(value, depth = 0) {
8414
+ if (value === null || typeof value !== "object") return depth;
8415
+ let deepest = depth;
8416
+ for (const item of Object.values(value)) deepest = Math.max(deepest, depthOf(item, depth + 1));
8417
+ return deepest;
8418
+ }
8419
+ var encoder = new TextEncoder();
8420
+ var byteLength = (value) => encoder.encode(JSON.stringify(value)).length;
8421
+ function ingestProblems(map, limits = INGEST_LIMITS) {
8422
+ const problems = [];
8423
+ const bytes = byteLength(map);
8424
+ const nodes = nodeCount(map);
8425
+ const depth = depthOf(map);
8426
+ if (bytes > limits.bytes - RESERVE.bytes) problems.push(`${bytes} bytes, over the ${limits.bytes - RESERVE.bytes} the upload can carry`);
8427
+ if (nodes > limits.nodes - RESERVE.nodes) problems.push(`${nodes} nodes, over the ${limits.nodes - RESERVE.nodes} the upload can carry`);
8428
+ if (depth > limits.depth) problems.push(`nesting ${depth} levels deep, over ${limits.depth}`);
8429
+ if (map.endpoints.length > limits.endpoints) problems.push(`${map.endpoints.length} endpoints, over ${limits.endpoints}`);
8430
+ map.endpoints.forEach((e, i) => {
8431
+ const at = `endpoint ${i}`;
8432
+ if (typeof e.file !== "string" || e.file.length > limits.fileLength) problems.push(`${at} has a file path over ${limits.fileLength} characters`);
8433
+ if (e.inputs.length > limits.inputsPerEndpoint) problems.push(`${at} has ${e.inputs.length} inputs, over ${limits.inputsPerEndpoint}`);
8434
+ if (e.sinks.length > limits.sinksPerEndpoint) problems.push(`${at} has ${e.sinks.length} sinks, over ${limits.sinksPerEndpoint}`);
8435
+ if (e.flows.length > limits.flowsPerEndpoint) problems.push(`${at} has ${e.flows.length} flows, over ${limits.flowsPerEndpoint}`);
8436
+ if ((e.dependencyInputFlows?.length ?? 0) > limits.dependencyInputFlowsPerEndpoint) problems.push(`${at} has ${e.dependencyInputFlows.length} dependency input links, over ${limits.dependencyInputFlowsPerEndpoint}`);
8437
+ });
8438
+ return problems;
8439
+ }
8440
+ function keepPriority(flow) {
8441
+ if (flow.confidence === "exact-local") return flow.ruleGeneratable ? 3 : 2;
8442
+ if (flow.confidence === "transformed-local") return 1;
8443
+ return 0;
8444
+ }
8445
+ var STAGES = ["unproven", "invocation", "dependency", "transformed", "exact", "generatable", "import"];
8446
+ function budgetDocument(input, limits = INGEST_LIMITS) {
8447
+ let endpointsOmitted = 0;
8448
+ let endpoints = input.endpoints.filter((e) => {
8449
+ const ok = typeof e.file === "string" && e.file.length <= limits.fileLength;
8450
+ if (!ok) endpointsOmitted++;
8451
+ return ok;
8452
+ });
8453
+ if (endpoints.length > limits.endpoints) {
8454
+ endpointsOmitted += endpoints.length - limits.endpoints;
8455
+ endpoints = endpoints.slice(0, limits.endpoints);
8456
+ }
8457
+ endpoints = endpoints.map((e) => capEndpoint(e, limits));
8458
+ let map = { ...input, endpoints };
8459
+ if (endpointsOmitted > 0) map = { ...map, coverage: { ...map.coverage, endpointsOmitted } };
8460
+ const removals = [];
8461
+ map.endpoints.forEach((e, s) => {
8462
+ e.flows.forEach((f, i) => {
8463
+ const p = keepPriority(f);
8464
+ const stage = p === 3 ? "generatable" : p === 2 ? "exact" : p === 1 ? "transformed" : "unproven";
8465
+ removals.push({ stage, section: s, item: i, nodes: 1 + nodeCount(f), bytes: byteLength(f) + 1 });
8466
+ });
8467
+ (e.dependencyInputFlows ?? []).forEach((d, i) => removals.push({ stage: "dependency", section: s, item: i, nodes: 1 + nodeCount(d), bytes: byteLength(d) + 1 }));
8468
+ });
8469
+ (map.apiInvocations ?? []).forEach((v, i) => removals.push({ stage: "invocation", section: -1, item: i, nodes: 1 + nodeCount(v), bytes: byteLength(v) + 1 }));
8470
+ (map.imports ?? []).forEach((v, i) => removals.push({ stage: "import", section: -1, item: i, nodes: 1 + nodeCount(v), bytes: byteLength(v) + 1 }));
8471
+ removals.sort((a, b) => STAGES.indexOf(a.stage) - STAGES.indexOf(b.stage) || b.section - a.section || b.item - a.item);
8472
+ const removed = /* @__PURE__ */ new Set();
8473
+ const key = (r) => `${r.stage === "unproven" || r.stage === "transformed" || r.stage === "exact" || r.stage === "generatable" ? "flow" : r.stage}:${r.section}:${r.item}`;
8474
+ const fitsSize = (doc2) => nodeCount(doc2) <= limits.nodes - RESERVE.nodes && byteLength(doc2) <= limits.bytes - RESERVE.bytes;
8475
+ let doc = withNote(applyRemovals(map, removed));
8476
+ let nodes = nodeCount(doc);
8477
+ let bytes = byteLength(doc);
8478
+ for (const r of removals) {
8479
+ if (nodes <= limits.nodes - RESERVE.nodes && bytes <= limits.bytes - RESERVE.bytes) {
8480
+ doc = withNote(applyRemovals(map, removed));
8481
+ nodes = nodeCount(doc);
8482
+ bytes = byteLength(doc);
8483
+ if (fitsSize(doc)) break;
8484
+ }
8485
+ removed.add(key(r));
8486
+ nodes -= r.nodes;
8487
+ bytes -= r.bytes;
8488
+ }
8489
+ doc = withNote(applyRemovals(map, removed));
8490
+ const problems = ingestProblems(doc, limits);
8491
+ if (problems.length === 0) return doc;
8492
+ return {
8493
+ ...doc,
8494
+ coverage: {
8495
+ ...doc.coverage,
8496
+ notes: [...doc.coverage.notes, `This map cannot be fitted to the size the Patchstack API accepts (${problems.join("; ")}), even after leaving out everything that may be left out. It must not be uploaded.`]
8497
+ }
8498
+ };
8499
+ }
8500
+ function capEndpoint(e, limits) {
8501
+ let flows = e.flows;
8502
+ let droppedProven = 0;
8503
+ let droppedUnproven = 0;
8504
+ const drop = (keep) => {
8505
+ flows = flows.filter((f) => {
8506
+ if (keep(f)) return true;
8507
+ if (isProvenFlow(f.confidence)) droppedProven++;
8508
+ else droppedUnproven++;
8509
+ return false;
8510
+ });
8511
+ };
8512
+ if (flows.length > limits.flowsPerEndpoint) {
8513
+ const kept = new Set(
8514
+ flows.map((f, i) => ({ f, i, p: keepPriority(f) })).sort((a, b) => b.p - a.p || a.i - b.i).slice(0, limits.flowsPerEndpoint).map(({ i }) => i)
8515
+ );
8516
+ const byIndex = new Map(flows.map((f, i) => [f, i]));
8517
+ drop((f) => kept.has(byIndex.get(f)));
8518
+ }
8519
+ const ranked = [...flows].sort((a, b) => keepPriority(b) - keepPriority(a));
8520
+ const pick = (items, limit, idOf) => {
8521
+ if (items.length <= limit) return { kept: items, omitted: 0 };
8522
+ const wanted = /* @__PURE__ */ new Set();
8523
+ for (const f of ranked) {
8524
+ const id = idOf(f);
8525
+ if (id !== void 0 && wanted.size < limit) wanted.add(id);
8526
+ }
8527
+ const chosen = new Set(items.filter((it) => it.id !== void 0 && wanted.has(it.id)));
8528
+ for (const it of items) if (chosen.size < limit) chosen.add(it);
8529
+ return { kept: items.filter((it) => chosen.has(it)), omitted: items.length - chosen.size };
8530
+ };
8531
+ const inputs = pick(e.inputs, limits.inputsPerEndpoint, (f) => f.inputId);
8532
+ const sinks = pick(e.sinks, limits.sinksPerEndpoint, (f) => f.sink?.id);
8533
+ if (inputs.omitted > 0 || sinks.omitted > 0) {
8534
+ const inputIds = new Set(inputs.kept.map((i) => i.id));
8535
+ const sinkIds = new Set(sinks.kept.map((s) => s.id));
8536
+ drop((f) => (f.inputId === void 0 || inputIds.has(f.inputId)) && (f.sink?.id === void 0 || sinkIds.has(f.sink.id)));
8537
+ }
8538
+ let links = e.dependencyInputFlows;
8539
+ let linksTruncated = e.dependencyInputFlowsTruncated === true;
8540
+ if (links !== void 0 && links.length > limits.dependencyInputFlowsPerEndpoint) {
8541
+ links = links.slice(0, limits.dependencyInputFlowsPerEndpoint);
8542
+ linksTruncated = true;
8543
+ }
8544
+ if (flows === e.flows && inputs.omitted === 0 && sinks.omitted === 0 && links === e.dependencyInputFlows) return e;
8545
+ return {
8546
+ ...e,
8547
+ inputs: inputs.kept,
8548
+ sinks: sinks.kept,
8549
+ flows,
8550
+ ...links !== void 0 ? { dependencyInputFlows: links } : {},
8551
+ ...linksTruncated ? { dependencyInputFlowsTruncated: true } : {},
8552
+ ...droppedUnproven > 0 ? { flowsTruncated: true } : {},
8553
+ ...droppedProven > 0 ? { provenFlowsOmitted: (e.provenFlowsOmitted ?? 0) + droppedProven } : {},
8554
+ ...inputs.omitted > 0 ? { inputsOmitted: inputs.omitted } : {},
8555
+ ...sinks.omitted > 0 ? { sinksOmitted: sinks.omitted } : {}
8556
+ };
8557
+ }
8558
+ function applyRemovals(map, removed) {
8559
+ if (removed.size === 0) return map;
8560
+ const endpoints = map.endpoints.map((e, s) => {
8561
+ let proven = 0;
8562
+ let unproven = 0;
8563
+ const flows = e.flows.filter((f, i) => {
8564
+ if (!removed.has(`flow:${s}:${i}`)) return true;
8565
+ if (isProvenFlow(f.confidence)) proven++;
8566
+ else unproven++;
8567
+ return false;
8568
+ });
8569
+ const links = e.dependencyInputFlows?.filter((_, i) => !removed.has(`dependency:${s}:${i}`));
8570
+ const linksDropped = (e.dependencyInputFlows?.length ?? 0) - (links?.length ?? 0);
8571
+ if (proven === 0 && unproven === 0 && linksDropped === 0) return e;
8572
+ return {
8573
+ ...e,
8574
+ flows,
8575
+ ...links !== void 0 ? { dependencyInputFlows: links } : {},
8576
+ ...unproven > 0 ? { flowsTruncated: true } : {},
8577
+ ...proven > 0 ? { provenFlowsOmitted: (e.provenFlowsOmitted ?? 0) + proven } : {},
8578
+ ...linksDropped > 0 ? { dependencyInputFlowsTruncated: true } : {}
8579
+ };
8580
+ });
8581
+ const invocations = map.apiInvocations?.filter((_, i) => !removed.has(`invocation:-1:${i}`));
8582
+ const imports = map.imports?.filter((_, i) => !removed.has(`import:-1:${i}`));
8583
+ const invocationsOmitted = (map.apiInvocations?.length ?? 0) - (invocations?.length ?? 0);
8584
+ const importsOmitted = (map.imports?.length ?? 0) - (imports?.length ?? 0);
8585
+ return {
8586
+ ...map,
8587
+ endpoints,
8588
+ ...invocations !== void 0 ? { apiInvocations: invocations } : {},
8589
+ ...imports !== void 0 ? { imports } : {},
8590
+ coverage: {
8591
+ ...map.coverage,
8592
+ ...invocationsOmitted > 0 ? { apiInvocationsOmitted: invocationsOmitted } : {},
8593
+ // A package missing from a trimmed inventory may still be imported, so the inventory stops licensing
8594
+ // any "not imported" conclusion.
8595
+ ...importsOmitted > 0 ? { importsComplete: false, importCoverageGaps: { ...map.coverage.importCoverageGaps, omittedForSize: importsOmitted } } : {}
8596
+ }
8597
+ };
8598
+ }
8599
+ function withNote(doc) {
8600
+ const parts = [];
8601
+ const proven = doc.endpoints.reduce((n, e) => n + (e.provenFlowsOmitted ?? 0), 0);
8602
+ const marked = doc.endpoints.filter((e) => e.provenFlowsOmitted || e.inputsOmitted || e.sinksOmitted).length;
8603
+ if (proven > 0) parts.push(`${proven} proven flow(s)`);
8604
+ const inputs = doc.endpoints.reduce((n, e) => n + (e.inputsOmitted ?? 0), 0);
8605
+ const sinks = doc.endpoints.reduce((n, e) => n + (e.sinksOmitted ?? 0), 0);
8606
+ if (inputs > 0) parts.push(`${inputs} input(s)`);
8607
+ if (sinks > 0) parts.push(`${sinks} sink(s)`);
8608
+ if (doc.coverage.apiInvocationsOmitted) parts.push(`${doc.coverage.apiInvocationsOmitted} API invocation(s)`);
8609
+ if (doc.coverage.importCoverageGaps?.omittedForSize) parts.push(`${doc.coverage.importCoverageGaps.omittedForSize} import(s)`);
8610
+ if (doc.coverage.endpointsOmitted) parts.push(`${doc.coverage.endpointsOmitted} endpoint(s)`);
8611
+ const links = doc.endpoints.filter((e) => e.dependencyInputFlowsTruncated).length;
8612
+ if (parts.length === 0) return doc;
8613
+ return {
8614
+ ...doc,
8615
+ coverage: {
8616
+ ...doc.coverage,
8617
+ notes: [...doc.coverage.notes, `To stay within the size the Patchstack API accepts, this map left out ${parts.join(", ")}${marked > 0 ? ` (${marked} endpoint(s) marked provenFlowsOmitted, inputsOmitted or sinksOmitted)` : ""}${links > 0 ? `; ${links} endpoint(s) have dependencyInputFlowsTruncated` : ""}. What is listed is true; what was left out is unknown, not absent.`]
8618
+ }
8619
+ };
8620
+ }
8621
+
8394
8622
  // src/map/module-graph.ts
8395
8623
  import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
8396
8624
  import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
@@ -9221,7 +9449,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9221
9449
  notes.push(deploymentShapes.length === 0 ? "`deploymentShapes` is EMPTY: no deployment artifact was recognized. That is not evidence the app has no server-side runtime \u2014 a serverless handler this analysis cannot parse produces no endpoint and looks identical to an app that has none, and entry-point recognition has no completeness flag (coverage.importsComplete covers the import inventory only). A definitive answer needs deployment or build attestation, which source analysis cannot supply." : `\`deploymentShapes\` records ${deploymentShapes.length} deployment artifact(s) the project declares (${deploymentShapes.map((s) => s.shape).join(", ")}). POSITIVE EVIDENCE ONLY, and findings differ in strength: only \`runtime-entry\` (a worker entry, or a provider function directory with source) shows something that serves. \`deployment-config\` means the project deploys to a platform \u2014 a static site on Netlify has a \`netlify.toml\` \u2014 and \`layout\` (a root \`api/\` or \`functions/\` folder) is an ordinary application folder. Neither may on its own be read as a server runtime.`);
9222
9450
  const serverSurface = classifyServerSurface(cwd, endpoints.length, deploymentShapes, ts);
9223
9451
  notes.push(surfaceNote(serverSurface));
9224
- return {
9452
+ return budgetDocument({
9225
9453
  version: 3,
9226
9454
  framework: detectFramework(cwd),
9227
9455
  deploymentShapes,
@@ -9261,7 +9489,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9261
9489
  roots: ["."],
9262
9490
  notes
9263
9491
  }
9264
- };
9492
+ });
9265
9493
  }
9266
9494
 
9267
9495
  // src/map/index.ts
@@ -9562,6 +9790,11 @@ async function runMap(flags) {
9562
9790
  console.log(json);
9563
9791
  }
9564
9792
  if (flags.get("upload") === true) {
9793
+ const problems = ingestProblems(map);
9794
+ if (problems.length > 0) {
9795
+ console.error(`patchstack: did not upload the attack surface \u2014 it cannot be fitted to the size Patchstack accepts (${problems.join("; ")}).`);
9796
+ return 0;
9797
+ }
9565
9798
  if (map.endpoints.length === 0) {
9566
9799
  console.error(
9567
9800
  "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."