@node9/proxy 2.33.2 → 2.34.0

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/scan-ink.mjs CHANGED
@@ -1039,6 +1039,11 @@ var CHECK_TEXT = {
1039
1039
  example: "cat /etc/shadow",
1040
1040
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
1041
1041
  },
1042
+ "files.system-dirs": {
1043
+ plain: "The agent writes, creates or deletes a file in a system directory, or sends a system file in a request. /usr/local and a container working directory under /usr/src are ordinary project space and are not included.",
1044
+ example: "wget -O /usr/bin/tool https://example.com/tool",
1045
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
1046
+ },
1042
1047
  // ── Commands ──────────────────────────────────────────────────────────────
1043
1048
  "commands.inline-exec": {
1044
1049
  plain: "The agent runs a small program written inside the command itself.",
@@ -1235,7 +1240,7 @@ var CHECK_TEXT = {
1235
1240
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
1236
1241
  },
1237
1242
  "loading.malicious-package": {
1238
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
1243
+ plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
1239
1244
  example: "npm install of a package flagged as malware",
1240
1245
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
1241
1246
  }
@@ -1272,6 +1277,15 @@ var PRODUCT_CHECKS = [
1272
1277
  defaultValue: "review",
1273
1278
  legacy: ["review-password-hash-read"]
1274
1279
  },
1280
+ {
1281
+ id: "files.system-dirs",
1282
+ group: "files",
1283
+ title: "System directories",
1284
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
1285
+ values: ALL,
1286
+ defaultValue: "review",
1287
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
1288
+ },
1275
1289
  // ── Commands ────────────────────────────────────────────────────────────
1276
1290
  {
1277
1291
  id: "commands.inline-exec",
@@ -2222,6 +2236,17 @@ var DEFAULT_OPS = new Set(
2222
2236
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
2223
2237
  )
2224
2238
  );
2239
+ var SYSTEM_DIRS = [
2240
+ "/usr",
2241
+ "/etc",
2242
+ "/boot",
2243
+ "/bin",
2244
+ "/sbin",
2245
+ "/lib",
2246
+ "/lib64",
2247
+ "/var/log"
2248
+ ];
2249
+ var UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
2225
2250
  var LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
2226
2251
  var BIP39_ENGLISH = `abandon
2227
2252
  ability
@@ -4493,6 +4518,7 @@ var REFERENCE_VALUE_RE = /^[$@%]|^(?:process\.env|os\.environ|env|config|setting
4493
4518
  var IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
4494
4519
  var PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
4495
4520
  var DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
4521
+ var CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
4496
4522
  var EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
4497
4523
  function looksLikeJoinedWords(value) {
4498
4524
  const segments2 = value.replace(/[0-9]/g, "").split(/[-_.]/);
@@ -4523,26 +4549,32 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
4523
4549
  if (EMAIL_VALUE_RE.test(value)) return false;
4524
4550
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
4525
4551
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
4552
+ if (CONCATENATION_RE.test(value)) return false;
4526
4553
  if (rejectedByStructuredValidator(value)) return false;
4527
4554
  return shannonEntropy(value) >= minEntropy;
4528
4555
  }
4529
4556
  var FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
4530
- var DERIVATIVE_NAME_RE = /(?:hash|digest|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
4557
+ var DERIVATIVE_NAME_RE = /(?:hash|digest|fingerprint|thumbprint|checksum|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
4531
4558
  function nameHoldsCredential(name) {
4532
4559
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
4533
4560
  }
4534
4561
  var QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
4535
4562
  var OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
4563
+ var OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
4564
+ function isOtpDocSecret(value) {
4565
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
4566
+ }
4536
4567
  function validateQueryCredential(raw) {
4537
4568
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
4538
4569
  if (value === null) return false;
4539
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
4570
+ if (OTP_SECRET_RE.test(value))
4571
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
4540
4572
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
4541
4573
  return looksGenerated(value, 12, 3);
4542
4574
  }
4543
- var ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
4575
+ var ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
4544
4576
  var ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
4545
- var NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
4577
+ var NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|pass)/i;
4546
4578
  function keywordEndsAPart(name) {
4547
4579
  const keyword = name.match(NAME_KEYWORD_RE)?.[0];
4548
4580
  if (!keyword) return false;
@@ -4554,13 +4586,43 @@ function keywordEndsAPart(name) {
4554
4586
  var ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
4555
4587
  function validateAssignedSecret(raw, before = "") {
4556
4588
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
4557
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
4589
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
4558
4590
  if (value === null || name === null) return false;
4559
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
4560
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
4591
+ if (!keywordEndsAPart(name)) return false;
4592
+ if (ARN_CONTEXT_RE.test(before)) return false;
4593
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
4561
4594
  if (!nameHoldsCredential(prefix + name)) return false;
4562
4595
  return looksGenerated(value, 12, 3.5);
4563
4596
  }
4597
+ var BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
4598
+ var BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
4599
+ function validateBasicCredential(raw) {
4600
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
4601
+ if (b64 === null) return false;
4602
+ const bare = b64.replace(/=+$/, "");
4603
+ if (bare.length % 4 === 1) return false;
4604
+ const bytes = Buffer.from(b64, "base64");
4605
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
4606
+ const text = bytes.toString("utf8");
4607
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
4608
+ for (let i = 0; i < text.length; i++) {
4609
+ const c = text.charCodeAt(i);
4610
+ if (c < 32 || c === 127) return false;
4611
+ }
4612
+ const pair = BASIC_PAIR_RE.exec(text);
4613
+ if (!pair) return false;
4614
+ const password = pair[2];
4615
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
4616
+ const lower2 = password.toLowerCase();
4617
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
4618
+ return shannonEntropy(password) >= 3;
4619
+ }
4620
+ var OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
4621
+ function validateOtpSecret(raw) {
4622
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
4623
+ if (secret === null || isOtpDocSecret(secret)) return false;
4624
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
4625
+ }
4564
4626
  var DLP_PATTERNS = [
4565
4627
  // ── AWS ───────────────────────────────────────────────────────────────────
4566
4628
  {
@@ -4898,6 +4960,22 @@ var DLP_PATTERNS = [
4898
4960
  contextBoost: true,
4899
4961
  minEntropy: 3
4900
4962
  },
4963
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
4964
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
4965
+ // config list. The validator decodes the value and accepts only a
4966
+ // `user:password` whose password looks generated (letters and digits, 8+
4967
+ // characters, the query-credential rule), so prose ("Basic authentication")
4968
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
4969
+ // review: the same weight as a bare Bearer token.
4970
+ {
4971
+ name: "HTTP Basic Credential",
4972
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
4973
+ severity: "review",
4974
+ keywords: ["basic"],
4975
+ validate: validateBasicCredential,
4976
+ redactGroup: 1,
4977
+ argumentsOnly: true
4978
+ },
4901
4979
  // ── Resend ────────────────────────────────────────────────────────────────
4902
4980
  {
4903
4981
  name: "Resend API Key",
@@ -5039,6 +5117,19 @@ var DLP_PATTERNS = [
5039
5117
  validate: validateQueryCredential,
5040
5118
  argumentsOnly: true
5041
5119
  },
5120
+ // ── OTP secret in a constructor ───────────────────────────────────────────
5121
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
5122
+ // the shared secret of a one-time-password generator written as a literal.
5123
+ // The otpauth URI form is `Credential in URL Query`. review.
5124
+ {
5125
+ name: "OTP Secret",
5126
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
5127
+ severity: "review",
5128
+ keywords: ["otp"],
5129
+ validate: validateOtpSecret,
5130
+ redactGroup: 1,
5131
+ argumentsOnly: true
5132
+ },
5042
5133
  // ── Secret assigned to a telling name ─────────────────────────────────────
5043
5134
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
5044
5135
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -5046,12 +5137,14 @@ var DLP_PATTERNS = [
5046
5137
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
5047
5138
  // bits per character or more. review, never block (node9Firewall#348,
5048
5139
  // item 4). Tuned on CredData's public split; its look-alike lines are the
5049
- // negative control.
5140
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
5141
+ // JSON written inside a string literal. The two optional quotes after the
5142
+ // name are written out, not as a counted group, to keep star height 1.
5050
5143
  {
5051
5144
  name: "Assigned Secret",
5052
- regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)[A-Za-z0-9_]{0,40}["'\]]{0,2}[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
5145
+ regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|\bpass(?![A-Za-z0-9])|_pass(?![A-Za-z0-9]))[A-Za-z0-9_]{0,40}\\?["'\]]?\\?["'\]]?[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
5053
5146
  severity: "review",
5054
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
5147
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
5055
5148
  validate: validateAssignedSecret,
5056
5149
  argumentsOnly: true
5057
5150
  }
@@ -5126,6 +5219,35 @@ function assertBuiltinPatternsAreSafe() {
5126
5219
  }
5127
5220
  assertBuiltinPatternsAreSafe();
5128
5221
  var { syntax: syntax3 } = mvdanSh3;
5222
+ var EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
5223
+ var MANAGER_HEADS = /* @__PURE__ */ new Set([
5224
+ "gem",
5225
+ "bundle",
5226
+ "cargo",
5227
+ "go",
5228
+ "dotnet",
5229
+ "nuget",
5230
+ "mvn",
5231
+ "mvnw",
5232
+ "composer",
5233
+ ...EDITOR_HEADS,
5234
+ "npm",
5235
+ "npx",
5236
+ "pnpm",
5237
+ "yarn",
5238
+ "bun",
5239
+ "bunx",
5240
+ "pip",
5241
+ "pip3",
5242
+ "pipx",
5243
+ "python",
5244
+ "python3",
5245
+ "py",
5246
+ "uv",
5247
+ "uvx",
5248
+ "poetry",
5249
+ "pipenv"
5250
+ ]);
5129
5251
  var TUNING_FIELDS = [
5130
5252
  { checkId: "network.unknown-host", name: "allow", legacy: ["egress", "allow"] },
5131
5253
  { checkId: "network.unknown-host", name: "deny", legacy: ["egress", "deny"] },
@@ -5184,6 +5306,7 @@ var TUNING_FIELDS = [
5184
5306
  var MAP_GOVERNED = /* @__PURE__ */ new Set([
5185
5307
  "files.shell-startup",
5186
5308
  "files.password-hashes",
5309
+ "files.system-dirs",
5187
5310
  "commands.inline-exec",
5188
5311
  "commands.eval-dynamic",
5189
5312
  "commands.curl-pipe-shell",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.33.2",
3
+ "version": "2.34.0",
4
4
  "description": "Access control for AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",