@node9/proxy 2.33.1 → 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/cli.js +1277 -249
- package/dist/cli.mjs +1285 -257
- package/dist/dashboard.mjs +230 -13
- package/dist/index.js +1087 -110
- package/dist/index.mjs +1087 -110
- package/dist/scan-ink.mjs +134 -11
- package/package.json +1 -1
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))
|
|
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)
|
|
4560
|
-
|
|
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}["'\]]
|
|
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: ["
|
|
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.
|
|
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",
|