@node9/proxy 2.8.5 → 2.9.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.
@@ -89,10 +89,47 @@ var init_types = __esm({
89
89
 
90
90
  // packages/policy-engine/dist/index.mjs
91
91
  import safeRegex from "safe-regex2";
92
+ import { createHash } from "crypto";
92
93
  import mvdanSh from "mvdan-sh";
93
94
  import pm from "picomatch";
94
95
  import safeRegex2 from "safe-regex2";
95
96
  import safeRegex3 from "safe-regex2";
97
+ function validateBase58Check(s) {
98
+ if (!s) return null;
99
+ let n = 0n;
100
+ for (const c of s) {
101
+ const v = B58_INDEX[c];
102
+ if (v === void 0) return null;
103
+ n = n * 58n + BigInt(v);
104
+ }
105
+ let hex = n.toString(16);
106
+ if (hex.length % 2) hex = "0" + hex;
107
+ let zeros = 0;
108
+ for (const c of s) {
109
+ if (c !== "1") break;
110
+ zeros++;
111
+ }
112
+ const bytes = Buffer.concat([
113
+ Buffer.alloc(zeros),
114
+ n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
115
+ ]);
116
+ if (bytes.length < 5) return null;
117
+ const body = bytes.subarray(0, bytes.length - 4);
118
+ const check = bytes.subarray(bytes.length - 4);
119
+ const h = createHash("sha256").update(createHash("sha256").update(body).digest()).digest();
120
+ return h.subarray(0, 4).equals(check) ? body : null;
121
+ }
122
+ function validateWif(s) {
123
+ const p = validateBase58Check(s);
124
+ if (!p || p[0] !== 128) return false;
125
+ return p.length === 33 || p.length === 34 && p[33] === 1;
126
+ }
127
+ function validateXprv(s) {
128
+ const p = validateBase58Check(s);
129
+ if (!p || p.length !== 78) return false;
130
+ const version = p.readUInt32BE(0);
131
+ return XPRV_VERSIONS.has(version) && p[45] === 0;
132
+ }
96
133
  function isAssignmentContext(text) {
97
134
  return ASSIGNMENT_CONTEXT_RE.test(text);
98
135
  }
@@ -133,6 +170,33 @@ function maskSecret(raw, pattern) {
133
170
  const stars = "*".repeat(Math.min(secret.length - 8, 12));
134
171
  return `${prefix}${stars}${suffix}`;
135
172
  }
173
+ function suppressed(pattern, raw) {
174
+ if (pattern.validate) {
175
+ let ok;
176
+ try {
177
+ ok = pattern.validate(raw);
178
+ } catch {
179
+ ok = true;
180
+ }
181
+ return !ok;
182
+ }
183
+ if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) return true;
184
+ if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) return true;
185
+ return false;
186
+ }
187
+ function firstAcceptedMatch(pattern, text) {
188
+ const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : pattern.regex.flags + "g";
189
+ const re = new RegExp(pattern.regex.source, flags);
190
+ let m;
191
+ while ((m = re.exec(text)) !== null) {
192
+ if (m[0].length === 0) {
193
+ re.lastIndex = m.index + 1;
194
+ continue;
195
+ }
196
+ if (!suppressed(pattern, m[0])) return m[0];
197
+ }
198
+ return null;
199
+ }
136
200
  function scanArgs(args, depth = 0, fieldPath = "args") {
137
201
  if (depth > MAX_DEPTH || args === null || args === void 0) return null;
138
202
  if (Array.isArray(args)) {
@@ -157,18 +221,16 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
157
221
  if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
158
222
  continue;
159
223
  }
160
- if (pattern.regex.test(text)) {
161
- const raw = text.match(pattern.regex)?.[0] ?? "";
162
- if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) continue;
163
- if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) continue;
164
- const severity = pattern.contextBoost && assignmentCtx ? "block" : pattern.severity;
165
- return {
166
- patternName: pattern.name,
167
- fieldPath,
168
- redactedSample: maskSecret(text, pattern.regex),
169
- severity
170
- };
171
- }
224
+ const raw = firstAcceptedMatch(pattern, text);
225
+ if (raw === null) continue;
226
+ const severity = pattern.contextBoost && assignmentCtx ? "block" : pattern.severity;
227
+ return {
228
+ patternName: pattern.name,
229
+ fieldPath,
230
+ // Mask the ACCEPTED token, not the first regex hit in the field.
231
+ redactedSample: maskSecret(raw, pattern.regex),
232
+ severity
233
+ };
172
234
  }
