@node9/policy-engine 1.60.0 → 1.61.1
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 +94 -2
- package/dist/index.mjs +94 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1140,7 +1140,7 @@ function analyzeSqlDestructive(command) {
|
|
|
1140
1140
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1141
1141
|
};
|
|
1142
1142
|
}
|
|
1143
|
-
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
1143
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
1144
1144
|
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1145
1145
|
"sudo",
|
|
1146
1146
|
"doas",
|
|
@@ -1460,6 +1460,97 @@ function analyzeFsOperation(command) {
|
|
|
1460
1460
|
fsOpCache.set(normalized, computed);
|
|
1461
1461
|
return computed;
|
|
1462
1462
|
}
|
|
1463
|
+
var stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
1464
|
+
function isSensitiveCleanupName(p) {
|
|
1465
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
1466
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
1467
|
+
}
|
|
1468
|
+
function isWaivableCleanupTarget(p) {
|
|
1469
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
1470
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
1471
|
+
if (/[*?[{]/.test(p)) return false;
|
|
1472
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
function deriveRedirOp(sample) {
|
|
1476
|
+
try {
|
|
1477
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1478
|
+
let op = -1;
|
|
1479
|
+
syntax.Walk(f, (node) => {
|
|
1480
|
+
const n = node;
|
|
1481
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
1482
|
+
return true;
|
|
1483
|
+
});
|
|
1484
|
+
return op;
|
|
1485
|
+
} catch {
|
|
1486
|
+
return -1;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
1490
|
+
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
1491
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1492
|
+
deriveRedirOp("cat <<-X\nX")
|
|
1493
|
+
]);
|
|
1494
|
+
function collectSameCommandCreations(f) {
|
|
1495
|
+
const created = /* @__PURE__ */ new Set();
|
|
1496
|
+
try {
|
|
1497
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1498
|
+
for (const stmt of stmts) {
|
|
1499
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1500
|
+
const redirs = stmt.Redirs || [];
|
|
1501
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1502
|
+
for (const r of redirs) {
|
|
1503
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1504
|
+
const w = resolveWordLiteral(r.Word);
|
|
1505
|
+
if (w) created.add(stripDotSlash(w));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
return created;
|
|
1511
|
+
}
|
|
1512
|
+
return created;
|
|
1513
|
+
}
|
|
1514
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1515
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1516
|
+
const f = parseShared(command);
|
|
1517
|
+
if (f === PARSE_FAIL) return false;
|
|
1518
|
+
const created = collectSameCommandCreations(f);
|
|
1519
|
+
if (created.size === 0) return false;
|
|
1520
|
+
let sawRm = false;
|
|
1521
|
+
let ok = true;
|
|
1522
|
+
try {
|
|
1523
|
+
syntax.Walk(f, (node) => {
|
|
1524
|
+
if (!node || !ok) return false;
|
|
1525
|
+
const n = node;
|
|
1526
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1527
|
+
const args = n.Args || [];
|
|
1528
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1529
|
+
if (name !== "rm") return true;
|
|
1530
|
+
sawRm = true;
|
|
1531
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1532
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1533
|
+
ok = false;
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
if (paths.length === 0) {
|
|
1537
|
+
ok = false;
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
for (const p of paths) {
|
|
1541
|
+
const np = stripDotSlash(p);
|
|
1542
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1543
|
+
ok = false;
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return true;
|
|
1548
|
+
});
|
|
1549
|
+
} catch {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
return sawRm && ok;
|
|
1553
|
+
}
|
|
1463
1554
|
function analyzeFsOperationImpl(command) {
|
|
1464
1555
|
const f = parseShared(command);
|
|
1465
1556
|
if (f === PARSE_FAIL) return null;
|
|
@@ -2218,8 +2309,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2218
2309
|
}
|
|
2219
2310
|
}
|
|
2220
2311
|
if (config.policy.smartRules.length > 0) {
|
|
2312
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2221
2313
|
const matches = config.policy.smartRules.filter(
|
|
2222
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
2314
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2223
2315
|
);
|
|
2224
2316
|
const matchedRule = resolvePinned(matches);
|
|
2225
2317
|
if (matchedRule) {
|
package/dist/index.mjs
CHANGED
|
@@ -1031,7 +1031,7 @@ function analyzeSqlDestructive(command) {
|
|
|
1031
1031
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1032
1032
|
};
|
|
1033
1033
|
}
|
|
1034
|
-
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
1034
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
1035
1035
|
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1036
1036
|
"sudo",
|
|
1037
1037
|
"doas",
|
|
@@ -1351,6 +1351,97 @@ function analyzeFsOperation(command) {
|
|
|
1351
1351
|
fsOpCache.set(normalized, computed);
|
|
1352
1352
|
return computed;
|
|
1353
1353
|
}
|
|
1354
|
+
var stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
1355
|
+
function isSensitiveCleanupName(p) {
|
|
1356
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
1357
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
1358
|
+
}
|
|
1359
|
+
function isWaivableCleanupTarget(p) {
|
|
1360
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
1361
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
1362
|
+
if (/[*?[{]/.test(p)) return false;
|
|
1363
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
function deriveRedirOp(sample) {
|
|
1367
|
+
try {
|
|
1368
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1369
|
+
let op = -1;
|
|
1370
|
+
syntax.Walk(f, (node) => {
|
|
1371
|
+
const n = node;
|
|
1372
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
1373
|
+
return true;
|
|
1374
|
+
});
|
|
1375
|
+
return op;
|
|
1376
|
+
} catch {
|
|
1377
|
+
return -1;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
1381
|
+
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
1382
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1383
|
+
deriveRedirOp("cat <<-X\nX")
|
|
1384
|
+
]);
|
|
1385
|
+
function collectSameCommandCreations(f) {
|
|
1386
|
+
const created = /* @__PURE__ */ new Set();
|
|
1387
|
+
try {
|
|
1388
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1389
|
+
for (const stmt of stmts) {
|
|
1390
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1391
|
+
const redirs = stmt.Redirs || [];
|
|
1392
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1393
|
+
for (const r of redirs) {
|
|
1394
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1395
|
+
const w = resolveWordLiteral(r.Word);
|
|
1396
|
+
if (w) created.add(stripDotSlash(w));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
} catch {
|
|
1401
|
+
return created;
|
|
1402
|
+
}
|
|
1403
|
+
return created;
|
|
1404
|
+
}
|
|
1405
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1406
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1407
|
+
const f = parseShared(command);
|
|
1408
|
+
if (f === PARSE_FAIL) return false;
|
|
1409
|
+
const created = collectSameCommandCreations(f);
|
|
1410
|
+
if (created.size === 0) return false;
|
|
1411
|
+
let sawRm = false;
|
|
1412
|
+
let ok = true;
|
|
1413
|
+
try {
|
|
1414
|
+
syntax.Walk(f, (node) => {
|
|
1415
|
+
if (!node || !ok) return false;
|
|
1416
|
+
const n = node;
|
|
1417
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1418
|
+
const args = n.Args || [];
|
|
1419
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1420
|
+
if (name !== "rm") return true;
|
|
1421
|
+
sawRm = true;
|
|
1422
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1423
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1424
|
+
ok = false;
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
if (paths.length === 0) {
|
|
1428
|
+
ok = false;
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
for (const p of paths) {
|
|
1432
|
+
const np = stripDotSlash(p);
|
|
1433
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1434
|
+
ok = false;
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return true;
|
|
1439
|
+
});
|
|
1440
|
+
} catch {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
return sawRm && ok;
|
|
1444
|
+
}
|
|
1354
1445
|
function analyzeFsOperationImpl(command) {
|
|
1355
1446
|
const f = parseShared(command);
|
|
1356
1447
|
if (f === PARSE_FAIL) return null;
|
|
@@ -2109,8 +2200,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2109
2200
|
}
|
|
2110
2201
|
}
|
|
2111
2202
|
if (config.policy.smartRules.length > 0) {
|
|
2203
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2112
2204
|
const matches = config.policy.smartRules.filter(
|
|
2113
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
2205
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2114
2206
|
);
|
|
2115
2207
|
const matchedRule = resolvePinned(matches);
|
|
2116
2208
|
if (matchedRule) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.1",
|
|
4
4
|
"description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://node9.ai",
|