@wrongstack/tools 0.8.2 → 0.8.5

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/index.js CHANGED
@@ -3,10 +3,11 @@ import { stat } from 'node:fs/promises';
3
3
  import * as path from 'node:path';
4
4
  import { dirname } from 'node:path';
5
5
  import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
6
- import { spawn, execSync, spawnSync } from 'node:child_process';
6
+ import { spawn, execFileSync, spawnSync } from 'node:child_process';
7
7
  import * as os from 'node:os';
8
8
  import * as dns from 'node:dns/promises';
9
9
  import * as net from 'node:net';
10
+ import { Agent } from 'undici';
10
11
  import * as fs13 from 'node:fs';
11
12
  import { statSync, mkdirSync, writeFileSync } from 'node:fs';
12
13
  import { createRequire } from 'node:module';
@@ -371,6 +372,7 @@ var replaceTool = {
371
372
  const dryRun = input.dry_run ?? false;
372
373
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
373
374
  const fileList = await resolveFiles(filesInput, ctx, globRe);
375
+ const realRoot = await fs4.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
374
376
  const results = [];
375
377
  let totalReplacements = 0;
376
378
  for (const absPath of fileList) {
@@ -386,7 +388,7 @@ var replaceTool = {
386
388
  } catch {
387
389
  continue;
388
390
  }
389
- const rel = path.relative(ctx.projectRoot, realPath);
391
+ const rel = path.relative(realRoot, realPath);
390
392
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
391
393
  const stat11 = await fs4.stat(realPath).catch(() => null);
392
394
  if (!stat11 || !stat11.isFile()) continue;
@@ -1488,8 +1490,20 @@ var BLOCKED_ARG_PATTERNS = {
1488
1490
  // python -c/--command executes arbitrary code; python -m runs modules
1489
1491
  python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
1490
1492
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
1491
- // -C <dir> changes working directory, bypassing cwd sandbox
1492
- git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/, /^-C$/],
1493
+ // -C <dir> changes working directory, bypassing cwd sandbox;
1494
+ // -c/--config <k>=<v> injects config that runs commands
1495
+ // (e.g. core.sshCommand, core.pager, http.proxy, alias.x=!cmd).
1496
+ git: [
1497
+ /^--exec=/,
1498
+ /^--upload-pack=/,
1499
+ /^--receive-pack=/,
1500
+ /^-C$/,
1501
+ /^-c$/,
1502
+ /^--config$/,
1503
+ /^-c=/,
1504
+ /^--config=/,
1505
+ /^--config-env=/
1506
+ ],
1493
1507
  // node -r/--require preloads arbitrary modules; --eval executes code
1494
1508
  node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
1495
1509
  // go run could execute arbitrary .go files; -ldflags could inject build-time code
@@ -1675,6 +1689,48 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1675
1689
  var MAX_BYTES2 = 131072;
1676
1690
  var TIMEOUT_MS2 = 2e4;
1677
1691
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
1692
+ function guardedLookup(hostname, options, callback) {
1693
+ dns.lookup(hostname, { all: true }).then((records) => {
1694
+ const family = options?.family;
1695
+ const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
1696
+ const list = byFamily.length > 0 ? byFamily : records;
1697
+ if (!ALLOW_PRIVATE) {
1698
+ for (const r of list) {
1699
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
1700
+ if (bad) {
1701
+ callback(
1702
+ Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
1703
+ code: "EAI_FAIL"
1704
+ })
1705
+ );
1706
+ return;
1707
+ }
1708
+ }
1709
+ }
1710
+ if (options?.all) {
1711
+ callback(
1712
+ null,
1713
+ list.map((r) => ({ address: r.address, family: r.family }))
1714
+ );
1715
+ return;
1716
+ }
1717
+ const first = list[0];
1718
+ if (!first) {
1719
+ callback(
1720
+ Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
1721
+ );
1722
+ return;
1723
+ }
1724
+ callback(null, first.address, first.family);
1725
+ }).catch((err) => callback(err));
1726
+ }
1727
+ var pinnedAgent;
1728
+ function getPinnedDispatcher() {
1729
+ if (!pinnedAgent) {
1730
+ pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
1731
+ }
1732
+ return pinnedAgent;
1733
+ }
1678
1734
  async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1679
1735
  const headers = {
1680
1736
  "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
@@ -1691,11 +1747,13 @@ async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1691
1747
  throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
1692
1748
  }
1693
1749
  await assertNotPrivate(parsed.hostname);
