@node9/policy-engine 1.31.0 → 1.33.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/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +80 -18
- package/dist/index.mjs +80 -18
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -971,7 +971,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
|
|
|
971
971
|
* and fails CI when the hash drifts without a version bump — forgetting
|
|
972
972
|
* is loud, not silent.
|
|
973
973
|
*/
|
|
974
|
-
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
974
|
+
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v6";
|
|
975
975
|
/**
|
|
976
976
|
* SHA-256 prefix of the detector-source files
|
|
977
977
|
* (canonical.ts + pii.ts + destructive-regex.ts).
|
|
@@ -982,7 +982,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
|
|
|
982
982
|
* files changed, this hash must change too, and you must consciously
|
|
983
983
|
* decide whether to bump CANONICAL_EXTRACTOR_VERSION."
|
|
984
984
|
*/
|
|
985
|
-
declare const CANONICAL_EXTRACTOR_HASH = "
|
|
985
|
+
declare const CANONICAL_EXTRACTOR_HASH = "a0e2bb339fe67e19";
|
|
986
986
|
declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
|
|
987
987
|
declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
|
|
988
988
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -971,7 +971,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
|
|
|
971
971
|
* and fails CI when the hash drifts without a version bump — forgetting
|
|
972
972
|
* is loud, not silent.
|
|
973
973
|
*/
|
|
974
|
-
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
974
|
+
declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v6";
|
|
975
975
|
/**
|
|
976
976
|
* SHA-256 prefix of the detector-source files
|
|
977
977
|
* (canonical.ts + pii.ts + destructive-regex.ts).
|
|
@@ -982,7 +982,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
|
|
|
982
982
|
* files changed, this hash must change too, and you must consciously
|
|
983
983
|
* decide whether to bump CANONICAL_EXTRACTOR_VERSION."
|
|
984
984
|
*/
|
|
985
|
-
declare const CANONICAL_EXTRACTOR_HASH = "
|
|
985
|
+
declare const CANONICAL_EXTRACTOR_HASH = "a0e2bb339fe67e19";
|
|
986
986
|
declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
|
|
987
987
|
declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
|
|
988
988
|
/**
|
package/dist/index.js
CHANGED
|
@@ -829,6 +829,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
829
829
|
if (f === PARSE_FAIL) return command;
|
|
830
830
|
try {
|
|
831
831
|
const strips = [];
|
|
832
|
+
const rewrites = [];
|
|
833
|
+
const msgSpans = /* @__PURE__ */ new Set();
|
|
832
834
|
syntax.Walk(f, (node) => {
|
|
833
835
|
if (!node) return false;
|
|
834
836
|
const n = node;
|
|
@@ -844,25 +846,46 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
844
846
|
if (nextParts.length !== 1) continue;
|
|
845
847
|
const quotedNode = nextParts[0];
|
|
846
848
|
const nt = syntax.NodeType(quotedNode);
|
|
849
|
+
const markStrip = () => {
|
|
850
|
+
const s = next.Pos().Offset();
|
|
851
|
+
const e = next.End().Offset();
|
|
852
|
+
strips.push([s, e]);
|
|
853
|
+
msgSpans.add(`${s}:${e}`);
|
|
854
|
+
};
|
|
847
855
|
if (nt === "SglQuoted") {
|
|
848
|
-
|
|
856
|
+
markStrip();
|
|
849
857
|
} else if (nt === "DblQuoted") {
|
|
850
858
|
const innerParts = quotedNode.Parts || [];
|
|
851
859
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
852
860
|
if (allLit) {
|
|
853
|
-
|
|
861
|
+
markStrip();
|
|
854
862
|
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
855
|
-
|
|
863
|
+
markStrip();
|
|
856
864
|
}
|
|
857
865
|
}
|
|
858
866
|
}
|
|
867
|
+
for (const arg of args) {
|
|
868
|
+
const s = arg.Pos().Offset();
|
|
869
|
+
const e = arg.End().Offset();
|
|
870
|
+
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
871
|
+
const resolved = resolveWordLiteral(arg);
|
|
872
|
+
if (resolved === null) continue;
|
|
873
|
+
const source = command.slice(s, e);
|
|
874
|
+
if (resolved === source) continue;
|
|
875
|
+
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
876
|
+
rewrites.push([s, e, resolved]);
|
|
877
|
+
}
|
|
859
878
|
return true;
|
|
860
879
|
});
|
|
861
|
-
|
|
862
|
-
|
|
880
|
+
const edits = [
|
|
881
|
+
...strips.map(([s, e]) => [s, e, '""']),
|
|
882
|
+
...rewrites
|
|
883
|
+
];
|
|
884
|
+
if (edits.length === 0) return command;
|
|
885
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
863
886
|
let result = command;
|
|
864
|
-
for (const [
|
|
865
|
-
result = result.slice(0,
|
|
887
|
+
for (const [s, e, rep] of edits) {
|
|
888
|
+
result = result.slice(0, s) + rep + result.slice(e);
|
|
866
889
|
}
|
|
867
890
|
return result;
|
|
868
891
|
} catch {
|
|
@@ -1028,8 +1051,35 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1028
1051
|
"shield:project-jail:block-read-ssh",
|
|
1029
1052
|
"shield:project-jail:block-read-aws",
|
|
1030
1053
|
"shield:project-jail:block-read-env",
|
|
1031
|
-
"shield:project-jail:review-read-credentials"
|
|
1054
|
+
"shield:project-jail:review-read-credentials",
|
|
1055
|
+
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1056
|
+
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1057
|
+
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1058
|
+
"review-drop-truncate-shell"
|
|
1059
|
+
]);
|
|
1060
|
+
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1061
|
+
"psql",
|
|
1062
|
+
"mysql",
|
|
1063
|
+
"mariadb",
|
|
1064
|
+
"sqlite3",
|
|
1065
|
+
"sqlplus",
|
|
1066
|
+
"cockroach",
|
|
1067
|
+
"clickhouse-client",
|
|
1068
|
+
"mongo",
|
|
1069
|
+
"mongosh"
|
|
1032
1070
|
]);
|
|
1071
|
+
var SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
1072
|
+
function analyzeSqlDestructive(command) {
|
|
1073
|
+
if (!SQL_DDL_RE.test(command)) return null;
|
|
1074
|
+
const { actions } = analyzeShellCommand(command);
|
|
1075
|
+
if (!actions.some((a) => SQL_DB_CLIS.has(a))) return null;
|
|
1076
|
+
return {
|
|
1077
|
+
ruleName: "review-drop-truncate-shell",
|
|
1078
|
+
verdict: "review",
|
|
1079
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
1080
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1033
1083
|
function isProtectedHomePath(rawPath) {
|
|
1034
1084
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1035
1085
|
let underHome = false;
|
|
@@ -1278,19 +1328,20 @@ function extractShellDestinations(command) {
|
|
|
1278
1328
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1279
1329
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1280
1330
|
function analyzeFsOperation(command) {
|
|
1281
|
-
|
|
1282
|
-
if (
|
|
1283
|
-
|
|
1284
|
-
fsOpCache.
|
|
1285
|
-
fsOpCache.
|
|
1331
|
+
const normalized = normalizeCommandForPolicy(command);
|
|
1332
|
+
if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
|
|
1333
|
+
if (fsOpCache.has(normalized)) {
|
|
1334
|
+
const hit = fsOpCache.get(normalized) ?? null;
|
|
1335
|
+
fsOpCache.delete(normalized);
|
|
1336
|
+
fsOpCache.set(normalized, hit);
|
|
1286
1337
|
return hit;
|
|
1287
1338
|
}
|
|
1288
|
-
const computed = analyzeFsOperationImpl(
|
|
1339
|
+
const computed = analyzeFsOperationImpl(normalized);
|
|
1289
1340
|
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
1290
1341
|
const oldest = fsOpCache.keys().next().value;
|
|
1291
1342
|
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
1292
1343
|
}
|
|
1293
|
-
fsOpCache.set(
|
|
1344
|
+
fsOpCache.set(normalized, computed);
|
|
1294
1345
|
return computed;
|
|
1295
1346
|
}
|
|
1296
1347
|
function analyzeFsOperationImpl(command) {
|
|
@@ -2014,6 +2065,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2014
2065
|
ruleDescription: fsVerdict.reason
|
|
2015
2066
|
};
|
|
2016
2067
|
}
|
|
2068
|
+
const sqlVerdict = analyzeSqlDestructive(bashCommand);
|
|
2069
|
+
if (sqlVerdict) {
|
|
2070
|
+
return {
|
|
2071
|
+
decision: sqlVerdict.verdict,
|
|
2072
|
+
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2073
|
+
reason: sqlVerdict.reason,
|
|
2074
|
+
tier: 2,
|
|
2075
|
+
ruleName: sqlVerdict.ruleName,
|
|
2076
|
+
ruleDescription: sqlVerdict.description
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2017
2079
|
}
|
|
2018
2080
|
if (config.policy.smartRules.length > 0) {
|
|
2019
2081
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -3329,8 +3391,8 @@ function detectArgsPii(args) {
|
|
|
3329
3391
|
|
|
3330
3392
|
// src/scan/canonical.ts
|
|
3331
3393
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3332
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
3333
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
3394
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v6";
|
|
3395
|
+
var CANONICAL_EXTRACTOR_HASH = "a0e2bb339fe67e19";
|
|
3334
3396
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
3335
3397
|
function extractCanonicalFindings(call, ctx) {
|
|
3336
3398
|
const out = [];
|
|
@@ -3492,7 +3554,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3492
3554
|
})
|
|
3493
3555
|
);
|
|
3494
3556
|
}
|
|
3495
|
-
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
3557
|
+
if (command !== null && DESTRUCTIVE_OP_RE.test(normalizeCommandForPolicy(command))) {
|
|
3496
3558
|
out.push(
|
|
3497
3559
|
makeFinding({
|
|
3498
3560
|
type: "destructive-op",
|
package/dist/index.mjs
CHANGED
|
@@ -722,6 +722,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
722
722
|
if (f === PARSE_FAIL) return command;
|
|
723
723
|
try {
|
|
724
724
|
const strips = [];
|
|
725
|
+
const rewrites = [];
|
|
726
|
+
const msgSpans = /* @__PURE__ */ new Set();
|
|
725
727
|
syntax.Walk(f, (node) => {
|
|
726
728
|
if (!node) return false;
|
|
727
729
|
const n = node;
|
|
@@ -737,25 +739,46 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
737
739
|
if (nextParts.length !== 1) continue;
|
|
738
740
|
const quotedNode = nextParts[0];
|
|
739
741
|
const nt = syntax.NodeType(quotedNode);
|
|
742
|
+
const markStrip = () => {
|
|
743
|
+
const s = next.Pos().Offset();
|
|
744
|
+
const e = next.End().Offset();
|
|
745
|
+
strips.push([s, e]);
|
|
746
|
+
msgSpans.add(`${s}:${e}`);
|
|
747
|
+
};
|
|
740
748
|
if (nt === "SglQuoted") {
|
|
741
|
-
|
|
749
|
+
markStrip();
|
|
742
750
|
} else if (nt === "DblQuoted") {
|
|
743
751
|
const innerParts = quotedNode.Parts || [];
|
|
744
752
|
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
745
753
|
if (allLit) {
|
|
746
|
-
|
|
754
|
+
markStrip();
|
|
747
755
|
} else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
|
|
748
|
-
|
|
756
|
+
markStrip();
|
|
749
757
|
}
|
|
750
758
|
}
|
|
751
759
|
}
|
|
760
|
+
for (const arg of args) {
|
|
761
|
+
const s = arg.Pos().Offset();
|
|
762
|
+
const e = arg.End().Offset();
|
|
763
|
+
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
764
|
+
const resolved = resolveWordLiteral(arg);
|
|
765
|
+
if (resolved === null) continue;
|
|
766
|
+
const source = command.slice(s, e);
|
|
767
|
+
if (resolved === source) continue;
|
|
768
|
+
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
769
|
+
rewrites.push([s, e, resolved]);
|
|
770
|
+
}
|
|
752
771
|
return true;
|
|
753
772
|
});
|
|
754
|
-
|
|
755
|
-
|
|
773
|
+
const edits = [
|
|
774
|
+
...strips.map(([s, e]) => [s, e, '""']),
|
|
775
|
+
...rewrites
|
|
776
|
+
];
|
|
777
|
+
if (edits.length === 0) return command;
|
|
778
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
756
779
|
let result = command;
|
|
757
|
-
for (const [
|
|
758
|
-
result = result.slice(0,
|
|
780
|
+
for (const [s, e, rep] of edits) {
|
|
781
|
+
result = result.slice(0, s) + rep + result.slice(e);
|
|
759
782
|
}
|
|
760
783
|
return result;
|
|
761
784
|
} catch {
|
|
@@ -921,8 +944,35 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
921
944
|
"shield:project-jail:block-read-ssh",
|
|
922
945
|
"shield:project-jail:block-read-aws",
|
|
923
946
|
"shield:project-jail:block-read-env",
|
|
924
|
-
"shield:project-jail:review-read-credentials"
|
|
947
|
+
"shield:project-jail:review-read-credentials",
|
|
948
|
+
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
949
|
+
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
950
|
+
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
951
|
+
"review-drop-truncate-shell"
|
|
952
|
+
]);
|
|
953
|
+
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
954
|
+
"psql",
|
|
955
|
+
"mysql",
|
|
956
|
+
"mariadb",
|
|
957
|
+
"sqlite3",
|
|
958
|
+
"sqlplus",
|
|
959
|
+
"cockroach",
|
|
960
|
+
"clickhouse-client",
|
|
961
|
+
"mongo",
|
|
962
|
+
"mongosh"
|
|
925
963
|
]);
|
|
964
|
+
var SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
965
|
+
function analyzeSqlDestructive(command) {
|
|
966
|
+
if (!SQL_DDL_RE.test(command)) return null;
|
|
967
|
+
const { actions } = analyzeShellCommand(command);
|
|
968
|
+
if (!actions.some((a) => SQL_DB_CLIS.has(a))) return null;
|
|
969
|
+
return {
|
|
970
|
+
ruleName: "review-drop-truncate-shell",
|
|
971
|
+
verdict: "review",
|
|
972
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
973
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
974
|
+
};
|
|
975
|
+
}
|
|
926
976
|
function isProtectedHomePath(rawPath) {
|
|
927
977
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
928
978
|
let underHome = false;
|
|
@@ -1171,19 +1221,20 @@ function extractShellDestinations(command) {
|
|
|
1171
1221
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1172
1222
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1173
1223
|
function analyzeFsOperation(command) {
|
|
1174
|
-
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1177
|
-
fsOpCache.
|
|
1178
|
-
fsOpCache.
|
|
1224
|
+
const normalized = normalizeCommandForPolicy(command);
|
|
1225
|
+
if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
|
|
1226
|
+
if (fsOpCache.has(normalized)) {
|
|
1227
|
+
const hit = fsOpCache.get(normalized) ?? null;
|
|
1228
|
+
fsOpCache.delete(normalized);
|
|
1229
|
+
fsOpCache.set(normalized, hit);
|
|
1179
1230
|
return hit;
|
|
1180
1231
|
}
|
|
1181
|
-
const computed = analyzeFsOperationImpl(
|
|
1232
|
+
const computed = analyzeFsOperationImpl(normalized);
|
|
1182
1233
|
if (fsOpCache.size >= FS_OP_CACHE_MAX) {
|
|
1183
1234
|
const oldest = fsOpCache.keys().next().value;
|
|
1184
1235
|
if (oldest !== void 0) fsOpCache.delete(oldest);
|
|
1185
1236
|
}
|
|
1186
|
-
fsOpCache.set(
|
|
1237
|
+
fsOpCache.set(normalized, computed);
|
|
1187
1238
|
return computed;
|
|
1188
1239
|
}
|
|
1189
1240
|
function analyzeFsOperationImpl(command) {
|
|
@@ -1907,6 +1958,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1907
1958
|
ruleDescription: fsVerdict.reason
|
|
1908
1959
|
};
|
|
1909
1960
|
}
|
|
1961
|
+
const sqlVerdict = analyzeSqlDestructive(bashCommand);
|
|
1962
|
+
if (sqlVerdict) {
|
|
1963
|
+
return {
|
|
1964
|
+
decision: sqlVerdict.verdict,
|
|
1965
|
+
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
1966
|
+
reason: sqlVerdict.reason,
|
|
1967
|
+
tier: 2,
|
|
1968
|
+
ruleName: sqlVerdict.ruleName,
|
|
1969
|
+
ruleDescription: sqlVerdict.description
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1910
1972
|
}
|
|
1911
1973
|
if (config.policy.smartRules.length > 0) {
|
|
1912
1974
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -3222,8 +3284,8 @@ function detectArgsPii(args) {
|
|
|
3222
3284
|
|
|
3223
3285
|
// src/scan/canonical.ts
|
|
3224
3286
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3225
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
3226
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
3287
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v6";
|
|
3288
|
+
var CANONICAL_EXTRACTOR_HASH = "a0e2bb339fe67e19";
|
|
3227
3289
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
3228
3290
|
function extractCanonicalFindings(call, ctx) {
|
|
3229
3291
|
const out = [];
|
|
@@ -3385,7 +3447,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3385
3447
|
})
|
|
3386
3448
|
);
|
|
3387
3449
|
}
|
|
3388
|
-
if (DESTRUCTIVE_OP_RE.test(command)) {
|
|
3450
|
+
if (command !== null && DESTRUCTIVE_OP_RE.test(normalizeCommandForPolicy(command))) {
|
|
3389
3451
|
out.push(
|
|
3390
3452
|
makeFinding({
|
|
3391
3453
|
type: "destructive-op",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
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",
|