173
235
  if (text.length < MAX_JSON_PARSE_BYTES) {
174
236
  const trimmed = text.trim();
@@ -228,13 +290,13 @@ function matchesPattern(text, patterns) {
228
290
  const withoutDotSlash = text.replace(/^\.\//, "");
229
291
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
230
292
  }
231
- function getNestedValue(obj, path13) {
293
+ function getNestedValue(obj, path14) {
232
294
  if (!obj || typeof obj !== "object") return null;
233
- const segments = path13.split(".");
234
- for (const seg of segments) {
295
+ const segments2 = path14.split(".");
296
+ for (const seg of segments2) {
235
297
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
236
298
  }
237
- return segments.reduce((prev, curr) => prev?.[curr], obj);
299
+ return segments2.reduce((prev, curr) => prev?.[curr], obj);
238
300
  }
239
301
  function evaluateSmartConditions(args, rule) {
240
302
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -659,6 +721,162 @@ function analyzeFsOperationImpl(command) {
659
721
  return null;
660
722
  }
661
723
  }
724
+ function parseComponent(s) {
725
+ if (!s) return null;
726
+ if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
727
+ if (s === "0") return 0;
728
+ if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
729
+ if (/^[1-9][0-9]*$/.test(s)) return Number(s);
730
+ return null;
731
+ }
732
+ function parseIpv4(input) {
733
+ let s = input;
734
+ if (s.endsWith(".")) s = s.slice(0, -1);
735
+ if (!s) return null;
736
+ const parts = s.split(".");
737
+ if (parts.length > 4) return null;
738
+ const vals = [];
739
+ for (const p of parts) {
740
+ const v = parseComponent(p);
741
+ if (v === null || !Number.isFinite(v) || v < 0) return null;
742
+ vals.push(v);
743
+ }
744
+ const n = vals.length;
745
+ for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
746
+ const last = vals[n - 1];
747
+ const remainingBytes = 4 - (n - 1);
748
+ const limit = Math.pow(256, remainingBytes);
749
+ if (last >= limit) return null;
750
+ let value = last;
751
+ for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
752
+ if (value > 4294967295) return null;
753
+ return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
754
+ }
755
+ function expandIpv6(input) {
756
+ const s = input.toLowerCase();
757
+ if (!/^[0-9a-f:.]+$/.test(s)) return null;
758
+ if ((s.match(/::/g) ?? []).length > 1) return null;
759
+ let head = s;
760
+ let tailV4 = null;
761
+ const lastColon = s.lastIndexOf(":");
762
+ const afterLast = s.slice(lastColon + 1);
763
+ if (afterLast.includes(".")) {
764
+ const dotted = parseIpv4(afterLast);
765
+ if (!dotted) return null;
766
+ const o = dotted.split(".").map(Number);
767
+ tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
768
+ head = s.slice(0, lastColon + 1) + "0";
769
+ }
770
+ const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
771
+ const toGroups = (part) => {
772
+ if (!part) return [];
773
+ const out = [];
774
+ for (const g of part.split(":")) {
775
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
776
+ out.push(parseInt(g, 16));
777
+ }
778
+ return out;
779
+ };
780
+ const left = toGroups(lhs);
781
+ if (left === null) return null;
782
+ let right = [];
783
+ if (rhs !== null) {
784
+ const r = toGroups(rhs);
785
+ if (r === null) return null;
786
+ right = r;
787
+ }
788
+ if (tailV4) {
789
+ if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
790
+ else left.splice(left.length - 1, 1, ...tailV4);
791
+ }
792
+ const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
793
+ if (rhs === null && groups.length !== 8) return null;
794
+ if (rhs !== null && left.length + right.length > 8) return null;
795
+ if (groups.length !== 8) return null;
796
+ return groups;
797
+ }
798
+ function compressIpv6(g) {
799
+ let bestStart = -1;
800
+ let bestLen = 0;
801
+ let i = 0;
802
+ while (i < 8) {
803
+ if (g[i] !== 0) {
804
+ i++;
805
+ continue;
806
+ }
807
+ let j = i;
808
+ while (j < 8 && g[j] === 0) j++;
809
+ if (j - i > bestLen) {
810
+ bestLen = j - i;
811
+ bestStart = i;
812
+ }
813
+ i = j;
814
+ }
815
+ const hex = g.map((x) => x.toString(16));
816
+ if (bestLen < 2) return hex.join(":");
817
+ return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
818
+ }
819
+ function normalizeIpLiteral(host) {
820
+ try {
821
+ if (typeof host !== "string") return null;
822
+ let s = host.trim();
823
+ if (!s || s.length > SSRF_MAX_HOST) return null;
824
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
825
+ const zone = s.indexOf("%");
826
+ if (zone >= 0) s = s.slice(0, zone);
827
+ if (!s) return null;
828
+ if (s.includes(":")) {
829
+ const g = expandIpv6(s);
830
+ if (!g) return null;
831
+ const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
832
+ if (mapped) {
833
+ return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
834
+ }
835
+ return compressIpv6(g);
836
+ }
837
+ return parseIpv4(s);
838
+ } catch {
839
+ return null;
840
+ }
841
+ }
842
+ function classifySsrf(host) {
843
+ try {
844
+ if (typeof host !== "string" || !host) return null;
845
+ const lower = host.trim().toLowerCase().replace(/\.$/, "");
846
+ const ip = normalizeIpLiteral(host);
847
+ if (ip === null) {
848
+ return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
849
+ }
850
+ const hit = (tier, overridable) => ({
851
+ tier,
852
+ overridable,
853
+ kind: "address",
854
+ normalized: ip
855
+ });
856
+ if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
857
+ const o = v4Octets(ip);
858
+ if (o) {
859
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
860
+ if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
861
+ if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
862
+ if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
863
+ if (o[0] === 127) return hit("private", true);
864
+ if (o[0] === 10) return hit("private", true);
865
+ if (o[0] === 192 && o[1] === 168) return hit("private", true);
866
+ if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
867
+ return null;
868
+ }
869
+ const g = expandIpv6(ip);
870
+ if (!g) return null;
871
+ if (g.every((x) => x === 0)) return hit("unspecified", false);
872
+ if ((g[0] & 65472) === 65152) return hit("link-local", false);
873
+ if ((g[0] & 65280) === 65280) return hit("multicast", false);
874
+ if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
875
+ return null;
876
+ } catch {
877
+ return null;
878
+ }
879
+ }
662
880
  function isShieldVerdict(v) {
663
881
  return v === "allow" || v === "review" || v === "block";
664
882
  }
@@ -712,10 +930,123 @@ function assertBuiltinShieldRegexesAreSafe() {
712
930
  }
713
931
  }
714
932
  }
715
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LONG_OUTPUT_THRESHOLD_BYTES;
933
+ function percentDecodeOnce(s) {
934
+ try {
935
+ return decodeURIComponent(s);
936
+ } catch {
937
+ return s.replace(/%([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
938
+ }
939
+ }
940
+ function segments(s, alphabet) {
941
+ const out = /* @__PURE__ */ new Set();
942
+ if (alphabet.test(s)) out.add(s);
943
+ for (const seg of s.split(/[?&\s"'<>]+/)) {
944
+ if (seg.length >= MIN_SEGMENT && alphabet.test(seg)) out.add(seg);
945
+ for (const part of seg.split("=")) {
946
+ if (part.length >= MIN_SEGMENT && alphabet.test(part)) out.add(part);
947
+ }
948
+ }
949
+ return [...out];
950
+ }
951
+ function lowestOffset(cands, needles, stripped) {
952
+ let best = null;
953
+ for (const c of cands) {
954
+ for (const n of needles) {
955
+ const off = c.indexOf(stripped ? n.stripped : n.raw);
956
+ if (off >= 0 && (best === null || off < best.off)) best = { off, v: n.v };
957
+ }
958
+ }
959
+ return best?.v ?? null;
960
+ }
961
+ function prepare(values) {
962
+ const out = [];
963
+ for (const v of values) {
964
+ if (typeof v.value !== "string" || v.value.length < CANARY_MIN_LENGTH) {
965
+ console.error(
966
+ `[node9 engine] canary ${v.id}: value shorter than ${CANARY_MIN_LENGTH}, skipped`
967
+ );
968
+ continue;
969
+ }
970
+ out.push({ v, raw: v.value, stripped: stripSeparators(v.value) });
971
+ }
972
+ return out;
973
+ }
974
+ function matchPrepared(text, needles) {
975
+ if (!text || needles.length === 0) return null;
976
+ const t = text.length > MAX_TEXT ? text.slice(0, MAX_TEXT) : text;
977
+ const raw = lowestOffset([t], needles, false);
978
+ if (raw) return { v: raw, view: "raw" };
979
+ for (const { view, decoder, stripped } of VIEWS) {
980
+ let cands;
981
+ try {
982
+ cands = CANARY_DECODERS[decoder](t);
983
+ } catch (e) {
984
+ console.error(
985
+ `[node9 engine] canary view ${view} failed, skipped:`,
986
+ e instanceof Error ? e.message : String(e)
987
+ );
988
+ continue;
989
+ }
990
+ const hit = lowestOffset(cands, needles, stripped);
991
+ if (hit) return { v: hit, view };
992
+ }
993
+ return null;
994
+ }
995
+ function matchCanaryArgs(args, values) {
996
+ if (values.length === 0) return null;
997
+ const needles = prepare(values);
998
+ if (needles.length === 0) return null;
999
+ const walk = (v, depth, fieldPath) => {
1000
+ if (depth > MAX_DEPTH2) return null;
1001
+ if (typeof v === "string") {
1002
+ const hit = matchPrepared(v, needles);
1003
+ if (hit) return { id: hit.v.id, view: hit.view, fieldPath, retired: Boolean(hit.v.retired) };
1004
+ if (v.length < MAX_JSON_PARSE) {
1005
+ const trimmed = v.trim();
1006
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1007
+ try {
1008
+ return walk(JSON.parse(v), depth + 1, fieldPath);
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ }
1013
+ return null;
1014
+ }
1015
+ if (typeof v === "number" && Number.isFinite(v)) return walk(String(v), depth, fieldPath);
1016
+ if (Array.isArray(v)) {
1017
+ for (let i = 0; i < v.length; i++) {
1018
+ const h = walk(v[i], depth + 1, `${fieldPath}[${i}]`);
1019
+ if (h) return h;
1020
+ }
1021
+ return null;
1022
+ }
1023
+ if (v && typeof v === "object") {
1024
+ for (const [k, child] of Object.entries(v)) {
1025
+ const h = walk(child, depth + 1, fieldPath ? `${fieldPath}.${k}` : k);
1026
+ if (h) return h;
1027
+ }
1028
+ }
1029
+ return null;
1030
+ };
1031
+ try {
1032
+ return walk(args, 0, "");
1033
+ } catch (e) {
1034
+ console.error(
1035
+ "[node9 engine] canary args walk failed:",
1036
+ e instanceof Error ? e.message : String(e)
1037
+ );
1038
+ return null;
1039
+ }
1040
+ }
1041
+ var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
716
1042
  var init_dist = __esm({
717
1043
  "packages/policy-engine/dist/index.mjs"() {
718
1044
  "use strict";
1045
+ B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1046
+ B58_INDEX = Object.fromEntries(
1047
+ [...B58].map((c, i) => [c, i])
1048
+ );
1049
+ XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
719
1050
  ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
720
1051
  DLP_STOPWORDS = [
721
1052
  "example",
@@ -894,6 +1225,27 @@ var init_dist = __esm({
894
1225
  severity: "block",
895
1226
  keywords: ["sg."]
896
1227
  },
1228
+ // ── Cryptocurrency private keys (base58check-validated) ───────────────────
1229
+ // Both are anchored with \b on each side: unanchored, `[KL][base58]{51}`
1230
+ // matches INSIDE any longer base58 blob (an xprv, a Solana keypair, a
1231
+ // Monero address). Lookbehind fails safe-regex2; \b is the house style
1232
+ // (see the card regexes). Mainnet only, matching validateWif / validateXprv;
1233
+ // testnet (WIF 0xEF, tprv) is deferred. Cost was measured: the WIF regex
1234
+ // runs on every string (first keyword-less pattern) at 0.024 ms per 100 KB
1235
+ // of prose, so no prefilter is warranted.
1236
+ {
1237
+ name: "Bitcoin WIF Private Key",
1238
+ regex: /\b(?:5[1-9A-HJ-NP-Za-km-z]{50}|[KL][1-9A-HJ-NP-Za-km-z]{51})\b/,
1239
+ severity: "block",
1240
+ validate: validateWif
1241
+ },
1242
+ {
1243
+ name: "Extended Private Key",
1244
+ regex: /\b[xyz]prv[1-9A-HJ-NP-Za-km-z]{107}\b/,
1245
+ severity: "block",
1246
+ keywords: ["xprv", "yprv", "zprv"],
1247
+ validate: validateXprv
1248
+ },
897
1249
  // ── Private keys (PEM) ────────────────────────────────────────────────────
898
1250
  {
899
1251
  name: "Private Key (PEM)",
@@ -1399,6 +1751,22 @@ var init_dist = __esm({
1399
1751
  deriveRedirOp("cat <<X\nX"),
1400
1752
  deriveRedirOp("cat <<-X\nX")
1401
1753
  ]);
1754
+ SSRF_MAX_HOST = 253;
1755
+ METADATA_ADDRESSES = /* @__PURE__ */ new Set([
1756
+ "169.254.169.254",
1757
+ // AWS / Azure / DigitalOcean / OpenStack IMDS
1758
+ "169.254.170.2",
1759
+ // AWS ECS task role
1760
+ "168.63.129.16",
1761
+ // Azure WireServer
1762
+ "fd00:ec2::254"
1763
+ // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
1764
+ ]);
1765
+ METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
1766
+ v4Octets = (a) => {
1767
+ const p = a.split(".");
1768
+ return p.length === 4 ? p.map(Number) : null;
1769
+ };
1402
1770
  aws_default = {
1403
1771
  name: "aws",
1404
1772
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -2150,6 +2518,60 @@ var init_dist = __esm({
2150
2518
  [redis_default.name]: redis_default
2151
2519
  };
2152
2520
  assertBuiltinShieldRegexesAreSafe();
2521
+ CANARY_MIN_LENGTH = 16;
2522
+ MAX_TEXT = 1e5;
2523
+ MAX_DEPTH2 = 6;
2524
+ MAX_JSON_PARSE = 1e4;
2525
+ URL_DEPTH = 4;
2526
+ B64_DEPTH = 3;
2527
+ MIN_SEGMENT = 16;
2528
+ SEPARATORS = /[./\\?&= \t\n\r:;,\-_@%+#]/g;
2529
+ stripSeparators = (s) => s.replace(SEPARATORS, "");
2530
+ looksText = (s) => s.length > 0 && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(s);
2531
+ CANARY_DECODERS = {
2532
+ url: (s) => {
2533
+ const out = [];
2534
+ let cur = s;
2535
+ for (let i = 0; i < URL_DEPTH; i++) {
2536
+ const d = percentDecodeOnce(cur);
2537
+ if (d === cur) break;
2538
+ out.push(d);
2539
+ cur = d;
2540
+ }
2541
+ return out;
2542
+ },
2543
+ base64: (s) => {
2544
+ const out = [];
2545
+ let frontier = segments(s, /^[A-Za-z0-9+/\-_=]+$/);
2546
+ for (let depth = 0; depth < B64_DEPTH && frontier.length; depth++) {
2547
+ const next = [];
2548
+ for (const c of frontier) {
2549
+ const d = Buffer.from(c, "base64").toString("utf8");
2550
+ if (!looksText(d) || d.length < CANARY_MIN_LENGTH) continue;
2551
+ out.push(d);
2552
+ next.push(...segments(d, /^[A-Za-z0-9+/\-_=]+$/));
2553
+ }
2554
+ frontier = next;
2555
+ }
2556
+ return out;
2557
+ },
2558
+ hex: (s) => {
2559
+ const out = [];
2560
+ for (const c of segments(s, /^[0-9A-Fa-f]+$/)) {
2561
+ if (c.length % 2 !== 0 || c.length < CANARY_MIN_LENGTH * 2) continue;
2562
+ const d = Buffer.from(c, "hex").toString("utf8");
2563
+ if (looksText(d)) out.push(d);
2564
+ }
2565
+ return out;
2566
+ },
2567
+ separators: (s) => [stripSeparators(s)]
2568
+ };
2569
+ VIEWS = [
2570
+ { view: "url-decoded", decoder: "url", stripped: false },
2571
+ { view: "base64-decoded", decoder: "base64", stripped: false },
2572
+ { view: "hex-decoded", decoder: "hex", stripped: false },
2573
+ { view: "separators-stripped", decoder: "separators", stripped: true }
2574
+ ];
2153
2575
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2154
2576
  }
2155
2577
  });
@@ -2332,8 +2754,8 @@ function sanitizeConfig(raw) {
2332
2754
  }
2333
2755
  }
2334
2756
  const lines = result.error.issues.map((issue) => {
2335
- const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
2336
- return ` \u2022 ${path13}: ${issue.message}`;
2757
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
2758
+ return ` \u2022 ${path14}: ${issue.message}`;
2337
2759
  });
2338
2760
  return {
2339
2761
  sanitized,
@@ -2478,7 +2900,11 @@ var init_config_schema = __esm({
2478
2900
  mode: z.enum(["off", "review", "block"]).optional(),
2479
2901
  allow: z.array(z.string()).optional(),
2480
2902
  deny: z.array(z.string()).optional(),
2481
- allowPrivate: z.boolean().optional()
2903
+ allowPrivate: z.boolean().optional(),
2904
+ // SSRF floor. `ssrfAllow` exempts OVERRIDABLE tiers only; a tier-1
2905
+ // entry is dropped with a warning at load, never silently honoured.
2906
+ ssrfAllow: z.array(z.string()).optional(),
2907
+ ssrfStrict: z.boolean().optional()
2482
2908
  }).optional(),
2483
2909
  loopDetection: z.object({
2484
2910
  enabled: z.boolean().optional(),
@@ -2636,6 +3062,12 @@ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
2636
3062
  if (Array.isArray(managed.deny) && managed.deny.length > 0) {
2637
3063
  next.deny = [.../* @__PURE__ */ new Set([...local.deny ?? [], ...managed.deny])];
2638
3064
  }
3065
+ if (typeof managed.ssrfStrict === "boolean") {
3066
+ next.ssrfStrict = managed.ssrfStrict;
3067
+ }
3068
+ if (Array.isArray(managed.ssrfAllow)) {
3069
+ next.ssrfAllow = [...managed.ssrfAllow];
3070
+ }
2639
3071
  if (typeof managed.allowPrivate === "boolean") {
2640
3072
  next.allowPrivate = locked.includes("egressAllowPrivate") ? managed.allowPrivate : (local.allowPrivate ?? true) && managed.allowPrivate;
2641
3073
  }
@@ -2723,9 +3155,9 @@ function slug(s) {
2723
3155
  }
2724
3156
  function pathToRegexFragment(rawPath) {
2725
3157
  const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
2726
- const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
2727
- if (segments.length === 0) return "";
2728
- return `(^|${B})${segments.join(SEP)}(${B}|$)`;
3158
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
3159
+ if (segments2.length === 0) return "";
3160
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
2729
3161
  }
2730
3162
  function pathRules(rawPath, verdict, reason) {
2731
3163
  const value = pathToRegexFragment(rawPath);
@@ -2789,6 +3221,20 @@ var init_trusted_hosts = __esm({
2789
3221
  import fs4 from "fs";
2790
3222
  import path4 from "path";
2791
3223
  import os4 from "os";
3224
+ function sanitizeSsrfAllow(entries, source) {
3225
+ const kept = [];
3226
+ for (const entry of entries) {
3227
+ const m = classifySsrf(entry);
3228
+ if (m && !m.overridable) {
3229
+ process.emitWarning(
3230
+ `[node9] ${source} ssrfAllow entry "${entry}" is a protected address (${m.tier}) and cannot be exempted; ignoring it.`
3231
+ );
3232
+ continue;
3233
+ }
3234
+ kept.push(entry);
3235
+ }
3236
+ return kept;
3237
+ }
2792
3238
  function getCredentials() {
2793
3239
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
2794
3240
  if (process.env.NODE9_API_KEY) {
@@ -2895,7 +3341,8 @@ function getConfig(cwd) {
2895
3341
  egress: {
2896
3342
  ...DEFAULT_CONFIG.policy.egress,
2897
3343
  allow: [...DEFAULT_CONFIG.policy.egress.allow],
2898
- deny: [...DEFAULT_CONFIG.policy.egress.deny]
3344
+ deny: [...DEFAULT_CONFIG.policy.egress.deny],
3345
+ ssrfAllow: [...DEFAULT_CONFIG.policy.egress.ssrfAllow ?? []]
2899
3346
  },
2900
3347
  loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
2901
3348
  injectionScan: {
@@ -2921,6 +3368,7 @@ function getConfig(cwd) {
2921
3368
  };
2922
3369
  const pr2Creds = getCredentials();
2923
3370
  const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
3371
+ let ssrfStrictSource = "default";
2924
3372
  const applyLayer = (source, isProject = false, isCloud = false) => {
2925
3373
  if (!source) return;
2926
3374
  const s = source.settings || {};
@@ -3003,6 +3451,15 @@ function getConfig(cwd) {
3003
3451
  if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
3004
3452
  if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
3005
3453
  mergedPolicy.egress.allowPrivate = e.allowPrivate;
3454
+ if (!isProject) {
3455
+ if (Array.isArray(e.ssrfAllow)) {
3456
+ mergedPolicy.egress.ssrfAllow = sanitizeSsrfAllow(e.ssrfAllow, "egress.");
3457
+ }
3458
+ if (e.ssrfStrict !== void 0) {
3459
+ mergedPolicy.egress.ssrfStrict = e.ssrfStrict;
3460
+ ssrfStrictSource = "local";
3461
+ }
3462
+ }
3006
3463
  }
3007
3464
  if (p.loopDetection) {
3008
3465
  const ld = p.loopDetection;
@@ -3109,6 +3566,13 @@ function getConfig(cwd) {
3109
3566
  if (deny) mergedPolicy.egress.deny = deny;
3110
3567
  if (typeof e.allowPrivate === "boolean")
3111
3568
  mergedPolicy.egress.allowPrivate = e.allowPrivate;
3569
+ if (typeof e.ssrfStrict === "boolean") {
3570
+ mergedPolicy.egress.ssrfStrict = e.ssrfStrict;
3571
+ ssrfStrictSource = "workspace";
3572
+ }
3573
+ const ssrfAllow = hosts(e.ssrfAllow);
3574
+ if (ssrfAllow)
3575
+ mergedPolicy.egress.ssrfAllow = sanitizeSsrfAllow(ssrfAllow, "managed egress.");
3112
3576
  } else {
3113
3577
  mergedPolicy.egress = applyManagedEgress(
3114
3578
  mergedPolicy.egress,
@@ -3117,7 +3581,16 @@ function getConfig(cwd) {
3117
3581
  mode: typeof mc.egress.mode === "string" ? mc.egress.mode : void 0,
3118
3582
  allow: hosts(mc.egress.allow),
3119
3583
  deny: hosts(mc.egress.deny),
3120
- allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
3584
+ allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0,
3585
+ ssrfStrict: (() => {
3586
+ if (typeof mc.egress.ssrfStrict !== "boolean") return void 0;
3587
+ ssrfStrictSource = "workspace";
3588
+ return mc.egress.ssrfStrict;
3589
+ })(),
3590
+ ssrfAllow: (() => {
3591
+ const list = hosts(mc.egress.ssrfAllow);
3592
+ return list ? sanitizeSsrfAllow(list, "managed egress.") : void 0;
3593
+ })()
3121
3594
  },
3122
3595
  locked,
3123
3596
  egressModeUserSet
@@ -3212,13 +3685,13 @@ function getConfig(cwd) {
3212
3685
  }
3213
3686
  if (Array.isArray(mc.jailPaths)) {
3214
3687
  for (const jp of mc.jailPaths) {
3215
- const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
3216
- if (!path13) continue;
3688
+ const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
3689
+ if (!path14) continue;
3217
3690
  const verdict = jp?.verdict === "review" ? "review" : "block";
3218
- for (const r of pathRules(path13, verdict, "org-managed jail")) {
3691
+ for (const r of pathRules(path14, verdict, "org-managed jail")) {
3219
3692
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
3220
3693
  }
3221
- mergedPolicy.managedJailPaths.push({ path: path13, verdict });
3694
+ mergedPolicy.managedJailPaths.push({ path: path14, verdict });
3222
3695
  }
3223
3696
  }
3224
3697
  if (Array.isArray(mc.trustedHosts)) {
@@ -3347,6 +3820,7 @@ function getConfig(cwd) {
3347
3820
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
3348
3821
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
3349
3822
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
3823
+ const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
3350
3824
  const result = {
3351
3825
  settings: mergedSettings,
3352
3826
  policy: mergedPolicy,
@@ -3354,7 +3828,10 @@ function getConfig(cwd) {
3354
3828
  // PR-2 — the one truth introspection reads: which of the two working
3355
3829
  // modes this machine is in. 'workspace' = keyed, policy from the cloud;
3356
3830
  // 'local' = the local stack (incl. --local / named-profile keys).
3357
- policySource: keyed ? "workspace" : "local"
3831
+ policySource: keyed ? "workspace" : "local",
3832
+ // A keyed machine drops the local policy layers wholesale, so a 'local'
3833
+ // provenance recorded before the fork cannot survive into the result.
3834
+ ssrfStrictSource: resolvedSsrfStrictSource
3358
3835
  };
3359
3836
  if (!cwd) cachedConfig = result;
3360
3837
  return result;
@@ -3422,6 +3899,7 @@ var init_config = __esm({
3422
3899
  init_build();
3423
3900
  init_trusted_hosts();
3424
3901
  init_dist();
3902
+ init_dist();
3425
3903
  DANGEROUS_WORDS = [
3426
3904
  "mkfs",
3427
3905
  // formats/wipes a filesystem partition
@@ -3431,6 +3909,7 @@ var init_config = __esm({
3431
3909
  DEFAULT_CONFIG = {
3432
3910
  version: "1.0",
3433
3911
  policySource: "local",
3912
+ ssrfStrictSource: "default",
3434
3913
  settings: {
3435
3914
  mode: "standard",
3436
3915
  autoStartDaemon: true,
@@ -3614,7 +4093,17 @@ var init_config = __esm({
3614
4093
  }
3615
4094
  ],
3616
4095
  dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
3617
- egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
4096
+ egress: {
4097
+ enabled: false,
4098
+ mode: "review",
4099
+ allow: [],
4100
+ deny: [],
4101
+ allowPrivate: true,
4102
+ // The SSRF floor is always on and needs no default; these two only
4103
+ // widen or narrow it. See doc/roadmap/active/ssrf-floor-design.md.
4104
+ ssrfAllow: [],
4105
+ ssrfStrict: false
4106
+ },
3618
4107
  loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
3619
4108
  injectionScan: { enabled: false, minConfidence: "medium", allow: [] },
3620
4109
  skillPinning: { enabled: false, mode: "warn", roots: [] },
@@ -4050,12 +4539,74 @@ var init_decision = __esm({
4050
4539
  }
4051
4540
  });
4052
4541
 
4542
+ // src/shields/jail.ts
4543
+ var init_jail = __esm({
4544
+ "src/shields/jail.ts"() {
4545
+ "use strict";
4546
+ init_build();
4547
+ init_dist();
4548
+ init_shields();
4549
+ }
4550
+ });
4551
+
4552
+ // src/canary/registry.ts
4553
+ import fs7 from "fs";
4554
+ import os7 from "os";
4555
+ import path8 from "path";
4556
+ function canaryStorePath() {
4557
+ return path8.join(os7.homedir(), ".node9", "canaries.json");
4558
+ }
4559
+ function isRecord(x) {
4560
+ if (!x || typeof x !== "object") return false;
4561
+ const r = x;
4562
+ return typeof r.id === "string" && typeof r.value === "string" && typeof r.valueHash === "string";
4563
+ }
4564
+ function loadCanaries(opts) {
4565
+ const p = canaryStorePath();
4566
+ let raw;
4567
+ try {
4568
+ raw = fs7.readFileSync(p, "utf-8");
4569
+ } catch (e) {
4570
+ if (e.code === "ENOENT") return [];
4571
+ throw e;
4572
+ }
4573
+ let parsed;
4574
+ try {
4575
+ parsed = JSON.parse(raw);
4576
+ } catch (e) {
4577
+ throw new Error(
4578
+ `[node9] ${p} is not valid JSON; refusing to touch it (${e instanceof Error ? e.message : String(e)})`
4579
+ );
4580
+ }
4581
+ const recs = parsed?.records;
4582
+ if (!Array.isArray(recs)) return [];
4583
+ const out = recs.filter(isRecord);
4584
+ return opts?.includeRetired === false ? out.filter((r) => !r.retiredAt) : out;
4585
+ }
4586
+ function canaryCtxValues() {
4587
+ return loadCanaries().map((r) => ({
4588
+ id: r.id,
4589
+ value: r.value,
4590
+ retired: Boolean(r.retiredAt),
4591
+ kind: r.kind,
4592
+ path: r.path
4593
+ }));
4594
+ }
4595
+ var init_registry = __esm({
4596
+ "src/canary/registry.ts"() {
4597
+ "use strict";
4598
+ init_dist();
4599
+ init_jail();
4600
+ }
4601
+ });
4602
+
4053
4603
  // src/daemon/scan-watermark.ts
4054
4604
  var MAX_LINE_BYTES;
4055
4605
  var init_scan_watermark = __esm({
4056
4606
  "src/daemon/scan-watermark.ts"() {
4057
4607
  "use strict";
4058
4608
  init_dlp();
4609
+ init_registry();
4059
4610
  init_config();
4060
4611
  init_dist();
4061
4612
  MAX_LINE_BYTES = 2 * 1024 * 1024;
@@ -4063,9 +4614,9 @@ var init_scan_watermark = __esm({
4063
4614
  });
4064
4615
 
4065
4616
  // src/cli/aggregate/report-audit.ts
4066
- import fs7 from "fs";
4067
- import os7 from "os";
4068
- import path8 from "path";
4617
+ import fs8 from "fs";
4618
+ import os8 from "os";
4619
+ import path9 from "path";
4069
4620
  function buildTestTimestamps(allEntries) {
4070
4621
  const testTs = /* @__PURE__ */ new Set();
4071
4622
  for (const e of allEntries) {
@@ -4146,8 +4697,8 @@ function getDateRange(period, now) {
4146
4697
  }
4147
4698
  }
4148
4699
  function parseAuditLog(logPath) {
4149
- if (!fs7.existsSync(logPath)) return [];
4150
- const raw = fs7.readFileSync(logPath, "utf-8");
4700
+ if (!fs8.existsSync(logPath)) return [];
4701
+ const raw = fs8.readFileSync(logPath, "utf-8");
4151
4702
  return raw.split("\n").flatMap((line) => {
4152
4703
  if (!line.trim()) return [];
4153
4704
  try {
@@ -4201,10 +4752,10 @@ function freezeClaudeCost(acc) {
4201
4752
  };
4202
4753
  }
4203
4754
  function processClaudeCostProject(proj, projectsDir, start, end, acc) {
4204
- const projPath = path8.join(projectsDir, proj);
4755
+ const projPath = path9.join(projectsDir, proj);
4205
4756
  let files;
4206
4757
  try {
4207
- const stat = fs7.statSync(projPath);
4758
+ const stat = fs8.statSync(projPath);
4208
4759
  if (!stat.isDirectory()) return;
4209
4760
  files = listSessionFiles(projPath);
4210
4761
  } catch {
@@ -4212,14 +4763,14 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
4212
4763
  }
4213
4764
  const startMs = start.getTime();
4214
4765
  for (const file of files) {
4215
- const filePath = path8.join(projPath, file);
4766
+ const filePath = path9.join(projPath, file);
4216
4767
  try {
4217
- if (fs7.statSync(filePath).mtimeMs < startMs) continue;
4768
+ if (fs8.statSync(filePath).mtimeMs < startMs) continue;
4218
4769
  } catch {
4219
4770
  continue;
4220
4771
  }
4221
4772
  try {
4222
- const raw = fs7.readFileSync(filePath, "utf-8");
4773
+ const raw = fs8.readFileSync(filePath, "utf-8");
4223
4774
  for (const line of raw.split("\n")) {
4224
4775
  if (!line.trim()) continue;
4225
4776
  let entry;
@@ -4269,10 +4820,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
4269
4820
  }
4270
4821
  function loadClaudeCost(start, end, projectsDir) {
4271
4822
  const acc = emptyClaudeCostAccumulator();
4272
- if (!fs7.existsSync(projectsDir)) return freezeClaudeCost(acc);
4823
+ if (!fs8.existsSync(projectsDir)) return freezeClaudeCost(acc);
4273
4824
  let dirs;
4274
4825
  try {
4275
- dirs = fs7.readdirSync(projectsDir);
4826
+ dirs = fs8.readdirSync(projectsDir);
4276
4827
  } catch {
4277
4828
  return freezeClaudeCost(acc);
4278
4829
  }
@@ -4283,10 +4834,10 @@ function loadClaudeCost(start, end, projectsDir) {
4283
4834
  }
4284
4835
  async function loadClaudeCostAsync(start, end, projectsDir) {
4285
4836
  const acc = emptyClaudeCostAccumulator();
4286
- if (!fs7.existsSync(projectsDir)) return freezeClaudeCost(acc);
4837
+ if (!fs8.existsSync(projectsDir)) return freezeClaudeCost(acc);
4287
4838
  let dirs;
4288
4839
  try {
4289
- dirs = fs7.readdirSync(projectsDir);
4840
+ dirs = fs8.readdirSync(projectsDir);
4290
4841
  } catch {
4291
4842
  return freezeClaudeCost(acc);
4292
4843
  }
@@ -4299,7 +4850,7 @@ async function loadClaudeCostAsync(start, end, projectsDir) {
4299
4850
  function processCodexCostFile(filePath, start, end, acc) {
4300
4851
  let lines;
4301
4852
  try {
4302
- lines = fs7.readFileSync(filePath, "utf-8").split("\n");
4853
+ lines = fs8.readFileSync(filePath, "utf-8").split("\n");
4303
4854
  } catch {
4304
4855
  return;
4305
4856
  }
@@ -4354,31 +4905,31 @@ function processCodexCostFile(filePath, start, end, acc) {
4354
4905
  }
4355
4906
  function listCodexSessionFiles(sessionsBase) {
4356
4907
  const jsonlFiles = [];
4357
- if (!fs7.existsSync(sessionsBase)) return jsonlFiles;
4908
+ if (!fs8.existsSync(sessionsBase)) return jsonlFiles;
4358
4909
  try {
4359
- for (const year of fs7.readdirSync(sessionsBase)) {
4360
- const yearPath = path8.join(sessionsBase, year);
4910
+ for (const year of fs8.readdirSync(sessionsBase)) {
4911
+ const yearPath = path9.join(sessionsBase, year);
4361
4912
  try {
4362
- if (!fs7.statSync(yearPath).isDirectory()) continue;
4913
+ if (!fs8.statSync(yearPath).isDirectory()) continue;
4363
4914
  } catch {
4364
4915
  continue;
4365
4916
  }
4366
- for (const month of fs7.readdirSync(yearPath)) {
4367
- const monthPath = path8.join(yearPath, month);
4917
+ for (const month of fs8.readdirSync(yearPath)) {
4918
+ const monthPath = path9.join(yearPath, month);
4368
4919
  try {
4369
- if (!fs7.statSync(monthPath).isDirectory()) continue;
4920
+ if (!fs8.statSync(monthPath).isDirectory()) continue;
4370
4921
  } catch {
4371
4922
  continue;
4372
4923
  }
4373
- for (const day of fs7.readdirSync(monthPath)) {
4374
- const dayPath = path8.join(monthPath, day);
4924
+ for (const day of fs8.readdirSync(monthPath)) {
4925
+ const dayPath = path9.join(monthPath, day);
4375
4926
  try {
4376
- if (!fs7.statSync(dayPath).isDirectory()) continue;
4927
+ if (!fs8.statSync(dayPath).isDirectory()) continue;
4377
4928
  } catch {
4378
4929
  continue;
4379
4930
  }
4380
- for (const file of fs7.readdirSync(dayPath)) {
4381
- if (file.endsWith(".jsonl")) jsonlFiles.push(path8.join(dayPath, file));
4931
+ for (const file of fs8.readdirSync(dayPath)) {
4932
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path9.join(dayPath, file));
4382
4933
  }
4383
4934
  }
4384
4935
  }
@@ -4459,13 +5010,13 @@ function freezeGeminiCost(acc) {
4459
5010
  function processGeminiCostFile(filePath, projectKey, start, end, acc) {
4460
5011
  const startMs = start.getTime();
4461
5012
  try {
4462
- if (fs7.statSync(filePath).mtimeMs < startMs) return;
5013
+ if (fs8.statSync(filePath).mtimeMs < startMs) return;
4463
5014
  } catch {
4464
5015
  return;
4465
5016
  }
4466
5017
  let raw;
4467
5018
  try {
4468
- raw = fs7.readFileSync(filePath, "utf-8");
5019
+ raw = fs8.readFileSync(filePath, "utf-8");
4469
5020
  } catch {
4470
5021
  return;
4471
5022
  }
@@ -4514,30 +5065,30 @@ function listGeminiSessionFiles(geminiTmpDir) {
4514
5065
  const out = [];
4515
5066
  let dirs;
4516
5067
  try {
4517
- if (!fs7.statSync(geminiTmpDir).isDirectory()) return out;
4518
- dirs = fs7.readdirSync(geminiTmpDir);
5068
+ if (!fs8.statSync(geminiTmpDir).isDirectory()) return out;
5069
+ dirs = fs8.readdirSync(geminiTmpDir);
4519
5070
  } catch {
4520
5071
  return out;
4521
5072
  }
4522
5073
  for (const proj of dirs) {
4523
- const chatsDir = path8.join(geminiTmpDir, proj, "chats");
5074
+ const chatsDir = path9.join(geminiTmpDir, proj, "chats");
4524
5075
  let files;
4525
5076
  try {
4526
- if (!fs7.statSync(chatsDir).isDirectory()) continue;
4527
- files = fs7.readdirSync(chatsDir);
5077
+ if (!fs8.statSync(chatsDir).isDirectory()) continue;
5078
+ files = fs8.readdirSync(chatsDir);
4528
5079
  } catch {
4529
5080
  continue;
4530
5081
  }
4531
5082
  for (const f of files) {
4532
5083
  if (!f.endsWith(".jsonl")) continue;
4533
- out.push({ projectKey: proj, file: path8.join(chatsDir, f) });
5084
+ out.push({ projectKey: proj, file: path9.join(chatsDir, f) });
4534
5085
  }
4535
5086
  }
4536
5087
  return out;
4537
5088
  }
4538
5089
  function loadGeminiCost(start, end, geminiTmpDir) {
4539
5090
  const acc = emptyGeminiAccumulator();
4540
- if (!fs7.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
5091
+ if (!fs8.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4541
5092
  for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
4542
5093
  processGeminiCostFile(file, projectKey, start, end, acc);
4543
5094
  }
@@ -4545,7 +5096,7 @@ function loadGeminiCost(start, end, geminiTmpDir) {
4545
5096
  }
4546
5097
  async function loadGeminiCostAsync(start, end, geminiTmpDir) {
4547
5098
  const acc = emptyGeminiAccumulator();
4548
- if (!fs7.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
5099
+ if (!fs8.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
4549
5100
  const files = listGeminiSessionFiles(geminiTmpDir);
4550
5101
  const CHUNK_SIZE = 5;
4551
5102
  for (let i = 0; i < files.length; i++) {
@@ -4558,6 +5109,7 @@ async function loadGeminiCostAsync(start, end, geminiTmpDir) {
4558
5109
  }
4559
5110
  function dimensionOfBlock(checkedBy, ruleName) {
4560
5111
  if (checkedBy.includes("egress")) return "network";
5112
+ if (ruleName.startsWith("ssrf:")) return "network";
4561
5113
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
4562
5114
  if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
4563
5115
  return "detection";
@@ -4568,11 +5120,11 @@ function dimensionOfBlock(checkedBy, ruleName) {
4568
5120
  }
4569
5121
  function aggregateReportFromAudit(period, opts = {}) {
4570
5122
  const now = opts.now ?? /* @__PURE__ */ new Date();
4571
- const auditLogPath2 = opts.auditLogPath ?? path8.join(os7.homedir(), ".node9", "audit.log");
4572
- const claudeProjectsDir = opts.claudeProjectsDir ?? path8.join(os7.homedir(), ".claude", "projects");
4573
- const codexSessionsDir = opts.codexSessionsDir ?? path8.join(os7.homedir(), ".codex", "sessions");
4574
- const geminiTmpDir = opts.geminiTmpDir ?? path8.join(os7.homedir(), ".gemini", "tmp");
4575
- const hasAuditFile = fs7.existsSync(auditLogPath2);
5123
+ const auditLogPath2 = opts.auditLogPath ?? path9.join(os8.homedir(), ".node9", "audit.log");
5124
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path9.join(os8.homedir(), ".claude", "projects");
5125
+ const codexSessionsDir = opts.codexSessionsDir ?? path9.join(os8.homedir(), ".codex", "sessions");
5126
+ const geminiTmpDir = opts.geminiTmpDir ?? path9.join(os8.homedir(), ".gemini", "tmp");
5127
+ const hasAuditFile = fs8.existsSync(auditLogPath2);
4576
5128
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath2);
4577
5129
  const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
4578
5130
  const { start, end } = getDateRange(period, now);
@@ -4826,18 +5378,18 @@ var init_keyed_guard = __esm({
4826
5378
  });
4827
5379
 
4828
5380
  // src/utils/provenance.ts
4829
- import path9 from "path";
4830
- import os8 from "os";
5381
+ import path10 from "path";
5382
+ import os9 from "os";
4831
5383
  var USER_PREFIXES;
4832
5384
  var init_provenance = __esm({
4833
5385
  "src/utils/provenance.ts"() {
4834
5386
  "use strict";
4835
5387
  USER_PREFIXES = [
4836
- path9.join(os8.homedir(), "bin"),
4837
- path9.join(os8.homedir(), ".local", "bin"),
4838
- path9.join(os8.homedir(), ".cargo", "bin"),
4839
- path9.join(os8.homedir(), ".npm-global", "bin"),
4840
- path9.join(os8.homedir(), ".volta", "bin")
5388
+ path10.join(os9.homedir(), "bin"),
5389
+ path10.join(os9.homedir(), ".local", "bin"),
5390
+ path10.join(os9.homedir(), ".cargo", "bin"),
5391
+ path10.join(os9.homedir(), ".npm-global", "bin"),
5392
+ path10.join(os9.homedir(), ".volta", "bin")
4841
5393
  ];
4842
5394
  }
4843
5395
  });
@@ -4881,14 +5433,14 @@ var init_mcp_pin = __esm({
4881
5433
  });
4882
5434
 
4883
5435
  // src/daemon/hook-baseline.ts
4884
- import path10 from "path";
4885
- import os9 from "os";
5436
+ import path11 from "path";
5437
+ import os10 from "os";
4886
5438
  var BASELINE_FILE, NOTIFIED_FILE;
4887
5439
  var init_hook_baseline = __esm({
4888
5440
  "src/daemon/hook-baseline.ts"() {
4889
5441
  "use strict";
4890
- BASELINE_FILE = path10.join(os9.homedir(), ".node9", "hooks-baseline.json");
4891
- NOTIFIED_FILE = path10.join(os9.homedir(), ".node9", "hook-heal-notified.json");
5442
+ BASELINE_FILE = path11.join(os10.homedir(), ".node9", "hooks-baseline.json");
5443
+ NOTIFIED_FILE = path11.join(os10.homedir(), ".node9", "hook-heal-notified.json");
4892
5444
  }
4893
5445
  });
4894
5446
 
@@ -4947,9 +5499,9 @@ var init_scan_history = __esm({
4947
5499
 
4948
5500
  // src/cli/commands/scan.ts
4949
5501
  import chalk5 from "chalk";
4950
- import fs8 from "fs";
4951
- import path11 from "path";
4952
- import os10 from "os";
5502
+ import fs9 from "fs";
5503
+ import path12 from "path";
5504
+ import os11 from "os";
4953
5505
  import stringWidth2 from "string-width";
4954
5506
  function claudeModelPrice2(model) {
4955
5507
  const t = pricingFor(model);
@@ -4970,12 +5522,6 @@ function isNode9SelfOutput(text) {
4970
5522
  }
4971
5523
  return false;
4972
5524
  }
4973
- function looksLikeFixtureToken(sample) {
4974
- for (const re of FIXTURE_TOKEN_PATTERNS) {
4975
- if (re.test(sample)) return true;
4976
- }
4977
- return false;
4978
- }
4979
5525
  function stripTerminalEscapes(s) {
4980
5526
  return s.replace(TERMINAL_ESCAPE_RE, "");
4981
5527
  }
@@ -4985,7 +5531,7 @@ function preview(input, max) {
4985
5531
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4986
5532
  }
4987
5533
  function emptyScanDedup() {
4988
- return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
5534
+ return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set(), canaryIndex: /* @__PURE__ */ new Map() };
4989
5535
  }
4990
5536
  function findingKey(ruleName, inputPreview, projLabel) {
4991
5537
  return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
@@ -4993,6 +5539,73 @@ function findingKey(ruleName, inputPreview, projLabel) {
4993
5539
  function dlpKey(patternName, redactedSample, projLabel) {
4994
5540
  return `${patternName}|${redactedSample}|${projLabel}`;
4995
5541
  }
5542
+ function safeCanaryScanValues() {
5543
+ try {
5544
+ return canaryCtxValues();
5545
+ } catch {
5546
+ return [];
5547
+ }
5548
+ }
5549
+ function recordCanaries(scanned, toolName, timestamp, projLabel, sessionId, agent, result, dedup, values) {
5550
+ if (values.length === 0) return [];
5551
+ let pool = [...values];
5552
+ const matched = [];
5553
+ for (let guard = 0; guard < 8 && pool.length > 0; guard++) {
5554
+ const hit = matchCanaryArgs(scanned, pool);
5555
+ if (!hit) break;
5556
+ const v = pool.find((x) => x.id === hit.id);
5557
+ if (v) matched.push(v.value);
5558
+ pool = pool.filter((x) => x.id !== hit.id);
5559
+ const key = `canary|${hit.id}|${sessionId}|${agent}`;
5560
+ const existing = dedup.canaryIndex.get(key);
5561
+ if (existing) {
5562
+ existing.count++;
5563
+ if (timestamp && (!existing.timestamp || timestamp < existing.timestamp)) {
5564
+ existing.timestamp = timestamp;
5565
+ }
5566
+ continue;
5567
+ }
5568
+ const finding = {
5569
+ canaryId: hit.id,
5570
+ canaryHash: v?.valueHash ?? "",
5571
+ kind: v?.kind ?? "unknown",
5572
+ field: v?.field ?? "",
5573
+ path: v?.path ?? "",
5574
+ view: hit.view,
5575
+ retired: hit.retired,
5576
+ toolName,
5577
+ timestamp,
5578
+ project: projLabel,
5579
+ sessionId,
5580
+ agent,
5581
+ count: 1
5582
+ };
5583
+ dedup.canaryIndex.set(key, finding);
5584
+ result.canaryFindings.push(finding);
5585
+ }
5586
+ return matched;
5587
+ }
5588
+ function scrubDecoys(subject, values) {
5589
+ if (values.length === 0) return subject;
5590
+ const scrubText = (s) => {
5591
+ let out = s;
5592
+ for (const v of values) if (v) out = out.split(v).join("[node9-decoy]");
5593
+ return out;
5594
+ };
5595
+ const walk = (v, depth) => {
5596
+ if (depth > 6) return v;
5597
+ if (typeof v === "string") return scrubText(v);
5598
+ if (Array.isArray(v)) return v.map((x) => walk(x, depth + 1));
5599
+ if (v && typeof v === "object") {
5600
+ const out = {};
5601
+ for (const [k, child] of Object.entries(v))
5602
+ out[k] = walk(child, depth + 1);
5603
+ return out;
5604
+ }
5605
+ return v;
5606
+ };
5607
+ return walk(subject, 0);
5608
+ }
4996
5609
  function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
4997
5610
  const fsVerdict = analyzeFsOperation(command);
4998
5611
  if (!fsVerdict) return false;
@@ -5091,7 +5704,7 @@ function buildRuleSources() {
5091
5704
  }
5092
5705
  return sources;
5093
5706
  }
5094
- function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
5707
+ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, canaryVals, onProgress, onLine) {
5095
5708
  result.filesScanned++;
5096
5709
  result.sessions++;
5097
5710
  onProgress?.(result.filesScanned);
@@ -5099,7 +5712,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5099
5712
  const session = { sessionId, costUSD: 0, toolCalls: 0 };
5100
5713
  let raw;
5101
5714
  try {
5102
- raw = fs8.readFileSync(path11.join(projPath, file), "utf-8");
5715
+ raw = fs9.readFileSync(path12.join(projPath, file), "utf-8");
5103
5716
  } catch {
5104
5717
  return;
5105
5718
  }
@@ -5130,7 +5743,18 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5130
5743
  if (Array.isArray(content2)) {
5131
5744
  const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
5132
5745
  if (text) {
5133
- const dlpMatch = scanArgs({ text });
5746
+ const decoysHere0 = recordCanaries(
5747
+ { text },
5748
+ "user-prompt",
5749
+ entry.timestamp ?? "",
5750
+ projLabel,
5751
+ sessionId,
5752
+ "claude",
5753
+ result,
5754
+ dedup,
5755
+ canaryVals
5756
+ );
5757
+ const dlpMatch = scanArgs(scrubDecoys({ text }, decoysHere0));
5134
5758
  if (dlpMatch) {
5135
5759
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
5136
5760
  if (!dedup.dlpKeys.has(k)) {
@@ -5151,15 +5775,25 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5151
5775
  if (block.type !== "tool_result") continue;
5152
5776
  const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
5153
5777
  if (filePath) {
5154
- const ext = path11.extname(filePath).toLowerCase();
5778
+ const ext = path12.extname(filePath).toLowerCase();
5155
5779
  if (CODE_EXTENSIONS.has(ext)) continue;
5156
5780
  }
5157
5781
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
5158
5782
  if (!resultText) continue;
5159
5783
  if (isNode9SelfOutput(resultText)) continue;
5160
- const dlpMatch = scanArgs({ text: resultText });
5784
+ const decoysHere1 = recordCanaries(
5785
+ { text: resultText },
5786
+ "tool-result",
5787
+ entry.timestamp ?? "",
5788
+ projLabel,
5789
+ sessionId,
5790
+ "claude",
5791
+ result,
5792
+ dedup,
5793
+ canaryVals
5794
+ );
5795
+ const dlpMatch = scanArgs(scrubDecoys({ text: resultText }, decoysHere1));
5161
5796
  if (dlpMatch) {
5162
- if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
5163
5797
  if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
5164
5798
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
5165
5799
  if (!dedup.dlpKeys.has(k)) {
@@ -5211,9 +5845,20 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5211
5845
  const rawCmd = String(input.command ?? "").trimStart();
5212
5846
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
5213
5847
  const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
5214
- const inputFileExt = inputFilePath ? path11.extname(inputFilePath).toLowerCase() : "";
5215
- if (CODE_EXTENSIONS.has(inputFileExt)) continue;
5216
- const dlpMatch = scanArgs(input);
5848
+ const inputFileExt = inputFilePath ? path12.extname(inputFilePath).toLowerCase() : "";
5849
+ const canaryInInput = recordCanaries(
5850
+ input,
5851
+ toolName,
5852
+ entry.timestamp ?? "",
5853
+ projLabel,
5854
+ sessionId,
5855
+ "claude",
5856
+ result,
5857
+ dedup,
5858
+ canaryVals
5859
+ );
5860
+ if (canaryInInput.length === 0 && CODE_EXTENSIONS.has(inputFileExt)) continue;
5861
+ const dlpMatch = scanArgs(scrubDecoys(input, canaryInInput));
5217
5862
  if (dlpMatch) {
5218
5863
  if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
5219
5864
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
@@ -5308,14 +5953,14 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5308
5953
  }
5309
5954
  result.perSession.push(session);
5310
5955
  }
5311
- async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
5312
- const projPath = path11.join(projectsDir, proj);
5956
+ async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, canaryVals, onProgress, onLine) {
5957
+ const projPath = path12.join(projectsDir, proj);
5313
5958
  try {
5314
- if (!fs8.statSync(projPath).isDirectory()) return;
5959
+ if (!fs9.statSync(projPath).isDirectory()) return;
5315
5960
  } catch {
5316
5961
  return;
5317
5962
  }
5318
- const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os10.homedir(), "~")).slice(
5963
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os11.homedir(), "~")).slice(
5319
5964
  0,
5320
5965
  40
5321
5966
  );
@@ -5334,6 +5979,7 @@ async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDa
5334
5979
  startDate,
5335
5980
  result,
5336
5981
  dedup,
5982
+ canaryVals,
5337
5983
  onProgress,
5338
5984
  onLine
5339
5985
  );
@@ -5351,6 +5997,7 @@ function emptyClaudeScan() {
5351
5997
  bashCalls: 0,
5352
5998
  findings: [],
5353
5999
  dlpFindings: [],
6000
+ canaryFindings: [],
5354
6001
  loopFindings: [],
5355
6002
  totalCostUSD: 0,
5356
6003
  firstDate: null,
@@ -5360,17 +6007,18 @@ function emptyClaudeScan() {
5360
6007
  };
5361
6008
  }
5362
6009
  async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
5363
- const projectsDir = path11.join(os10.homedir(), ".claude", "projects");
6010
+ const projectsDir = path12.join(os11.homedir(), ".claude", "projects");
5364
6011
  const result = emptyClaudeScan();
5365
- if (!fs8.existsSync(projectsDir)) return result;
6012
+ if (!fs9.existsSync(projectsDir)) return result;
5366
6013
  let projDirs;
5367
6014
  try {
5368
- projDirs = fs8.readdirSync(projectsDir);
6015
+ projDirs = fs9.readdirSync(projectsDir);
5369
6016
  } catch {
5370
6017
  return result;
5371
6018
  }
5372
6019
  const ruleSources = buildRuleSources();
5373
6020
  const dedup = emptyScanDedup();
6021
+ const canaryVals = safeCanaryScanValues();
5374
6022
  for (const proj of projDirs) {
5375
6023
  await processClaudeProjectAsync(
5376
6024
  proj,
@@ -5379,6 +6027,7 @@ async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
5379
6027
  startDate,
5380
6028
  result,
5381
6029
  dedup,
6030
+ canaryVals,
5382
6031
  onProgress,
5383
6032
  onLine
5384
6033
  );
@@ -5386,7 +6035,8 @@ async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
5386
6035
  return result;
5387
6036
  }
5388
6037
  function scanGeminiHistory(startDate, onProgress, onLine) {
5389
- const tmpDir = path11.join(os10.homedir(), ".gemini", "tmp");
6038
+ const canaryVals = safeCanaryScanValues();
6039
+ const tmpDir = path12.join(os11.homedir(), ".gemini", "tmp");
5390
6040
  const result = {
5391
6041
  filesScanned: 0,
5392
6042
  sessions: 0,
@@ -5394,6 +6044,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5394
6044
  bashCalls: 0,
5395
6045
  findings: [],
5396
6046
  dlpFindings: [],
6047
+ canaryFindings: [],
5397
6048
  loopFindings: [],
5398
6049
  totalCostUSD: 0,
5399
6050
  firstDate: null,
@@ -5402,33 +6053,33 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5402
6053
  perSession: []
5403
6054
  };
5404
6055
  const dedup = emptyScanDedup();
5405
- if (!fs8.existsSync(tmpDir)) return result;
6056
+ if (!fs9.existsSync(tmpDir)) return result;
5406
6057
  let slugDirs;
5407
6058
  try {
5408
- slugDirs = fs8.readdirSync(tmpDir);
6059
+ slugDirs = fs9.readdirSync(tmpDir);
5409
6060
  } catch {
5410
6061
  return result;
5411
6062
  }
5412
6063
  const ruleSources = buildRuleSources();
5413
6064
  for (const slug2 of slugDirs) {
5414
- const slugPath = path11.join(tmpDir, slug2);
6065
+ const slugPath = path12.join(tmpDir, slug2);
5415
6066
  try {
5416
- if (!fs8.statSync(slugPath).isDirectory()) continue;
6067
+ if (!fs9.statSync(slugPath).isDirectory()) continue;
5417
6068
  } catch {
5418
6069
  continue;
5419
6070
  }
5420
6071
  let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
5421
6072
  try {
5422
6073
  projLabel = stripTerminalEscapes(
5423
- fs8.readFileSync(path11.join(slugPath, ".project_root"), "utf-8").trim()
5424
- ).replace(os10.homedir(), "~").slice(0, 40);
6074
+ fs9.readFileSync(path12.join(slugPath, ".project_root"), "utf-8").trim()
6075
+ ).replace(os11.homedir(), "~").slice(0, 40);
5425
6076
  } catch {
5426
6077
  }
5427
- const chatsDir = path11.join(slugPath, "chats");
5428
- if (!fs8.existsSync(chatsDir)) continue;
6078
+ const chatsDir = path12.join(slugPath, "chats");
6079
+ if (!fs9.existsSync(chatsDir)) continue;
5429
6080
  let chatFiles;
5430
6081
  try {
5431
- chatFiles = fs8.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
6082
+ chatFiles = fs9.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
5432
6083
  } catch {
5433
6084
  continue;
5434
6085
  }
@@ -5441,7 +6092,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5441
6092
  onProgress?.(result.filesScanned);
5442
6093
  let raw;
5443
6094
  try {
5444
- raw = fs8.readFileSync(path11.join(chatsDir, chatFile), "utf-8");
6095
+ raw = fs9.readFileSync(path12.join(chatsDir, chatFile), "utf-8");
5445
6096
  } catch {
5446
6097
  continue;
5447
6098
  }
@@ -5470,7 +6121,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5470
6121
  const content = msg.content;
5471
6122
  const text = Array.isArray(content) ? content.map((c) => c.text ?? "").join("\n") : typeof content === "string" ? content : "";
5472
6123
  if (text) {
5473
- const dlpMatch = scanArgs({ text });
6124
+ const decoysHere3 = recordCanaries(
6125
+ { text },
6126
+ "user-prompt",
6127
+ msg.timestamp ?? "",
6128
+ projLabel,
6129
+ sessionId,
6130
+ "gemini",
6131
+ result,
6132
+ dedup,
6133
+ canaryVals
6134
+ );
6135
+ const dlpMatch = scanArgs(scrubDecoys({ text }, decoysHere3));
5474
6136
  if (dlpMatch) {
5475
6137
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
5476
6138
  if (!dedup.dlpKeys.has(k)) {
@@ -5517,7 +6179,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5517
6179
  const rawCmd = String(input.command ?? "").trimStart();
5518
6180
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
5519
6181
  continue;
5520
- const dlpMatch = scanArgs(input);
6182
+ const decoysHere4 = recordCanaries(
6183
+ input,
6184
+ toolName,
6185
+ msg.timestamp ?? "",
6186
+ projLabel,
6187
+ sessionId,
6188
+ "gemini",
6189
+ result,
6190
+ dedup,
6191
+ canaryVals
6192
+ );
6193
+ const dlpMatch = scanArgs(scrubDecoys(input, decoysHere4));
5521
6194
  if (dlpMatch) {
5522
6195
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
5523
6196
  if (!dedup.dlpKeys.has(k)) {
@@ -5614,7 +6287,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5614
6287
  return result;
5615
6288
  }
5616
6289
  function scanCodexHistory(startDate, onProgress, onLine) {
5617
- const sessionsBase = path11.join(os10.homedir(), ".codex", "sessions");
6290
+ const canaryVals = safeCanaryScanValues();
6291
+ const sessionsBase = path12.join(os11.homedir(), ".codex", "sessions");
5618
6292
  const result = {
5619
6293
  filesScanned: 0,
5620
6294
  sessions: 0,
@@ -5622,6 +6296,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5622
6296
  bashCalls: 0,
5623
6297
  findings: [],
5624
6298
  dlpFindings: [],
6299
+ canaryFindings: [],
5625
6300
  loopFindings: [],
5626
6301
  totalCostUSD: 0,
5627
6302
  firstDate: null,
@@ -5630,32 +6305,32 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5630
6305
  perSession: []
5631
6306
  };
5632
6307
  const dedup = emptyScanDedup();
5633
- if (!fs8.existsSync(sessionsBase)) return result;
6308
+ if (!fs9.existsSync(sessionsBase)) return result;
5634
6309
  const jsonlFiles = [];
5635
6310
  try {
5636
- for (const year of fs8.readdirSync(sessionsBase)) {
5637
- const yearPath = path11.join(sessionsBase, year);
6311
+ for (const year of fs9.readdirSync(sessionsBase)) {
6312
+ const yearPath = path12.join(sessionsBase, year);
5638
6313
  try {
5639
- if (!fs8.statSync(yearPath).isDirectory()) continue;
6314
+ if (!fs9.statSync(yearPath).isDirectory()) continue;
5640
6315
  } catch {
5641
6316
  continue;
5642
6317
  }
5643
- for (const month of fs8.readdirSync(yearPath)) {
5644
- const monthPath = path11.join(yearPath, month);
6318
+ for (const month of fs9.readdirSync(yearPath)) {
6319
+ const monthPath = path12.join(yearPath, month);
5645
6320
  try {
5646
- if (!fs8.statSync(monthPath).isDirectory()) continue;
6321
+ if (!fs9.statSync(monthPath).isDirectory()) continue;
5647
6322
  } catch {
5648
6323
  continue;
5649
6324
  }
5650
- for (const day of fs8.readdirSync(monthPath)) {
5651
- const dayPath = path11.join(monthPath, day);
6325
+ for (const day of fs9.readdirSync(monthPath)) {
6326
+ const dayPath = path12.join(monthPath, day);
5652
6327
  try {
5653
- if (!fs8.statSync(dayPath).isDirectory()) continue;
6328
+ if (!fs9.statSync(dayPath).isDirectory()) continue;
5654
6329
  } catch {
5655
6330
  continue;
5656
6331
  }
5657
- for (const file of fs8.readdirSync(dayPath)) {
5658
- if (file.endsWith(".jsonl")) jsonlFiles.push(path11.join(dayPath, file));
6332
+ for (const file of fs9.readdirSync(dayPath)) {
6333
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path12.join(dayPath, file));
5659
6334
  }
5660
6335
  }
5661
6336
  }
@@ -5669,7 +6344,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5669
6344
  onProgress?.(result.filesScanned);
5670
6345
  let lines;
5671
6346
  try {
5672
- lines = fs8.readFileSync(filePath, "utf-8").split("\n");
6347
+ lines = fs9.readFileSync(filePath, "utf-8").split("\n");
5673
6348
  } catch {
5674
6349
  continue;
5675
6350
  }
@@ -5696,7 +6371,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5696
6371
  sessionId = String(payload["id"] ?? filePath);
5697
6372
  startTime = String(payload["timestamp"] ?? "");
5698
6373
  const cwd = String(payload["cwd"] ?? "");
5699
- projLabel = stripTerminalEscapes(cwd.replace(os10.homedir(), "~")).slice(0, 40);
6374
+ projLabel = stripTerminalEscapes(cwd.replace(os11.homedir(), "~")).slice(0, 40);
5700
6375
  continue;
5701
6376
  }
5702
6377
  if (entry.type === "turn_context" && typeof payload["model"] === "string") {
@@ -5714,7 +6389,18 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5714
6389
  if (entry.type === "event_msg" && payload["type"] === "user_message") {
5715
6390
  const text = String(payload["message"] ?? "");
5716
6391
  if (text) {
5717
- const dlpMatch2 = scanArgs({ text });
6392
+ const decoysHere9 = recordCanaries(
6393
+ { text },
6394
+ "user-prompt",
6395
+ entry.timestamp ?? startTime,
6396
+ projLabel,
6397
+ sessionId,
6398
+ "codex",
6399
+ result,
6400
+ dedup,
6401
+ canaryVals
6402
+ );
6403
+ const dlpMatch2 = scanArgs(scrubDecoys({ text }, decoysHere9));
5718
6404
  if (dlpMatch2) {
5719
6405
  const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
5720
6406
  if (!dedup.dlpKeys.has(k)) {
@@ -5758,7 +6444,18 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5758
6444
  }
5759
6445
  const rawCmd = String(input["command"] ?? "").trimStart();
5760
6446
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
5761
- const dlpMatch = scanArgs(input);
6447
+ const decoysHere10 = recordCanaries(
6448
+ input,
6449
+ toolName,
6450
+ ts,
6451
+ projLabel,
6452
+ sessionId,
6453
+ "codex",
6454
+ result,
6455
+ dedup,
6456
+ canaryVals
6457
+ );
6458
+ const dlpMatch = scanArgs(scrubDecoys(input, decoysHere10));
5762
6459
  if (dlpMatch) {
5763
6460
  const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
5764
6461
  if (!dedup.dlpKeys.has(k)) {
@@ -5857,7 +6554,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5857
6554
  }
5858
6555
  return result;
5859
6556
  }
5860
- var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
6557
+ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
5861
6558
  var init_scan = __esm({
5862
6559
  "src/cli/commands/scan.ts"() {
5863
6560
  "use strict";
@@ -5867,6 +6564,8 @@ var init_scan = __esm({
5867
6564
  init_policy();
5868
6565
  init_dist();
5869
6566
  init_dlp();
6567
+ init_dlp();
6568
+ init_registry();
5870
6569
  init_litellm();
5871
6570
  init_cost_gemini();
5872
6571
  init_cost_codex();
@@ -5915,16 +6614,6 @@ var init_scan = __esm({
5915
6614
  /\bseverity:\s*['"](?:block|review|allow)['"]/,
5916
6615
  /NODE9 SECURITY ALERT/
5917
6616
  ];
5918
- FIXTURE_TOKEN_PATTERNS = [
5919
- /(.)\1{5,}/,
5920
- // 6+ repeated characters (aaaaaa, 000000)
5921
- /(?:EXAMPLE|FAKE|DUMMY|PLACEHOLDER|XXXXX)/i,
5922
- /abcdefghijklmn/i,
5923
- // long alpha sequence — fixture, not entropy
5924
- /1234567890/,
5925
- // long digit sequence — fixture, not entropy
5926
- /qwerty/i
5927
- ];
5928
6617
  TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
5929
6618
  LOOP_TOOLS = /* @__PURE__ */ new Set([
5930
6619
  "bash",
@@ -5944,23 +6633,23 @@ var init_scan = __esm({
5944
6633
  });
5945
6634
 
5946
6635
  // src/tui/dashboard/data.ts
5947
- import fs9 from "fs";
5948
- import os11 from "os";
5949
- import path12 from "path";
6636
+ import fs10 from "fs";
6637
+ import os12 from "os";
6638
+ import path13 from "path";
5950
6639
  import http from "http";
5951
6640
  function auditLogPath() {
5952
- return path12.join(os11.homedir(), ".node9", "audit.log");
6641
+ return path13.join(os12.homedir(), ".node9", "audit.log");
5953
6642
  }
5954
6643
  function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
5955
6644
  return new Promise((resolve) => {
5956
6645
  const p = customPath ?? auditLogPath();
5957
- if (!fs9.existsSync(p)) {
6646
+ if (!fs10.existsSync(p)) {
5958
6647
  resolve([]);
5959
6648
  return;
5960
6649
  }
5961
6650
  let raw;
5962
6651
  try {
5963
- raw = fs9.readFileSync(p, "utf8");
6652
+ raw = fs10.readFileSync(p, "utf8");
5964
6653
  } catch {
5965
6654
  resolve([]);
5966
6655
  return;
@@ -6092,13 +6781,13 @@ function loadBlast() {
6092
6781
  }
6093
6782
  }
6094
6783
  function shortenPath(p) {
6095
- const home = os11.homedir();
6784
+ const home = os12.homedir();
6096
6785
  return p.startsWith(home) ? p.replace(home, "~") : p;
6097
6786
  }
6098
6787
  async function loadReportAuditAsync(period) {
6099
- const claudeProjectsDir = path12.join(os11.homedir(), ".claude", "projects");
6100
- const codexSessionsDir = path12.join(os11.homedir(), ".codex", "sessions");
6101
- const geminiTmpDir = path12.join(os11.homedir(), ".gemini", "tmp");
6788
+ const claudeProjectsDir = path13.join(os12.homedir(), ".claude", "projects");
6789
+ const codexSessionsDir = path13.join(os12.homedir(), ".codex", "sessions");
6790
+ const geminiTmpDir = path13.join(os12.homedir(), ".gemini", "tmp");
6102
6791
  const { start, end } = getDateRange(period, /* @__PURE__ */ new Date());
6103
6792
  const entries = await readAuditEntriesAsync();
6104
6793
  void ensurePricingLoaded();
@@ -7197,8 +7886,8 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-run
7197
7886
  function TopToolsProjects({ audit }) {
7198
7887
  const data = audit?.data;
7199
7888
  const tools = data ? [...data.toolMap.entries()].sort(([, a], [, b]) => b.calls - a.calls).slice(0, ROW_LIMIT) : [];
7200
- const projects = data ? [...data.cost.byProject.entries()].map(([path13, r]) => ({
7201
- name: basenameOf(path13),
7889
+ const projects = data ? [...data.cost.byProject.entries()].map(([path14, r]) => ({
7890
+ name: basenameOf(path14),
7202
7891
  cost: r.cost,
7203
7892
  tokens: r.inputTokens + r.outputTokens
7204
7893
  })).sort((a, b) => b.cost - a.cost).slice(0, ROW_LIMIT) : [];
@@ -7635,8 +8324,8 @@ function pickTopLoopFile(loops) {
7635
8324
  map.set(k, (map.get(k) ?? 0) + (l.count ?? 0));
7636
8325
  }
7637
8326
  if (map.size === 0) return void 0;
7638
- const [path13, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
7639
- return { path: path13, count };
8327
+ const [path14, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
8328
+ return { path: path14, count };
7640
8329
  }
7641
8330
  var EMPTY_FILTERED_SCAN;
7642
8331
  var init_derive = __esm({