1694
- const res = await fetch(currentUrl, {
1750
+ const init = {
1695
1751
  redirect: "manual",
1696
1752
  signal,
1697
- headers
1698
- });
1753
+ headers,
1754
+ dispatcher: getPinnedDispatcher()
1755
+ };
1756
+ const res = await fetch(currentUrl, init);
1699
1757
  if (res.status < 300 || res.status > 399) {
1700
1758
  return res;
1701
1759
  }
@@ -4006,17 +4064,35 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4006
4064
  let stdout = "";
4007
4065
  let stderr = "";
4008
4066
  const MAX = 2e5;
4067
+ let settled = false;
4068
+ const empty = () => ({
4069
+ source: `docker:${service}`,
4070
+ entries: [],
4071
+ total: 0,
4072
+ truncated: false,
4073
+ stream_mode: false
4074
+ });
4075
+ const finish = (result) => {
4076
+ if (settled) return;
4077
+ settled = true;
4078
+ clearTimeout(timer);
4079
+ resolve6(result);
4080
+ };
4009
4081
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
4082
+ const timer = setTimeout(() => {
4083
+ child.kill("SIGTERM");
4084
+ finish(empty());
4085
+ }, DOCKER_LOGS_TIMEOUT_MS);
4010
4086
  child.stdout?.on("data", (c) => {
4011
4087
  if (stdout.length < MAX) stdout += c.toString();
4012
4088
  });
4013
4089
  child.stderr?.on("data", (c) => {
4014
4090
  if (stderr.length < MAX) stderr += c.toString();
4015
4091
  });
4016
- child.on("close", (code) => {
4092
+ child.on("close", () => {
4017
4093
  const output = stdout + stderr;
4018
4094
  const entries = parseLogLines(output, filterRe);
4019
- resolve6({
4095
+ finish({
4020
4096
  source: `docker:${service}`,
4021
4097
  entries,
4022
4098
  total: entries.length,
@@ -4024,17 +4100,10 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4024
4100
  stream_mode: false
4025
4101
  });
4026
4102
  });
4027
- child.on("error", (e) => {
4028
- resolve6({
4029
- source: `docker:${service}`,
4030
- entries: [],
4031
- total: 0,
4032
- truncated: false,
4033
- stream_mode: false
4034
- });
4035
- });
4103
+ child.on("error", () => finish(empty()));
4036
4104
  });
4037
4105
  }
4106
+ var DOCKER_LOGS_TIMEOUT_MS = 3e3;
4038
4107
  var MAX_TAIL_LINES = 1e5;
4039
4108
  async function fileLogs(path18, lines, filterRe, stream) {
4040
4109
  const { createInterface } = await import('node:readline');
@@ -5747,7 +5816,7 @@ function syncGoParse(filePath, content, lang) {
5747
5816
  mkdirSync(tmpDir, { recursive: true });
5748
5817
  const scriptPath = path.join(tmpDir, "parse.go");
5749
5818
  writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
5750
- const stdout = execSync(`go run "${scriptPath}"`, {
5819
+ const stdout = execFileSync("go", ["run", scriptPath], {
5751
5820
  input: content,
5752
5821
  timeout: 15e3,
5753
5822
  encoding: "utf8",
@@ -5993,7 +6062,7 @@ function syncPyParse(filePath, lang) {
5993
6062
  mkdirSync(tmpDir, { recursive: true });
5994
6063
  const scriptPath = path.join(tmpDir, "parse.py");
5995
6064
  writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
5996
- const stdout = execSync(`python "${scriptPath}" "${filePath}"`, {
6065
+ const stdout = execFileSync("python", [scriptPath, filePath], {
5997
6066
  timeout: 15e3,
5998
6067
  encoding: "utf8",
5999
6068
  windowsHide: true
@@ -6031,11 +6100,19 @@ function parseSymbols4(opts) {
6031
6100
  }
6032
6101
  function checkNativeParser() {
6033
6102
  try {
6034
- execSync("rustc --version", { stdio: "pipe" });
6103
+ execFileSync("rustc", ["--version"], { stdio: "pipe" });
6035
6104
  const toolsDir = path.join(process.cwd(), "tools");
6036
6105
  try {
6037
- execSync(
6038
- "cargo metadata --no-deps --format-version 1 --manifest-path " + path.join(toolsDir, "Cargo.toml"),
6106
+ execFileSync(
6107
+ "cargo",
6108
+ [
6109
+ "metadata",
6110
+ "--no-deps",
6111
+ "--format-version",
6112
+ "1",
6113
+ "--manifest-path",
6114
+ path.join(toolsDir, "Cargo.toml")
6115
+ ],
6039
6116
  { stdio: "pipe" }
6040
6117
  );
6041
6118
  return true;