@mrciphersmith/keryx 0.4.5 → 0.4.6

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/core.js CHANGED
@@ -2496,7 +2496,7 @@ function detectPii(content) {
2496
2496
  perRule[PHONE_RULE] = dropRescansInsideCards(perRule[PHONE_RULE], perRule[CARD_RULE]);
2497
2497
  return perRule.flat();
2498
2498
  }
2499
- var UNICODE_EMAIL, ASCII_EMAIL, emailScanner, asciiEmailScanner, PHONE_FROM, PHONE_RUN_CHAR, PHONE_START_BLOCKER, DIGIT, PHONE_DIAL_GROUP, PHONE_FIRST_GROUP, PHONE_SEPARATOR, PHONE_NANP_SPACED, WHITESPACE, LINE_BREAK, PHONE_MAX_DIGITS = 15, PHONE_MIN_DIGITS = 9, rescanned, PHONE_JOINED_PIECE, PHONE_CARD_SHAPE, PHONE_GLUE, PHONE_GLUE_LETTER, PHONE_GLUE_EVIDENCE, phoneGlueScanner, phoneScanner, IBAN_START, IBAN_MAX_GROUPS = 8, IBAN_STATES, ibanFailed, ibanStamp = 0, ibanScanner, IBAN_LENGTHS, IBAN_GLUE_START, ibanGluedScanner, CARD_SEPARATOR = "[ \\t\\r\\n._/-]", CARD_EXTRA, CARD_GROUP_SEPARATOR, GLUE_LETTER_BEFORE, GLUE_LETTER_AFTER, NON_HEX_LETTER, GLUE_FLANK = 8, UUID_ANYWHERE, UUID_REACH = 36, SSN_BREAK_EXTRA, IPV4_EXTRA, RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN, rescanTag, NORMALIZE_CHUNK, NORMALIZE_OVERLAP = 512, NORMALIZE_SKIP = 256, NON_ASCII, DECIMAL_DIGIT, digitValues, BREAK_CHAR, PHONE_RULE, CARD_RULE, NUMBER_RULES, FULLWIDTH_CAPITAL, SOFT_HYPHEN, NUMBER_BREAK;
2499
+ var UNICODE_EMAIL, ASCII_EMAIL, emailScanner, asciiEmailScanner, PHONE_FROM, PHONE_RUN_CHAR, PHONE_START_BLOCKER, DIGIT, PHONE_DIAL_GROUP, PHONE_FIRST_GROUP, PHONE_SEPARATOR, PHONE_NANP_SPACED, WHITESPACE, LINE_BREAK, PHONE_MAX_DIGITS = 15, PHONE_MIN_DIGITS = 9, rescanned, PHONE_JOINED_PIECE, PHONE_CARD_SHAPE, PHONE_GLUE, PHONE_GLUE_LETTER, PHONE_GLUE_EVIDENCE, phoneGlueScanner, phoneScanner, IBAN_START, IBAN_MAX_GROUPS = 8, IBAN_STATES, ibanFailed, ibanStamp = 0, ibanScanner, IBAN_LENGTHS, IBAN_GLUE_START, ibanGluedScanner, CARD_SEPARATOR = "[ \\t\\r\\n._/-]", CARD_EXTRA, CARD_GROUP_SEPARATOR, GLUE_LETTER_BEFORE, GLUE_LETTER_AFTER, NON_HEX_LETTER, GLUE_FLANK = 8, UUID_ANYWHERE, UUID_REACH = 36, SSN_BREAK_EXTRA, IPV4_EXTRA, IPV6_COMPRESSED, RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN, rescanTag, NORMALIZE_CHUNK, NORMALIZE_OVERLAP = 512, NORMALIZE_SKIP = 256, NON_ASCII, DECIMAL_DIGIT, digitValues, BREAK_CHAR, PHONE_RULE, CARD_RULE, NUMBER_RULES, FULLWIDTH_CAPITAL, SOFT_HYPHEN, NUMBER_BREAK;
2500
2500
  var init_pii = __esm(() => {
2501
2501
  UNICODE_EMAIL = {
2502
2502
  from: /(?:(?<=[\p{L}\p{N}\p{M}_])(?![\p{L}\p{N}\p{M}_])|(?<![\p{L}\p{N}\p{M}_])(?=[\p{L}\p{N}\p{M}_]))[\p{L}\p{N}\p{M}._%+-]+@[\p{L}\p{N}\p{M}.-]+\.[\p{L}\p{M}]{2,}(?![\p{L}\p{N}\p{M}_])/uy,
@@ -2794,6 +2794,7 @@ var init_pii = __esm(() => {
2794
2794
  UUID_ANYWHERE = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/g;
2795
2795
  SSN_BREAK_EXTRA = /(?<![0-9])\d{3}(?:\r\n|[\t\r\n])\d{2}(?:\r\n|[\t\r\n])\d{4}(?![0-9])/g;
2796
2796
  IPV4_EXTRA = /(?<![0-9.])(?<!\b[vV])\d{1,3}(?:[\t\r\n]{0,2}\.[\t\r\n]{0,2}\d{1,3}){3}(?![0-9])/g;
2797
+ IPV6_COMPRESSED = /(?<![0-9A-Fa-f:])(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})?::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})?(?![0-9A-Fa-f:])/g;
2797
2798
  RULES = [
2798
2799
  {
2799
2800
  policyId: "pii.email",
@@ -2867,7 +2868,10 @@ var init_pii = __esm(() => {
2867
2868
  severity: "low",
2868
2869
  confidence: 0.6,
2869
2870
  validate: isValidIp,
2870
- also: [{ regex: IPV4_EXTRA, validate: (value) => isValidIp(value.replace(/[\t\r\n]/g, "")) }]
2871
+ also: [
2872
+ { regex: IPV4_EXTRA, validate: (value) => isValidIp(value.replace(/[\t\r\n]/g, "")) },
2873
+ { regex: IPV6_COMPRESSED, validate: (value) => /[0-9]/.test(value) && isValidIp(value) }
2874
+ ]
2871
2875
  }
2872
2876
  ];
2873
2877
  CALENDAR_DATE = /(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/;
@@ -2888,29 +2892,55 @@ var init_pii = __esm(() => {
2888
2892
  });
2889
2893
 
2890
2894
  // src/security/detect/secrets.ts
2895
+ function scanPrivateKeyBlocks(content) {
2896
+ const spans = [];
2897
+ PRIVATE_KEY_BEGIN.lastIndex = 0;
2898
+ let begin;
2899
+ while ((begin = PRIVATE_KEY_BEGIN.exec(content)) !== null) {
2900
+ PRIVATE_KEY_END.lastIndex = begin.index + begin[0].length;
2901
+ const end = PRIVATE_KEY_END.exec(content);
2902
+ if (end === null) {
2903
+ spans.push([begin.index, content.length]);
2904
+ break;
2905
+ }
2906
+ const stop = end.index + end[0].length;
2907
+ spans.push([begin.index, stop]);
2908
+ PRIVATE_KEY_BEGIN.lastIndex = stop;
2909
+ }
2910
+ return spans;
2911
+ }
2891
2912
  function detectSecrets(content) {
2892
2913
  const matches = [];
2914
+ const push = (rule, start, end) => {
2915
+ matches.push({
2916
+ category: "secret",
2917
+ policyId: rule.policyId,
2918
+ severity: rule.severity,
2919
+ confidence: rule.confidence,
2920
+ start,
2921
+ end,
2922
+ value: content.slice(start, end),
2923
+ mask: "secret",
2924
+ remediation: rule.remediation
2925
+ });
2926
+ };
2893
2927
  for (const rule of RULES2) {
2928
+ if (rule.scan) {
2929
+ for (const [start, end] of rule.scan(content)) {
2930
+ push(rule, start, end);
2931
+ }
2932
+ continue;
2933
+ }
2894
2934
  rule.regex.lastIndex = 0;
2895
2935
  let m;
2896
2936
  while ((m = rule.regex.exec(content)) !== null) {
2897
- const group = rule.valueGroup ?? 0;
2898
- const value = m[group];
2899
- if (value === undefined || value.length === 0) {
2937
+ const group = valueGroupOf(rule, m);
2938
+ const value = group === undefined ? undefined : m[group];
2939
+ if (group === undefined || value === undefined || value.length === 0) {
2900
2940
  continue;
2901
2941
  }
2902
- const start = group === 0 ? m.index : m.index + m[0].indexOf(value);
2903
- matches.push({
2904
- category: "secret",
2905
- policyId: rule.policyId,
2906
- severity: rule.severity,
2907
- confidence: rule.confidence,
2908
- start,
2909
- end: start + value.length,
2910
- value,
2911
- mask: "secret",
2912
- remediation: rule.remediation
2913
- });
2942
+ const start = group === 0 ? m.index : m.indices?.[group]?.[0] ?? m.index + m[0].indexOf(value);
2943
+ push(rule, start, start + value.length);
2914
2944
  if (m.index === rule.regex.lastIndex) {
2915
2945
  rule.regex.lastIndex += 1;
2916
2946
  }
@@ -2918,8 +2948,22 @@ function detectSecrets(content) {
2918
2948
  }
2919
2949
  return matches;
2920
2950
  }
2921
- var RULES2;
2951
+ function valueGroupOf(rule, m) {
2952
+ if (rule.valueGroup === undefined) {
2953
+ return 0;
2954
+ }
2955
+ for (let group = rule.valueGroup;group < m.length; group += 1) {
2956
+ if (m[group] !== undefined) {
2957
+ return group;
2958
+ }
2959
+ }
2960
+ return;
2961
+ }
2962
+ var PRIVATE_KEY_BEGIN, PRIVATE_KEY_END, ASSIGNED_VALUE, RULES2;
2922
2963
  var init_secrets = __esm(() => {
2964
+ PRIVATE_KEY_BEGIN = /-----BEGIN (?:[A-Z0-9]{1,16} ){0,3}PRIVATE KEY(?: BLOCK)?-----/g;
2965
+ PRIVATE_KEY_END = /-----END (?:[A-Z0-9]{1,16} ){0,3}PRIVATE KEY(?: BLOCK)?-----/g;
2966
+ ASSIGNED_VALUE = String.raw`(?:"([^"\n]{6,})"|'([^'\n]{6,})'|["']?([^\s"'#]{6,}))`;
2923
2967
  RULES2 = [
2924
2968
  {
2925
2969
  policyId: "secrets.aws-access-key",
@@ -2935,6 +2979,13 @@ var init_secrets = __esm(() => {
2935
2979
  confidence: 0.97,
2936
2980
  remediation: "Revoke the GitHub token and rotate it."
2937
2981
  },
2982
+ {
2983
+ policyId: "secrets.github-token",
2984
+ regex: /\bgithub_pat_[A-Za-z0-9_]{50,255}\b/g,
2985
+ severity: "critical",
2986
+ confidence: 0.97,
2987
+ remediation: "Revoke the GitHub token and rotate it."
2988
+ },
2938
2989
  {
2939
2990
  policyId: "secrets.google-api-key",
2940
2991
  regex: /\bAIza[0-9A-Za-z_-]{35}\b/g,
@@ -2965,14 +3016,15 @@ var init_secrets = __esm(() => {
2965
3016
  },
2966
3017
  {
2967
3018
  policyId: "secrets.private-key-block",
2968
- regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
3019
+ regex: PRIVATE_KEY_BEGIN,
3020
+ scan: scanPrivateKeyBlocks,
2969
3021
  severity: "critical",
2970
3022
  confidence: 0.99,
2971
3023
  remediation: "Remove the private key and rotate the corresponding key pair."
2972
3024
  },
2973
3025
  {
2974
3026
  policyId: "secrets.url-credentials",
2975
- regex: /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:([^/\s:@]+)@[^\s/]+/gi,
3027
+ regex: /[a-z][a-z0-9+.-]{0,31}:\/\/[^/\s:@]+:([^/\s:@]+)@[^\s/]+/gi,
2976
3028
  severity: "high",
2977
3029
  confidence: 0.92,
2978
3030
  remediation: "Remove inline credentials from URLs; use a secrets store."
@@ -3002,7 +3054,7 @@ var init_secrets = __esm(() => {
3002
3054
  },
3003
3055
  {
3004
3056
  policyId: "secrets.credential-file-assignment",
3005
- regex: /\b((?:aws_)?secret_access_key|aws_session_token|aws_secret_key|client_secret)\s*[:=]\s*["']?([^\s"'#]{6,})/gi,
3057
+ regex: new RegExp(String.raw`\b((?:aws_)?secret_access_key|aws_session_token|aws_secret_key|client_secret)\s*[:=]\s*` + ASSIGNED_VALUE, "gid"),
3006
3058
  severity: "critical",
3007
3059
  confidence: 0.95,
3008
3060
  remediation: "Rotate the credential; it was read from a credentials file into agent-visible output.",
@@ -3010,7 +3062,7 @@ var init_secrets = __esm(() => {
3010
3062
  },
3011
3063
  {
3012
3064
  policyId: "secrets.env-assignment",
3013
- regex: /\b([A-Z0-9_]*(?:DATABASE_URL|JWT_SECRET|SECRET(?:_KEY)?|API_?KEY|ACCESS_?KEY|ACCESS_?TOKEN|AUTH_?TOKEN|PASSWORD|PRIVATE_?KEY|TOKEN))["']?\s*[:=]\s*["']?([^\s"'#]{6,})/g,
3065
+ regex: new RegExp(String.raw`\b([A-Z0-9_]*(?:DATABASE_URL|JWT_SECRET|SECRET(?:_KEY)?|API_?KEY|ACCESS_?KEY|ACCESS_?TOKEN|AUTH_?TOKEN|PASSWORD|PRIVATE_?KEY|TOKEN))["']?\s*[:=]\s*` + ASSIGNED_VALUE, "gd"),
3014
3066
  severity: "high",
3015
3067
  confidence: 0.85,
3016
3068
  remediation: "Move the secret to an untracked env file or secrets manager.",
@@ -3021,7 +3073,16 @@ var init_secrets = __esm(() => {
3021
3073
 
3022
3074
  // src/security/detect/egress.ts
3023
3075
  function hostOf(url) {
3024
- const m = /^https?:\/\/([^/?#]+)/i.exec(url);
3076
+ try {
3077
+ const host = new URL(url).hostname;
3078
+ if (host) {
3079
+ return host.toLowerCase();
3080
+ }
3081
+ } catch {}
3082
+ return literalHostOf(url);
3083
+ }
3084
+ function literalHostOf(url) {
3085
+ const m = /^https?:[/\\]{2}([^/?#\\]+)/i.exec(url);
3025
3086
  if (!m || m[1] === undefined) {
3026
3087
  return null;
3027
3088
  }
@@ -3030,13 +3091,19 @@ function hostOf(url) {
3030
3091
  if (at >= 0) {
3031
3092
  authority = authority.slice(at + 1);
3032
3093
  }
3033
- if (!authority.startsWith("[")) {
3034
- const colon = authority.indexOf(":");
3035
- if (colon >= 0) {
3036
- authority = authority.slice(0, colon);
3037
- }
3094
+ if (authority.startsWith("[")) {
3095
+ const close = authority.indexOf("]");
3096
+ return close > 0 ? authority.slice(0, close + 1).toLowerCase() : null;
3097
+ }
3098
+ const colon = authority.indexOf(":");
3099
+ if (colon >= 0) {
3100
+ authority = authority.slice(0, colon);
3038
3101
  }
3039
- return authority.toLowerCase();
3102
+ return authority.toLowerCase() || null;
3103
+ }
3104
+ function hostCandidates(url) {
3105
+ const hosts = [hostOf(url), literalHostOf(url)].filter((h) => !!h);
3106
+ return [...new Set(hosts)];
3040
3107
  }
3041
3108
  function hostAllowed(host, allowlist) {
3042
3109
  const h = host.toLowerCase();
@@ -3045,46 +3112,130 @@ function hostAllowed(host, allowlist) {
3045
3112
  return h === e || h.endsWith(`.${e}`);
3046
3113
  });
3047
3114
  }
3048
- function isSsrfHost(host) {
3049
- const h = host.toLowerCase().replace(/^\[|\]$/g, "");
3050
- if (h === "metadata.google.internal" || h === "localhost") {
3115
+ function isPrivateV4(a, b) {
3116
+ if (a === 0)
3051
3117
  return true;
3052
- }
3118
+ if (a === 10)
3119
+ return true;
3120
+ if (a === 100 && b >= 64 && b <= 127)
3121
+ return true;
3122
+ if (a === 127)
3123
+ return true;
3124
+ if (a === 169 && b === 254)
3125
+ return true;
3126
+ if (a === 172 && b >= 16 && b <= 31)
3127
+ return true;
3128
+ if (a === 192 && b === 168)
3129
+ return true;
3130
+ return false;
3131
+ }
3132
+ function decimalV4(h) {
3053
3133
  const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
3134
+ if (!v4)
3135
+ return null;
3136
+ const o = v4.slice(1, 5).map((n) => Number(n));
3137
+ if (o.some((n) => n > 255))
3138
+ return null;
3139
+ return [o[0], o[1]];
3140
+ }
3141
+ function whatwgV4(h) {
3142
+ const parts = h.split(".");
3143
+ if (parts.length > 1 && parts[parts.length - 1] === "")
3144
+ parts.pop();
3145
+ if (parts.length < 1 || parts.length > 4)
3146
+ return null;
3147
+ const nums = [];
3148
+ for (const p of parts) {
3149
+ let n;
3150
+ if (/^0x[0-9a-f]*$/i.test(p))
3151
+ n = p.length === 2 ? 0 : parseInt(p.slice(2), 16);
3152
+ else if (/^0[0-7]+$/.test(p))
3153
+ n = parseInt(p.slice(1), 8);
3154
+ else if (/^(0|[1-9]\d*)$/.test(p))
3155
+ n = Number(p);
3156
+ else
3157
+ return null;
3158
+ nums.push(n);
3159
+ }
3160
+ const last = nums.pop();
3161
+ if (nums.some((n) => n > 255) || last >= 256 ** (4 - nums.length))
3162
+ return null;
3163
+ let value = last;
3164
+ nums.forEach((n, i) => {
3165
+ value += n * 256 ** (3 - i);
3166
+ });
3167
+ return [Math.floor(value / 2 ** 24), Math.floor(value / 2 ** 16) % 256];
3168
+ }
3169
+ function parseV6(h) {
3170
+ let text = h;
3171
+ const tail = [];
3172
+ const v4 = /^(.*:)(\d{1,3}(?:\.\d{1,3}){3})$/.exec(text);
3054
3173
  if (v4) {
3055
- const o = v4.slice(1, 5).map((n) => Number(n));
3056
- if (o.some((n) => n > 255)) {
3057
- return false;
3174
+ const o = v4[2].split(".").map(Number);
3175
+ if (o.some((n) => n > 255))
3176
+ return null;
3177
+ tail.push(o[0] << 8 | o[1], o[2] << 8 | o[3]);
3178
+ text = `${v4[1]}0`;
3179
+ }
3180
+ const halves = text.split("::");
3181
+ if (halves.length > 2)
3182
+ return null;
3183
+ const read = (s) => {
3184
+ if (s === "")
3185
+ return [];
3186
+ const out = [];
3187
+ for (const g of s.split(":")) {
3188
+ if (!/^[0-9a-f]{1,4}$/.test(g))
3189
+ return null;
3190
+ out.push(parseInt(g, 16));
3058
3191
  }
3059
- const [a, b] = o;
3060
- if (a === 10)
3061
- return true;
3062
- if (a === 127)
3063
- return true;
3064
- if (a === 169 && b === 254)
3192
+ return out;
3193
+ };
3194
+ const head = read(halves[0]);
3195
+ const rest = halves.length === 2 ? read(halves[1]) : [];
3196
+ if (!head || !rest)
3197
+ return null;
3198
+ if (tail.length > 0)
3199
+ (rest.length > 0 ? rest : head).pop();
3200
+ const given = head.length + rest.length + tail.length;
3201
+ if (halves.length === 1 ? given !== 8 : given > 7)
3202
+ return null;
3203
+ return [...head, ...new Array(8 - given).fill(0), ...rest, ...tail];
3204
+ }
3205
+ function isSsrfHost(host) {
3206
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
3207
+ if (h === "metadata.google.internal" || h === "localhost" || h.endsWith(".localhost")) {
3208
+ return true;
3209
+ }
3210
+ if (h.includes(":")) {
3211
+ const g = parseV6(h.replace(/%.*$/, ""));
3212
+ if (!g)
3213
+ return false;
3214
+ const [g0, , , , , g5, g6, g7] = g;
3215
+ if (g.slice(0, 7).every((x) => x === 0) && g7 <= 1)
3065
3216
  return true;
3066
- if (a === 172 && b >= 16 && b <= 31)
3217
+ if ((g0 & 65024) === 64512)
3067
3218
  return true;
3068
- if (a === 192 && b === 168)
3219
+ if ((g0 & 65472) === 65152)
3069
3220
  return true;
3221
+ if (g.slice(0, 5).every((x) => x === 0) && g5 === 65535) {
3222
+ return isPrivateV4(g6 >> 8, g6 & 255);
3223
+ }
3070
3224
  return false;
3071
3225
  }
3072
- if (h === "::1" || h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80")) {
3073
- return true;
3074
- }
3075
- return false;
3226
+ return [decimalV4(h), whatwgV4(h)].some((v4) => v4 !== null && isPrivateV4(v4[0], v4[1]));
3076
3227
  }
3077
3228
  function detectEgress(content, allowlist = []) {
3078
3229
  const matches = [];
3079
- const ssrfSpans = new Set;
3230
+ const ssrfSpans = [];
3080
3231
  EXTERNAL_URL.lastIndex = 0;
3081
3232
  let m;
3082
3233
  while ((m = EXTERNAL_URL.exec(content)) !== null) {
3083
3234
  const url = m[0];
3084
3235
  const start = m.index;
3085
3236
  const end = start + url.length;
3086
- const host = hostOf(url);
3087
- if (host && isSsrfHost(host)) {
3237
+ const hosts = hostCandidates(url);
3238
+ if (hosts.some(isSsrfHost)) {
3088
3239
  matches.push({
3089
3240
  category: "egress",
3090
3241
  policyId: "egress.ssrf-metadata",
@@ -3095,8 +3246,8 @@ function detectEgress(content, allowlist = []) {
3095
3246
  value: url,
3096
3247
  remediation: "Block requests to private, loopback, link-local, or cloud-metadata hosts (SSRF)."
3097
3248
  });
3098
- ssrfSpans.add(`${start}:${end}`);
3099
- } else if (host && allowlist.length > 0 && !hostAllowed(host, allowlist)) {
3249
+ ssrfSpans.push({ start, end });
3250
+ } else if (hosts.length > 0 && allowlist.length > 0 && hosts.some((h) => !hostAllowed(h, allowlist))) {
3100
3251
  matches.push({
3101
3252
  category: "egress",
3102
3253
  policyId: "egress.non-allowlisted-domain",
@@ -3126,14 +3277,16 @@ function detectEgress(content, allowlist = []) {
3126
3277
  }
3127
3278
  }
3128
3279
  BARE_SSRF_HOST.lastIndex = 0;
3280
+ let span = 0;
3129
3281
  while ((m = BARE_SSRF_HOST.exec(content)) !== null) {
3130
3282
  const value = m[0];
3131
3283
  const start = m.index;
3132
3284
  const end = start + value.length;
3133
- const covered = [...ssrfSpans].some((span) => {
3134
- const [s, e] = span.split(":").map((n) => Number(n));
3135
- return start >= (s ?? 0) && end <= (e ?? 0);
3136
- });
3285
+ while (span < ssrfSpans.length && ssrfSpans[span].end <= start) {
3286
+ span += 1;
3287
+ }
3288
+ const cur = ssrfSpans[span];
3289
+ const covered = cur !== undefined && start >= cur.start && end <= cur.end;
3137
3290
  if (!covered && isSsrfHost(value)) {
3138
3291
  matches.push({
3139
3292
  category: "egress",
@@ -3258,7 +3411,7 @@ function isExfilDestination(url, allowlist) {
3258
3411
  return !(allowlist.length > 0 && hostAllowed(host, allowlist));
3259
3412
  }
3260
3413
  function considerUrl(hit, allowlist, out) {
3261
- if (!isExfilDestination(hit.classify ?? hit.url, allowlist)) {
3414
+ if (!hit.unexamined && !isExfilDestination(hit.classify ?? hit.url, allowlist)) {
3262
3415
  return;
3263
3416
  }
3264
3417
  out.push({
@@ -3649,27 +3802,181 @@ function spanHasNonWhitespace(content, index, budget, start, end) {
3649
3802
  budget.work -= width;
3650
3803
  return stripBlockContainerMarkers(content.slice(start, end)).trim().length > 0;
3651
3804
  }
3805
+ function newInlineDestinationCache() {
3806
+ return { readings: new Map, heads: null, bare: new Map };
3807
+ }
3808
+ function isAsciiPunctuation(code) {
3809
+ return code >= 33 && code <= 47 || code >= 58 && code <= 64 || code >= 91 && code <= 96 || code >= 123 && code <= 126;
3810
+ }
3811
+ function classificationHeadEnd(content, from, cache) {
3812
+ const heads = cache.heads ??= new Int32Array(content.length + 1);
3813
+ let at = from;
3814
+ while (at < content.length) {
3815
+ const known = heads[at];
3816
+ if (known !== 0) {
3817
+ at = known - 1;
3818
+ break;
3819
+ }
3820
+ const code = content.charCodeAt(at);
3821
+ if (code === 41 || isCollapsibleSpace(code, content[at]))
3822
+ break;
3823
+ at += 1;
3824
+ }
3825
+ for (let fill = from;fill < at && heads[fill] === 0; fill += 1)
3826
+ heads[fill] = at + 1;
3827
+ return at;
3828
+ }
3829
+ function bareDestinationEnd(content, from, memo) {
3830
+ const known = memo.get(from);
3831
+ if (known !== undefined)
3832
+ return known;
3833
+ const opens = [];
3834
+ const fail = () => {
3835
+ for (const start of opens)
3836
+ memo.set(start, -1);
3837
+ memo.set(from, -1);
3838
+ return -1;
3839
+ };
3840
+ let at = from;
3841
+ for (;; ) {
3842
+ const code = at < content.length ? content.charCodeAt(at) : 0;
3843
+ if (code <= 32 || code === 127 || isCollapsibleSpace(code, content[at])) {
3844
+ if (opens.length === 0)
3845
+ break;
3846
+ memo.set(opens.pop(), at);
3847
+ return fail();
3848
+ }
3849
+ if (code === 92 && isAsciiPunctuation(content.charCodeAt(at + 1))) {
3850
+ at += 2;
3851
+ continue;
3852
+ }
3853
+ if (code === 40) {
3854
+ const inner = memo.get(at + 1);
3855
+ if (inner === undefined) {
3856
+ if (opens.length >= MAX_DESTINATION_PAREN_DEPTH)
3857
+ return fail();
3858
+ opens.push(at + 1);
3859
+ at += 1;
3860
+ continue;
3861
+ }
3862
+ if (inner === -1 || content.charCodeAt(inner) !== 41)
3863
+ return fail();
3864
+ at = inner + 1;
3865
+ continue;
3866
+ }
3867
+ if (code === 41) {
3868
+ if (opens.length === 0)
3869
+ break;
3870
+ memo.set(opens.pop(), at);
3871
+ }
3872
+ at += 1;
3873
+ }
3874
+ memo.set(from, at);
3875
+ return at;
3876
+ }
3877
+ function skipSpacesAndTabs(content, from) {
3878
+ let at = from;
3879
+ while (at < content.length) {
3880
+ const code = content.charCodeAt(at);
3881
+ if (code !== 32 && code !== 9)
3882
+ break;
3883
+ at += 1;
3884
+ }
3885
+ return at;
3886
+ }
3887
+ function resolvesAsInline(content, from, at, memo) {
3888
+ for (let gap = from;gap < at; gap += 1) {
3889
+ const code = content.charCodeAt(gap);
3890
+ if (code !== 32 && code !== 9)
3891
+ return false;
3892
+ }
3893
+ let cursor = at;
3894
+ if (content.charCodeAt(cursor) === 60) {
3895
+ for (cursor += 1;; cursor += 1) {
3896
+ if (cursor >= content.length)
3897
+ return false;
3898
+ const code = content.charCodeAt(cursor);
3899
+ if (code === 92 && isAsciiPunctuation(content.charCodeAt(cursor + 1))) {
3900
+ cursor += 1;
3901
+ continue;
3902
+ }
3903
+ if (code === 62)
3904
+ break;
3905
+ if (code === 60 || code === 10 || code === 13)
3906
+ return false;
3907
+ }
3908
+ cursor += 1;
3909
+ } else {
3910
+ cursor = bareDestinationEnd(content, cursor, memo);
3911
+ if (cursor === -1)
3912
+ return false;
3913
+ }
3914
+ const destinationEnd = cursor;
3915
+ cursor = skipSpacesAndTabs(content, cursor);
3916
+ if (content[cursor] === ")")
3917
+ return true;
3918
+ if (cursor === destinationEnd)
3919
+ return false;
3920
+ const opener = content[cursor];
3921
+ const closer = opener === "(" ? ")" : opener === '"' || opener === "'" ? opener : null;
3922
+ if (closer === null)
3923
+ return false;
3924
+ for (cursor += 1;; cursor += 1) {
3925
+ if (cursor >= content.length)
3926
+ return false;
3927
+ const code = content.charCodeAt(cursor);
3928
+ if (code === 92 && isAsciiPunctuation(content.charCodeAt(cursor + 1))) {
3929
+ cursor += 1;
3930
+ continue;
3931
+ }
3932
+ if (content[cursor] === closer)
3933
+ break;
3934
+ if (opener === "(" && code === 40)
3935
+ return false;
3936
+ if (LINE_TERMINATORS.has(code))
3937
+ return false;
3938
+ }
3939
+ return content[skipSpacesAndTabs(content, cursor + 1)] === ")";
3940
+ }
3652
3941
  function readInlineDestination(content, index, descriptionEnd, cache) {
3653
- const cached = cache.get(descriptionEnd);
3942
+ const cached = cache.readings.get(descriptionEnd);
3654
3943
  if (cached !== undefined)
3655
3944
  return cached;
3656
3945
  let destination = null;
3657
3946
  if (content[descriptionEnd] === "(" && firstAtOrAfter(index.closeParens, descriptionEnd) !== -1) {
3658
3947
  const at = skipDestinationLeadingWhitespace(content, descriptionEnd + 1);
3659
- INLINE_DESTINATION.lastIndex = at;
3660
- const match = INLINE_DESTINATION.exec(content);
3661
- const url = match ? match[1] ?? match[2] ?? "" : "";
3662
- if (match && url) {
3948
+ const head = readClassifiedHead(content, index, at, cache);
3949
+ if (head) {
3663
3950
  destination = {
3664
- url,
3665
- start: at + match[0].indexOf(url),
3666
- end: INLINE_DESTINATION.lastIndex
3951
+ ...head,
3952
+ resolves: resolvesAsInline(content, descriptionEnd + 1, at, cache.bare)
3667
3953
  };
3668
3954
  }
3669
3955
  }
3670
- cache.set(descriptionEnd, destination);
3956
+ cache.readings.set(descriptionEnd, destination);
3671
3957
  return destination;
3672
3958
  }
3959
+ function readClassifiedHead(content, index, at, cache) {
3960
+ if (content[at] === "<") {
3961
+ let close = at + 1;
3962
+ while (close < content.length && !`<>
3963
+ `.includes(content[close])) {
3964
+ close += 1;
3965
+ }
3966
+ const paren = content[close] === ">" ? firstAtOrAfter(index.closeParens, close + 1) : -1;
3967
+ if (paren !== -1) {
3968
+ const url = content.slice(at + 1, close);
3969
+ return url ? { url, start: at + 1, end: paren + 1 } : null;
3970
+ }
3971
+ }
3972
+ const headEnd = classificationHeadEnd(content, at, cache);
3973
+ if (headEnd === at)
3974
+ return null;
3975
+ const paren = firstAtOrAfter(index.closeParens, headEnd);
3976
+ if (paren === -1)
3977
+ return null;
3978
+ return { url: content.slice(at, headEnd), start: at, end: paren + 1 };
3979
+ }
3673
3980
  function readBracketConstructs(content, index, open, destinations, budget, wantReference) {
3674
3981
  const constructs = [];
3675
3982
  const { ends, blind } = descriptionEnds(index, open);
@@ -3684,7 +3991,8 @@ function readBracketConstructs(content, index, open, destinations, budget, wantR
3684
3991
  start: inline.start,
3685
3992
  descriptionEnd,
3686
3993
  end: inline.end,
3687
- escapeAware
3994
+ escapeAware,
3995
+ resolves: inline.resolves
3688
3996
  });
3689
3997
  continue;
3690
3998
  }
@@ -3792,13 +4100,8 @@ function readStartTag(content, from) {
3792
4100
  }
3793
4101
  return { attributes, end: index };
3794
4102
  }
3795
- function isSensitiveDestination(url, descriptionEnd, cache) {
3796
- const cached = cache.get(descriptionEnd);
3797
- if (cached !== undefined)
3798
- return cached;
3799
- const sensitive = SENSITIVE_URL_VALUE.test(url);
3800
- cache.set(descriptionEnd, sensitive);
3801
- return sensitive;
4103
+ function isSensitiveDestination(url) {
4104
+ return SENSITIVE_URL_VALUE.test(url) || SENSITIVE_URL_VALUE.test(renderableUrl(url));
3802
4105
  }
3803
4106
  function countCommas(value) {
3804
4107
  let total = 0;
@@ -3811,6 +4114,8 @@ function countCommas(value) {
3811
4114
  function detectExfil(content, allowlist = []) {
3812
4115
  const matches = [];
3813
4116
  let m;
4117
+ if (!content.includes("[") && !content.includes("<"))
4118
+ return matches;
3814
4119
  const {
3815
4120
  refs,
3816
4121
  maxLength: maxLabelLength,
@@ -3824,11 +4129,12 @@ function detectExfil(content, allowlist = []) {
3824
4129
  work: LABEL_WORK_FACTOR * content.length,
3825
4130
  exhausted: false
3826
4131
  };
3827
- const destinations = new Map;
4132
+ const destinations = newInlineDestinationCache();
3828
4133
  let destinationStart = -1;
3829
4134
  let destinationEnd = -1;
3830
- const sensitive = new Map;
3831
4135
  const flaggedInlineStarts = new Set;
4136
+ const examinedLinkStarts = new Set;
4137
+ let classifyWork = INLINE_CLASSIFY_WORK_FACTOR * content.length;
3832
4138
  BRACKET_OPEN.lastIndex = 0;
3833
4139
  while ((m = BRACKET_OPEN.exec(content)) !== null) {
3834
4140
  const isImage = m[1] === "!";
@@ -3841,7 +4147,7 @@ function detectExfil(content, allowlist = []) {
3841
4147
  if (!inline || inline.kind !== "inline") {
3842
4148
  continue;
3843
4149
  }
3844
- if (inline.escapeAware) {} else if (isImage) {
4150
+ if (inline.escapeAware || !inline.resolves) {} else if (isImage) {
3845
4151
  BRACKET_OPEN.lastIndex = inline.end;
3846
4152
  } else {
3847
4153
  destinationStart = inline.descriptionEnd;
@@ -3849,15 +4155,26 @@ function detectExfil(content, allowlist = []) {
3849
4155
  }
3850
4156
  if (flaggedInlineStarts.has(inline.start))
3851
4157
  continue;
3852
- if (!isImage && !isSensitiveDestination(inline.url, inline.descriptionEnd, sensitive)) {
3853
- continue;
4158
+ if (!isImage) {
4159
+ if (examinedLinkStarts.has(inline.start))
4160
+ continue;
4161
+ examinedLinkStarts.add(inline.start);
3854
4162
  }
3855
- flaggedInlineStarts.add(inline.start);
3856
- considerUrl({
4163
+ const hit = {
3857
4164
  url: inline.url,
3858
4165
  start: inline.start,
3859
4166
  policyId: isImage ? "egress.markdown-image-exfil" : "egress.markdown-link-sensitive-value"
3860
- }, allowlist, matches);
4167
+ };
4168
+ if (inline.url.length > classifyWork) {
4169
+ flaggedInlineStarts.add(inline.start);
4170
+ considerUrl({ ...hit, unexamined: true }, allowlist, matches);
4171
+ continue;
4172
+ }
4173
+ classifyWork -= inline.url.length;
4174
+ if (!isImage && !isSensitiveDestination(inline.url))
4175
+ continue;
4176
+ flaggedInlineStarts.add(inline.start);
4177
+ considerUrl(hit, allowlist, matches);
3861
4178
  }
3862
4179
  destinationStart = -1;
3863
4180
  destinationEnd = -1;
@@ -3873,7 +4190,7 @@ function detectExfil(content, allowlist = []) {
3873
4190
  const constructs = readBracketConstructs(content, index, open, destinations, budget, isImage);
3874
4191
  const inline = constructs.find((construct) => construct.kind === "inline");
3875
4192
  if (inline && inline.kind === "inline") {
3876
- if (!inline.escapeAware) {
4193
+ if (!inline.escapeAware && inline.resolves) {
3877
4194
  if (isImage)
3878
4195
  BRACKET_OPEN.lastIndex = inline.end;
3879
4196
  else {
@@ -4035,7 +4352,7 @@ function scanHtmlStartTags(source, content, origin, allowlist, out) {
4035
4352
  }
4036
4353
  }
4037
4354
  }
4038
- var EXFIL_CONFIDENCE = 0.85, NAMED_CHARACTER_REFERENCES, CHARACTER_REFERENCE, MAX_DECODE_PASSES = 3, MAX_CODE_POINT = 1114111, URL_STRIPPED_CHARACTERS, C0_OR_SPACE = 32, SYNTHETIC_BASE_PAIRS, FETCH_REMEDIATION = "Strip or allowlist auto-fetched destinations in rendered output \u2014 markdown images, <img>/<image>/<input type=image>/<video poster>/background, media and embedded-content elements, and any <base href> that re-points them; they exfiltrate context on render.", META_REFRESH_REMEDIATION = "Strip or allowlist this <meta http-equiv=refresh>. It is not a subresource fetch: on render it navigates the whole client to the destination, so the entire directive is masked rather than the URL alone.", BASE_REMEDIATION = "Strip or allowlist this <base href>. Unlike an <img src>, masking it re-points EVERY relative URL in the document to the reader's own origin, so a false positive here changes the whole document's resolution, not just one destination.", UNICODE_WHITESPACE, BRACKET_OPEN, INLINE_DESTINATION, REFERENCE_DESTINATION, LABEL_WHITESPACE_RUN, LINE_TERMINATORS, LABEL_WORK_FACTOR = 8, HTML_START_TAG, IMAGE_POLICY = "egress.html-image-exfil", MEDIA_POLICY = "egress.html-media-exfil", EMBEDDED_POLICY = "egress.html-embedded-document-exfil", IMAGE_URL, IMAGE_SRCSET, MEDIA_URL, MEDIA_SRCSET, EMBEDDED_URL, BACKGROUND_ONLY, FETCHING_ATTRIBUTES, META_REFRESH_CONTENT, SENSITIVE_URL_VALUE, SRCSET_DESCRIPTOR;
4355
+ var EXFIL_CONFIDENCE = 0.85, NAMED_CHARACTER_REFERENCES, CHARACTER_REFERENCE, MAX_DECODE_PASSES = 3, MAX_CODE_POINT = 1114111, URL_STRIPPED_CHARACTERS, C0_OR_SPACE = 32, SYNTHETIC_BASE_PAIRS, FETCH_REMEDIATION = "Strip or allowlist auto-fetched destinations in rendered output \u2014 markdown images, <img>/<image>/<input type=image>/<video poster>/background, media and embedded-content elements, and any <base href> that re-points them; they exfiltrate context on render.", META_REFRESH_REMEDIATION = "Strip or allowlist this <meta http-equiv=refresh>. It is not a subresource fetch: on render it navigates the whole client to the destination, so the entire directive is masked rather than the URL alone.", BASE_REMEDIATION = "Strip or allowlist this <base href>. Unlike an <img src>, masking it re-points EVERY relative URL in the document to the reader's own origin, so a false positive here changes the whole document's resolution, not just one destination.", UNICODE_WHITESPACE, BRACKET_OPEN, REFERENCE_DESTINATION, LABEL_WHITESPACE_RUN, LINE_TERMINATORS, LABEL_WORK_FACTOR = 8, MAX_DESTINATION_PAREN_DEPTH = 32, HTML_START_TAG, IMAGE_POLICY = "egress.html-image-exfil", MEDIA_POLICY = "egress.html-media-exfil", EMBEDDED_POLICY = "egress.html-embedded-document-exfil", IMAGE_URL, IMAGE_SRCSET, MEDIA_URL, MEDIA_SRCSET, EMBEDDED_URL, BACKGROUND_ONLY, FETCHING_ATTRIBUTES, META_REFRESH_CONTENT, SENSITIVE_URL_VALUE, INLINE_CLASSIFY_WORK_FACTOR = 8, SRCSET_DESCRIPTOR;
4039
4356
  var init_exfil = __esm(() => {
4040
4357
  init_egress();
4041
4358
  NAMED_CHARACTER_REFERENCES = {
@@ -4085,7 +4402,6 @@ var init_exfil = __esm(() => {
4085
4402
  ];
4086
4403
  UNICODE_WHITESPACE = /\s/;
4087
4404
  BRACKET_OPEN = /(!?)\[/g;
4088
- INLINE_DESTINATION = /(?:<([^<>\n]*)>|([^)\s]+))[^)]*\)/y;
4089
4405
  REFERENCE_DESTINATION = /(?:<([^<>\n]*)>|(\S+))/y;
4090
4406
  LABEL_WHITESPACE_RUN = /\s+/g;
4091
4407
  LINE_TERMINATORS = new Set([10, 13, 8232, 8233]);
@@ -4127,6 +4443,356 @@ var init_exfil = __esm(() => {
4127
4443
  SRCSET_DESCRIPTOR = /\s+/;
4128
4444
  });
4129
4445
 
4446
+ // src/security/detect/entropy.ts
4447
+ function isAdjacentLabel(content, start) {
4448
+ const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
4449
+ if (ADJACENT_LABEL.test(before)) {
4450
+ return true;
4451
+ }
4452
+ if (before.trim().length > 0) {
4453
+ return false;
4454
+ }
4455
+ let lineStart = start - 1;
4456
+ while (lineStart >= 0 && content[lineStart] !== `
4457
+ ` && /\s/.test(content[lineStart])) {
4458
+ lineStart -= 1;
4459
+ }
4460
+ if (lineStart < 0 || content[lineStart] !== `
4461
+ `) {
4462
+ return false;
4463
+ }
4464
+ const prevTail = lastLineOf(content.slice(Math.max(0, lineStart - CONTINUATION_WINDOW), lineStart)).trimEnd();
4465
+ return CONTINUATION_LABEL_LINE_RE.test(prevTail);
4466
+ }
4467
+ function isHexBlob(value) {
4468
+ return HEX_BLOB.test(value) && /[a-f]/i.test(value);
4469
+ }
4470
+ function isAllowShapedValue(value) {
4471
+ return FULL_GIT_SHA_RE.test(value) || UUID_RE.test(value) || INTEGRITY_RE.test(value);
4472
+ }
4473
+ function lastLineOf(before) {
4474
+ const newline = before.lastIndexOf(`
4475
+ `);
4476
+ return newline < 0 ? before : before.slice(newline + 1);
4477
+ }
4478
+ function isPlainCasedWord(letters) {
4479
+ return /^[a-z]+$/.test(letters) || /^[A-Z]+$/.test(letters) || /^[A-Z][a-z]*$/.test(letters);
4480
+ }
4481
+ function isSingleTransitionTag(segment) {
4482
+ const letterThenDigit = /^([A-Za-z]+)([0-9]+)$/.exec(segment);
4483
+ if (letterThenDigit !== null) {
4484
+ const letters = letterThenDigit[1];
4485
+ return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
4486
+ }
4487
+ const digitThenLetter = /^([0-9]+)([A-Za-z]+)$/.exec(segment);
4488
+ if (digitThenLetter !== null) {
4489
+ const letters = digitThenLetter[2];
4490
+ return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
4491
+ }
4492
+ return false;
4493
+ }
4494
+ function isSlugHexTail(segment) {
4495
+ return SLUG_HEX_TAIL_RE.test(segment);
4496
+ }
4497
+ function isWordSlug(value) {
4498
+ const segments = value.split(/[-_]/);
4499
+ if (segments.length < 3) {
4500
+ return false;
4501
+ }
4502
+ let words = 0;
4503
+ for (let i = 0;i < segments.length; i += 1) {
4504
+ const segment = segments[i];
4505
+ const isDigits = /^[0-9]+$/.test(segment);
4506
+ if (isDigits) {
4507
+ continue;
4508
+ }
4509
+ if (isPlainCasedWord(segment)) {
4510
+ if (segment.length >= 3) {
4511
+ words += 1;
4512
+ }
4513
+ continue;
4514
+ }
4515
+ if (isSingleTransitionTag(segment)) {
4516
+ continue;
4517
+ }
4518
+ if (i === segments.length - 1 && isSlugHexTail(segment)) {
4519
+ continue;
4520
+ }
4521
+ return false;
4522
+ }
4523
+ return words >= 1;
4524
+ }
4525
+ function shannonEntropy(value) {
4526
+ const counts = new Map;
4527
+ for (const ch of value) {
4528
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
4529
+ }
4530
+ let entropy = 0;
4531
+ for (const count of counts.values()) {
4532
+ const p = count / value.length;
4533
+ entropy -= p * Math.log2(p);
4534
+ }
4535
+ return entropy;
4536
+ }
4537
+ function bareShapeQualifies(head) {
4538
+ if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
4539
+ return { qualifies: false, entropy: 0 };
4540
+ }
4541
+ if (isWordSlug(head)) {
4542
+ return { qualifies: false, entropy: 0 };
4543
+ }
4544
+ const entropy = shannonEntropy(head);
4545
+ const hexBlob = isHexBlob(head);
4546
+ if (entropy < ENTROPY_FLOOR_BITS && !hexBlob) {
4547
+ return { qualifies: false, entropy };
4548
+ }
4549
+ return { qualifies: true, entropy };
4550
+ }
4551
+ function shapeQualifiesAsSecret(value) {
4552
+ if (isAllowShapedValue(value)) {
4553
+ return { qualifies: false, entropy: 0 };
4554
+ }
4555
+ return bareShapeQualifies(value);
4556
+ }
4557
+ function looksSecretShaped(value) {
4558
+ return shapeQualifiesAsSecret(value).qualifies;
4559
+ }
4560
+ function buildMatch(start, end, value, entropy) {
4561
+ const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - ENTROPY_FLOOR_BITS) * 0.21));
4562
+ return {
4563
+ category: "secret",
4564
+ policyId: "secrets.high-entropy",
4565
+ severity: "medium",
4566
+ confidence: Number(confidence.toFixed(2)),
4567
+ start,
4568
+ end,
4569
+ value,
4570
+ mask: "entropy",
4571
+ remediation: "Verify this high-entropy value is not a live credential."
4572
+ };
4573
+ }
4574
+ function overlaps(a, start, end) {
4575
+ return start < a.end && end > a.start;
4576
+ }
4577
+ function isKnownDocumentHost(authority) {
4578
+ if (authority.includes("@")) {
4579
+ return false;
4580
+ }
4581
+ const host = authority.replace(/:[0-9]*$/, "").toLowerCase();
4582
+ return DOCUMENT_HOSTS.has(host) || NOTION_SITE_HOST_RE.test(host);
4583
+ }
4584
+ function findUrlSpans(content) {
4585
+ const spans = [];
4586
+ URL_SPAN_RE.lastIndex = 0;
4587
+ let m;
4588
+ while ((m = URL_SPAN_RE.exec(content)) !== null) {
4589
+ let raw = m[0];
4590
+ while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
4591
+ raw = raw.slice(0, -1);
4592
+ }
4593
+ if (raw.length > 0) {
4594
+ spans.push({ start: m.index, end: m.index + raw.length, raw });
4595
+ }
4596
+ if (m.index === URL_SPAN_RE.lastIndex) {
4597
+ URL_SPAN_RE.lastIndex += 1;
4598
+ }
4599
+ }
4600
+ return spans;
4601
+ }
4602
+ function urlComponentSpans(raw) {
4603
+ const spans = [];
4604
+ const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
4605
+ if (schemeMatch === null) {
4606
+ return spans;
4607
+ }
4608
+ let i = schemeMatch[0].length;
4609
+ while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
4610
+ i += 1;
4611
+ }
4612
+ const documentHost = isKnownDocumentHost(raw.slice(schemeMatch[0].length, i));
4613
+ let pathEnd = i;
4614
+ while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
4615
+ pathEnd += 1;
4616
+ }
4617
+ let segStart = i;
4618
+ for (let j = i;j <= pathEnd; j += 1) {
4619
+ if (j === pathEnd || raw[j] === "/") {
4620
+ if (j > segStart && !documentHost) {
4621
+ spans.push({ start: segStart, end: j, kind: "path" });
4622
+ }
4623
+ segStart = j + 1;
4624
+ }
4625
+ }
4626
+ let cursor = pathEnd;
4627
+ if (cursor < raw.length && raw[cursor] === "?") {
4628
+ let queryEnd = cursor + 1;
4629
+ while (queryEnd < raw.length && raw[queryEnd] !== "#") {
4630
+ queryEnd += 1;
4631
+ }
4632
+ let pairStart = cursor + 1;
4633
+ for (let j = cursor + 1;j <= queryEnd; j += 1) {
4634
+ if (j === queryEnd || raw[j] === "&") {
4635
+ const pair = raw.slice(pairStart, j);
4636
+ const eq = pair.indexOf("=");
4637
+ if (eq !== -1 && eq + 1 < pair.length) {
4638
+ spans.push({ start: pairStart + eq + 1, end: j, kind: "query" });
4639
+ } else if (eq === -1 && pair.length > 0) {
4640
+ spans.push({ start: pairStart, end: j, kind: "query" });
4641
+ }
4642
+ pairStart = j + 1;
4643
+ }
4644
+ }
4645
+ cursor = queryEnd;
4646
+ }
4647
+ if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
4648
+ spans.push({ start: cursor + 1, end: raw.length, kind: "fragment" });
4649
+ }
4650
+ return spans;
4651
+ }
4652
+ function isQueryWord(word) {
4653
+ return /^(?:[A-Za-z]*|[0-9]+)$/.test(word) || isSingleTransitionTag(word);
4654
+ }
4655
+ function isQueryWordRun(run) {
4656
+ return run.includes("+") && run.split("+").every((piece) => piece.split(/[-_]/).every(isQueryWord));
4657
+ }
4658
+ function secretShapedCandidatesIn(text) {
4659
+ const normalized = text.replace(TOKEN_RUN, (run) => isQueryWordRun(run) ? run.replace(/\+/g, " ") : run);
4660
+ const out = [];
4661
+ TOKEN.lastIndex = 0;
4662
+ let m;
4663
+ while ((m = TOKEN.exec(normalized)) !== null) {
4664
+ const value = m[0];
4665
+ if (value.length < URL_COMPONENT_MIN_LENGTH) {
4666
+ continue;
4667
+ }
4668
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
4669
+ const { qualifies, entropy } = shapeQualifiesAsSecret(head);
4670
+ if (!qualifies) {
4671
+ continue;
4672
+ }
4673
+ const start = m.index;
4674
+ const end = start + value.length;
4675
+ out.push({ start, end, value: text.slice(start, end), entropy });
4676
+ }
4677
+ return out;
4678
+ }
4679
+ function looksSecretShapedIn(text) {
4680
+ return secretShapedCandidatesIn(text).length > 0;
4681
+ }
4682
+ function labelledPieceQualifies(piece) {
4683
+ if (isAllowShapedValue(piece)) {
4684
+ return { entropy: shannonEntropy(piece) };
4685
+ }
4686
+ if (isHexBlob(piece)) {
4687
+ return { entropy: shannonEntropy(piece) };
4688
+ }
4689
+ if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
4690
+ return null;
4691
+ }
4692
+ const withoutSeparators = piece.replace(/[-_.+]/g, "");
4693
+ const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
4694
+ if (entropy >= ENTROPY_FLOOR_BITS) {
4695
+ return { entropy };
4696
+ }
4697
+ return null;
4698
+ }
4699
+ function isPathWordPiece(piece) {
4700
+ return /^[A-Za-z]{3,}$/.test(piece) && isPlainCasedWord(piece) || isWordSlug(piece);
4701
+ }
4702
+ function labelledValueQualifies(head) {
4703
+ if (!head.includes("/")) {
4704
+ return labelledPieceQualifies(head);
4705
+ }
4706
+ let pathLike = false;
4707
+ for (const piece of head.split("/")) {
4708
+ if (piece.length === 0) {
4709
+ continue;
4710
+ }
4711
+ const found = labelledPieceQualifies(piece);
4712
+ if (found !== null) {
4713
+ return found;
4714
+ }
4715
+ pathLike ||= isPathWordPiece(piece);
4716
+ }
4717
+ return pathLike ? null : labelledPieceQualifies(head.replace(/\//g, ""));
4718
+ }
4719
+ function detectEntropy(content) {
4720
+ const matches = [];
4721
+ const urlSpans = findUrlSpans(content);
4722
+ for (const url of urlSpans) {
4723
+ for (const comp of urlComponentSpans(url.raw)) {
4724
+ const compText = url.raw.slice(comp.start, comp.end);
4725
+ for (const cand of secretShapedCandidatesIn(compText)) {
4726
+ const absStart = url.start + comp.start + cand.start;
4727
+ const absEnd = url.start + comp.start + cand.end;
4728
+ matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
4729
+ }
4730
+ }
4731
+ }
4732
+ LABEL_ASSIGNMENT_RE.lastIndex = 0;
4733
+ let am;
4734
+ while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
4735
+ const value = am[1];
4736
+ const end = am.index + am[0].length;
4737
+ const start = end - value.length;
4738
+ if (urlSpans.some((u) => overlaps(u, start, end))) {
4739
+ continue;
4740
+ }
4741
+ const found = labelledValueQualifies(value.split(".")[0]);
4742
+ if (found !== null) {
4743
+ matches.push(buildMatch(start, end, value, found.entropy));
4744
+ }
4745
+ }
4746
+ TOKEN.lastIndex = 0;
4747
+ let m;
4748
+ while ((m = TOKEN.exec(content)) !== null) {
4749
+ const value = m[0];
4750
+ const start = m.index;
4751
+ const end = start + value.length;
4752
+ if (urlSpans.some((u) => overlaps(u, start, end))) {
4753
+ continue;
4754
+ }
4755
+ if (matches.some((existing) => overlaps(existing, start, end))) {
4756
+ continue;
4757
+ }
4758
+ if (!isAdjacentLabel(content, start)) {
4759
+ continue;
4760
+ }
4761
+ const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
4762
+ const found = labelledValueQualifies(head);
4763
+ if (found === null) {
4764
+ continue;
4765
+ }
4766
+ matches.push(buildMatch(start, end, value, found.entropy));
4767
+ }
4768
+ return matches.sort((a, b) => a.start - b.start);
4769
+ }
4770
+ var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", LABEL_CONTINUATION_MAX = 40, ENTROPY_FLOOR_BITS = 3.6, caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, CONTINUATION_WINDOW, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20, DOCUMENT_HOSTS, NOTION_SITE_HOST_RE, TOKEN_RUN;
4771
+ var init_entropy = __esm(() => {
4772
+ ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
4773
+ CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}["']?\\s*[:=]?\\s*\\\\\\s*$`);
4774
+ CONTINUATION_WINDOW = 2 * LABEL_CONTINUATION_MAX;
4775
+ LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}"?\\s*=\\s*"?` + `([A-Za-z0-9+/_=-]{6,}(?:\\.[A-Za-z0-9+/_=-]{6,})*)`, "gi");
4776
+ TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
4777
+ TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
4778
+ HEX_BLOB = /^[0-9a-f]{24,}$/i;
4779
+ FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
4780
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4781
+ INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
4782
+ SLUG_HEX_TAIL_RE = /^[0-9a-f]{10,16}$/i;
4783
+ URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
4784
+ URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
4785
+ DOCUMENT_HOSTS = new Set([
4786
+ "docs.google.com",
4787
+ "sheets.google.com",
4788
+ "drive.google.com",
4789
+ "notion.so",
4790
+ "www.notion.so"
4791
+ ]);
4792
+ NOTION_SITE_HOST_RE = /^[a-z0-9-]+\.notion\.site$/;
4793
+ TOKEN_RUN = /[A-Za-z0-9+/=_-]+/g;
4794
+ });
4795
+
4130
4796
  // src/lib/contained-path.ts
4131
4797
  import { lstat as lstat3, realpath as realpath3 } from "fs/promises";
4132
4798
  import { existsSync as existsSync2, lstatSync, realpathSync } from "fs";
@@ -4157,7 +4823,7 @@ function isInside(root, candidate) {
4157
4823
  function outsideProjectMessage(candidate, rootReal) {
4158
4824
  return `refused: ${candidate} resolves outside the project root. Paths must stay inside ${rootReal}.`;
4159
4825
  }
4160
- async function resolveContainedPath(projectRoot, candidate) {
4826
+ async function resolveContainedPath(projectRoot, candidate, baseDir) {
4161
4827
  try {
4162
4828
  const rootStat = await lstat3(projectRoot);
4163
4829
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
@@ -4167,7 +4833,7 @@ async function resolveContainedPath(projectRoot, candidate) {
4167
4833
  return { ok: false, reason: "not-found", message: `no such file or directory: ${candidate}` };
4168
4834
  }
4169
4835
  const rootReal = await realpath3(projectRoot).catch(() => path10.resolve(projectRoot));
4170
- const absolute = path10.resolve(rootReal, candidate);
4836
+ const absolute = path10.resolve(baseDir ?? rootReal, candidate);
4171
4837
  const real = await realpath3(absolute).catch(() => null);
4172
4838
  const effective = real ?? absolute;
4173
4839
  if (!isInside(rootReal, effective)) {
@@ -4771,330 +5437,6 @@ var init_config = __esm(() => {
4771
5437
  ]);
4772
5438
  });
4773
5439
 
4774
- // src/security/detect/entropy.ts
4775
- function isAdjacentLabel(content, start) {
4776
- const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
4777
- if (ADJACENT_LABEL.test(before)) {
4778
- return true;
4779
- }
4780
- if (before.trim().length > 0) {
4781
- return false;
4782
- }
4783
- const lineStart = content.lastIndexOf(`
4784
- `, start - 1);
4785
- if (lineStart < 0) {
4786
- return false;
4787
- }
4788
- const prevLineStart = content.lastIndexOf(`
4789
- `, lineStart - 1) + 1;
4790
- const prevLine = content.slice(prevLineStart, lineStart);
4791
- return CONTINUATION_LABEL_LINE_RE.test(prevLine);
4792
- }
4793
- function isHexBlob(value) {
4794
- return HEX_BLOB.test(value) && /[a-f]/i.test(value);
4795
- }
4796
- function isAllowShapedValue(value) {
4797
- return FULL_GIT_SHA_RE.test(value) || UUID_RE.test(value) || INTEGRITY_RE.test(value);
4798
- }
4799
- function lastLineOf(before) {
4800
- const newline = before.lastIndexOf(`
4801
- `);
4802
- return newline < 0 ? before : before.slice(newline + 1);
4803
- }
4804
- function isPlainCasedWord(letters) {
4805
- return /^[a-z]+$/.test(letters) || /^[A-Z]+$/.test(letters) || /^[A-Z][a-z]*$/.test(letters);
4806
- }
4807
- function isSingleTransitionTag(segment) {
4808
- const letterThenDigit = /^([A-Za-z]+)([0-9]+)$/.exec(segment);
4809
- if (letterThenDigit !== null) {
4810
- const letters = letterThenDigit[1];
4811
- return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
4812
- }
4813
- const digitThenLetter = /^([0-9]+)([A-Za-z]+)$/.exec(segment);
4814
- if (digitThenLetter !== null) {
4815
- const letters = digitThenLetter[2];
4816
- return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
4817
- }
4818
- return false;
4819
- }
4820
- function isSlugHexTail(segment) {
4821
- return SLUG_HEX_TAIL_RE.test(segment);
4822
- }
4823
- function isWordSlug(value) {
4824
- const segments = value.split(/[-_]/);
4825
- if (segments.length < 3) {
4826
- return false;
4827
- }
4828
- let words = 0;
4829
- for (let i = 0;i < segments.length; i += 1) {
4830
- const segment = segments[i];
4831
- const isDigits = /^[0-9]+$/.test(segment);
4832
- if (isDigits) {
4833
- continue;
4834
- }
4835
- if (isPlainCasedWord(segment)) {
4836
- if (segment.length >= 3) {
4837
- words += 1;
4838
- }
4839
- continue;
4840
- }
4841
- if (isSingleTransitionTag(segment)) {
4842
- continue;
4843
- }
4844
- if (i === segments.length - 1 && isSlugHexTail(segment)) {
4845
- continue;
4846
- }
4847
- return false;
4848
- }
4849
- return words >= 1;
4850
- }
4851
- function shannonEntropy(value) {
4852
- const counts = new Map;
4853
- for (const ch of value) {
4854
- counts.set(ch, (counts.get(ch) ?? 0) + 1);
4855
- }
4856
- let entropy = 0;
4857
- for (const count of counts.values()) {
4858
- const p = count / value.length;
4859
- entropy -= p * Math.log2(p);
4860
- }
4861
- return entropy;
4862
- }
4863
- function bareShapeQualifies(head) {
4864
- if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
4865
- return { qualifies: false, entropy: 0 };
4866
- }
4867
- if (isWordSlug(head)) {
4868
- return { qualifies: false, entropy: 0 };
4869
- }
4870
- const entropy = shannonEntropy(head);
4871
- const hexBlob = isHexBlob(head);
4872
- if (entropy < 3.6 && !hexBlob) {
4873
- return { qualifies: false, entropy };
4874
- }
4875
- return { qualifies: true, entropy };
4876
- }
4877
- function shapeQualifiesAsSecret(value) {
4878
- if (isAllowShapedValue(value)) {
4879
- return { qualifies: false, entropy: 0 };
4880
- }
4881
- return bareShapeQualifies(value);
4882
- }
4883
- function looksSecretShaped(value) {
4884
- return shapeQualifiesAsSecret(value).qualifies;
4885
- }
4886
- function buildMatch(start, end, value, entropy) {
4887
- const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - 3.6) * 0.21));
4888
- return {
4889
- category: "secret",
4890
- policyId: "secrets.high-entropy",
4891
- severity: "medium",
4892
- confidence: Number(confidence.toFixed(2)),
4893
- start,
4894
- end,
4895
- value,
4896
- mask: "entropy",
4897
- remediation: "Verify this high-entropy value is not a live credential."
4898
- };
4899
- }
4900
- function findUrlSpans(content) {
4901
- const spans = [];
4902
- URL_SPAN_RE.lastIndex = 0;
4903
- let m;
4904
- while ((m = URL_SPAN_RE.exec(content)) !== null) {
4905
- let raw = m[0];
4906
- while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
4907
- raw = raw.slice(0, -1);
4908
- }
4909
- if (raw.length > 0) {
4910
- spans.push({ start: m.index, end: m.index + raw.length, raw });
4911
- }
4912
- if (m.index === URL_SPAN_RE.lastIndex) {
4913
- URL_SPAN_RE.lastIndex += 1;
4914
- }
4915
- }
4916
- return spans;
4917
- }
4918
- function urlComponentSpans(raw) {
4919
- const spans = [];
4920
- const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
4921
- if (schemeMatch === null) {
4922
- return spans;
4923
- }
4924
- let i = schemeMatch[0].length;
4925
- while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
4926
- i += 1;
4927
- }
4928
- let pathEnd = i;
4929
- while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
4930
- pathEnd += 1;
4931
- }
4932
- let segStart = i;
4933
- for (let j = i;j <= pathEnd; j += 1) {
4934
- if (j === pathEnd || raw[j] === "/") {
4935
- if (j > segStart) {
4936
- spans.push({ start: segStart, end: j });
4937
- }
4938
- segStart = j + 1;
4939
- }
4940
- }
4941
- let cursor = pathEnd;
4942
- if (cursor < raw.length && raw[cursor] === "?") {
4943
- let queryEnd = cursor + 1;
4944
- while (queryEnd < raw.length && raw[queryEnd] !== "#") {
4945
- queryEnd += 1;
4946
- }
4947
- let pairStart = cursor + 1;
4948
- for (let j = cursor + 1;j <= queryEnd; j += 1) {
4949
- if (j === queryEnd || raw[j] === "&") {
4950
- const pair = raw.slice(pairStart, j);
4951
- const eq = pair.indexOf("=");
4952
- if (eq !== -1 && eq + 1 < pair.length) {
4953
- spans.push({ start: pairStart + eq + 1, end: j });
4954
- } else if (eq === -1 && pair.length > 0) {
4955
- spans.push({ start: pairStart, end: j });
4956
- }
4957
- pairStart = j + 1;
4958
- }
4959
- }
4960
- cursor = queryEnd;
4961
- }
4962
- if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
4963
- spans.push({ start: cursor + 1, end: raw.length });
4964
- }
4965
- return spans;
4966
- }
4967
- function secretShapedCandidatesIn(text) {
4968
- const normalized = text.replace(/\+/g, " ");
4969
- const out = [];
4970
- TOKEN.lastIndex = 0;
4971
- let m;
4972
- while ((m = TOKEN.exec(normalized)) !== null) {
4973
- const value = m[0];
4974
- if (value.length < URL_COMPONENT_MIN_LENGTH) {
4975
- continue;
4976
- }
4977
- const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
4978
- const { qualifies, entropy } = shapeQualifiesAsSecret(head);
4979
- if (!qualifies) {
4980
- continue;
4981
- }
4982
- const start = m.index;
4983
- const end = start + value.length;
4984
- out.push({ start, end, value: text.slice(start, end), entropy });
4985
- }
4986
- return out;
4987
- }
4988
- function looksSecretShapedIn(text) {
4989
- return secretShapedCandidatesIn(text).length > 0;
4990
- }
4991
- function labelledPieceQualifies(piece) {
4992
- if (isAllowShapedValue(piece)) {
4993
- return { entropy: shannonEntropy(piece) };
4994
- }
4995
- if (isHexBlob(piece)) {
4996
- return { entropy: shannonEntropy(piece) };
4997
- }
4998
- if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
4999
- return null;
5000
- }
5001
- const withoutSeparators = piece.replace(/[-_.+]/g, "");
5002
- const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
5003
- if (entropy >= 3.6) {
5004
- return { entropy };
5005
- }
5006
- return null;
5007
- }
5008
- function firstQualifyingSlashPiece(head) {
5009
- let offset = 0;
5010
- for (const piece of head.split("/")) {
5011
- if (piece.length > 0) {
5012
- const found = labelledPieceQualifies(piece);
5013
- if (found !== null) {
5014
- return { offset, length: piece.length, entropy: found.entropy };
5015
- }
5016
- }
5017
- offset += piece.length + 1;
5018
- }
5019
- return null;
5020
- }
5021
- function detectEntropy(content) {
5022
- const matches = [];
5023
- const urlSpans = findUrlSpans(content);
5024
- for (const url of urlSpans) {
5025
- for (const comp of urlComponentSpans(url.raw)) {
5026
- const compText = url.raw.slice(comp.start, comp.end);
5027
- for (const cand of secretShapedCandidatesIn(compText)) {
5028
- const absStart = url.start + comp.start + cand.start;
5029
- const absEnd = url.start + comp.start + cand.end;
5030
- matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
5031
- }
5032
- }
5033
- }
5034
- LABEL_ASSIGNMENT_RE.lastIndex = 0;
5035
- let am;
5036
- while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
5037
- const value = am[1];
5038
- const end = am.index + am[0].length;
5039
- const start = end - value.length;
5040
- if (urlSpans.some((u) => start < u.end && end > u.start)) {
5041
- continue;
5042
- }
5043
- const found = labelledPieceQualifies(value);
5044
- if (found !== null) {
5045
- matches.push(buildMatch(start, end, value, found.entropy));
5046
- }
5047
- }
5048
- TOKEN.lastIndex = 0;
5049
- let m;
5050
- while ((m = TOKEN.exec(content)) !== null) {
5051
- const value = m[0];
5052
- const start = m.index;
5053
- const end = start + value.length;
5054
- if (urlSpans.some((u) => start < u.end && end > u.start)) {
5055
- continue;
5056
- }
5057
- if (matches.some((existing) => start < existing.end && end > existing.start)) {
5058
- continue;
5059
- }
5060
- if (!isAdjacentLabel(content, start)) {
5061
- continue;
5062
- }
5063
- const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
5064
- if (head.includes("/")) {
5065
- const piece = firstQualifyingSlashPiece(head);
5066
- if (piece === null) {
5067
- continue;
5068
- }
5069
- const pieceStart = start + piece.offset;
5070
- const pieceEnd = pieceStart + piece.length;
5071
- matches.push(buildMatch(pieceStart, pieceEnd, content.slice(pieceStart, pieceEnd), piece.entropy));
5072
- continue;
5073
- }
5074
- const found = labelledPieceQualifies(head);
5075
- if (found === null) {
5076
- continue;
5077
- }
5078
- matches.push(buildMatch(start, end, value, found.entropy));
5079
- }
5080
- return matches.sort((a, b) => a.start - b.start);
5081
- }
5082
- var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
5083
- var init_entropy = __esm(() => {
5084
- ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
5085
- CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*\\\\\\s*$`);
5086
- LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
5087
- TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
5088
- TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
5089
- HEX_BLOB = /^[0-9a-f]{24,}$/i;
5090
- FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
5091
- UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5092
- INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
5093
- SLUG_HEX_TAIL_RE = /^[0-9a-f]{10,16}$/i;
5094
- URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
5095
- URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
5096
- });
5097
-
5098
5440
  // src/security/entropy-gate.ts
5099
5441
  function ensureLoading() {
5100
5442
  if (loadStarted) {
@@ -5274,12 +5616,12 @@ function isNumericContextKey(key) {
5274
5616
  function isNumericText(value) {
5275
5617
  return /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value);
5276
5618
  }
5277
- function redactString(value, key, exemptExfil) {
5619
+ function floorMatches(value, key, exemptExfil) {
5278
5620
  const secretMatches = detectSecrets(value);
5279
5621
  const piiMatches = detectPii(value);
5280
5622
  const allExfilMatches = detectExfil(value);
5281
5623
  const exfilMatches = exemptExfil ? allExfilMatches.filter((match) => !exemptExfil(match)) : allExfilMatches;
5282
- const exemptedCount = allExfilMatches.length - exfilMatches.length;
5624
+ const entropyMatches = isEntropyBackendEnabled() ? detectEntropy(value) : [];
5283
5625
  const contextualMatches = isSensitiveFieldKey(key) && value.length > 0 && secretMatches.length === 0 ? [
5284
5626
  {
5285
5627
  category: "secret",
@@ -5293,21 +5635,23 @@ function redactString(value, key, exemptExfil) {
5293
5635
  remediation: "Keep credential fields out of transport output."
5294
5636
  }
5295
5637
  ] : [];
5296
- const matches = isNumericContextKey(key) && isNumericText(value) ? [
5297
- ...secretMatches,
5298
- ...contextualMatches,
5299
- ...piiMatches.filter((match) => match.policyId !== "pii.phone"),
5300
- ...exfilMatches
5301
- ] : [...secretMatches, ...contextualMatches, ...piiMatches, ...exfilMatches];
5638
+ const pii = isNumericContextKey(key) && isNumericText(value) ? piiMatches.filter((match) => match.policyId !== "pii.phone") : piiMatches;
5639
+ return [...secretMatches, ...contextualMatches, ...pii, ...exfilMatches, ...entropyMatches];
5640
+ }
5641
+ function redactString(value, key, exemptExfil) {
5642
+ const matches = floorMatches(value, key, exemptExfil);
5302
5643
  if (matches.length === 0) {
5303
5644
  return { value, reasons: [] };
5304
5645
  }
5305
- const redacted = matches.length === secretMatches.length + piiMatches.length + exfilMatches.length && contextualMatches.length === 0 && exemptedCount === 0 ? redactSensitiveText(value) : applyRedaction(value, matches);
5306
5646
  return {
5307
- value: redacted,
5647
+ value: applyRedaction(value, matches),
5308
5648
  reasons: matches.map((match) => match.policyId)
5309
5649
  };
5310
5650
  }
5651
+ function isSensitiveNumber(value, exemptExfil) {
5652
+ const text = String(value);
5653
+ return floorMatches(text, undefined, exemptExfil).some((match) => match.policyId !== "pii.phone" && (match.policyId !== "pii.credit-card" || CARD_LEADING_DIGIT.test(match.value)));
5654
+ }
5311
5655
  function isSensitivePropertyName(name) {
5312
5656
  if (name.length === 0) {
5313
5657
  return false;
@@ -5335,6 +5679,9 @@ function sanitizeJsonValue(value, key, active, exemptExfil) {
5335
5679
  reason: isSensitiveFieldKey(key) ? "sensitive-numeric-field" : "non-json-value"
5336
5680
  };
5337
5681
  }
5682
+ if (isSensitiveNumber(value, exemptExfil)) {
5683
+ return { ok: false, reason: "sensitive-numeric-value" };
5684
+ }
5338
5685
  return { ok: true, value, reasons: [] };
5339
5686
  }
5340
5687
  if (typeof value !== "object") {
@@ -5705,6 +6052,9 @@ function validateSerializedContentForTransport(content, exemptExfil) {
5705
6052
  } catch {
5706
6053
  return validateOutputForTransport({ value: content, format: "text", ...exemption });
5707
6054
  }
6055
+ if (typeof value === "number") {
6056
+ return validateOutputForTransport({ value: content, format: "text", ...exemption });
6057
+ }
5708
6058
  const result = validateOutputForTransport({ value, format: "json", ...exemption });
5709
6059
  if (!result.ok || result.redaction.state === "redacted") {
5710
6060
  return result;
@@ -5714,11 +6064,13 @@ function validateSerializedContentForTransport(content, exemptExfil) {
5714
6064
  redaction: { state: "redacted", reasons: [SERIALIZED_NORMALIZED_REASON] }
5715
6065
  };
5716
6066
  }
5717
- var FORMAT_UNSAFE_OUTPUT_TEXT = "Output withheld: format-unsafe", SUPPORTED_SCHEMA_KEYS, JSON_TYPES, EXTERNAL_SCHEMA_REFS, SENSITIVE_NUMERIC_KEYS, REFERENCE_SAFE_SIBLINGS, SERIALIZED_NORMALIZED_REASON = "serialized-content-normalized", JSON_WHITESPACE, JSON_KEYWORDS;
6067
+ var FORMAT_UNSAFE_OUTPUT_TEXT = "Output withheld: format-unsafe", SUPPORTED_SCHEMA_KEYS, JSON_TYPES, EXTERNAL_SCHEMA_REFS, SENSITIVE_NUMERIC_KEYS, REFERENCE_SAFE_SIBLINGS, CARD_LEADING_DIGIT, SERIALIZED_NORMALIZED_REASON = "serialized-content-normalized", JSON_WHITESPACE, JSON_KEYWORDS;
5718
6068
  var init_output_validation = __esm(() => {
5719
6069
  init_pii();
5720
6070
  init_secrets();
5721
6071
  init_exfil();
6072
+ init_entropy();
6073
+ init_entropy_gate();
5722
6074
  init_redact();
5723
6075
  init_schemas();
5724
6076
  SUPPORTED_SCHEMA_KEYS = new Set([
@@ -5769,6 +6121,7 @@ var init_output_validation = __esm(() => {
5769
6121
  "token"
5770
6122
  ]);
5771
6123
  REFERENCE_SAFE_SIBLINGS = new Set(["$ref", "$schema", "$id", "title", "$defs"]);
6124
+ CARD_LEADING_DIGIT = /^[2-6]/;
5772
6125
  JSON_WHITESPACE = new Set([" ", "\t", `
5773
6126
  `, "\r"]);
5774
6127
  JSON_KEYWORDS = [
@@ -5980,24 +6333,42 @@ var init_seam = __esm(() => {
5980
6333
  });
5981
6334
 
5982
6335
  // src/security/detect/injection.ts
5983
- function foldForMatching(content) {
6336
+ function foldForMatching(content, map = CONFUSABLES) {
5984
6337
  let folded = "";
5985
6338
  for (let i = 0;i < content.length; i += 1) {
5986
6339
  const ch = content[i];
5987
- folded += CONFUSABLES[ch] ?? ch;
6340
+ folded += map[ch] ?? ch;
5988
6341
  }
5989
6342
  const nfkc = folded.normalize("NFKC");
5990
6343
  return nfkc.length === folded.length ? nfkc : folded;
5991
6344
  }
5992
6345
  function detectInjection(content) {
5993
6346
  const matches = [];
5994
- const folded = foldForMatching(content);
6347
+ const seen = new Set;
6348
+ const views = [foldForMatching(content)];
6349
+ if (HAS_ALT_CONFUSABLE.test(content)) {
6350
+ views.push(foldForMatching(content, ALT_CONFUSABLES));
6351
+ }
6352
+ for (const folded of views) {
6353
+ scanView(content, folded, matches, seen);
6354
+ }
6355
+ return matches;
6356
+ }
6357
+ function scanView(content, folded, matches, seen) {
5995
6358
  for (const pattern of PATTERNS) {
5996
6359
  pattern.regex.lastIndex = 0;
5997
6360
  let m;
5998
6361
  while ((m = pattern.regex.exec(folded)) !== null) {
5999
6362
  const start = Math.min(m.index, content.length);
6000
6363
  const end = Math.min(m.index + m[0].length, content.length);
6364
+ const key = `${pattern.policyId}:${start}:${end}`;
6365
+ if (seen.has(key)) {
6366
+ if (m.index === pattern.regex.lastIndex) {
6367
+ pattern.regex.lastIndex += 1;
6368
+ }
6369
+ continue;
6370
+ }
6371
+ seen.add(key);
6001
6372
  matches.push({
6002
6373
  category: "prompt-injection",
6003
6374
  policyId: pattern.policyId,
@@ -6013,9 +6384,8 @@ function detectInjection(content) {
6013
6384
  }
6014
6385
  }
6015
6386
  }
6016
- return matches;
6017
6387
  }
6018
- var CONFUSABLES, PATTERNS;
6388
+ var CONFUSABLES, ALT_CONFUSABLES, HAS_ALT_CONFUSABLE, PATTERNS;
6019
6389
  var init_injection = __esm(() => {
6020
6390
  CONFUSABLES = {
6021
6391
  "\u0430": "a",
@@ -6034,8 +6404,8 @@ var init_injection = __esm(() => {
6034
6404
  "\u0423": "Y",
6035
6405
  "\u0455": "s",
6036
6406
  "\u0405": "S",
6037
- "\u0456": "o",
6038
- "\u0406": "O",
6407
+ "\u0456": "i",
6408
+ "\u0406": "I",
6039
6409
  "\u0458": "j",
6040
6410
  "\u0408": "J",
6041
6411
  "\u03B1": "a",
@@ -6059,6 +6429,8 @@ var init_injection = __esm(() => {
6059
6429
  "\u03C7": "x",
6060
6430
  "\u03A7": "X"
6061
6431
  };
6432
+ ALT_CONFUSABLES = { ...CONFUSABLES, "\u0456": "o", "\u0406": "O" };
6433
+ HAS_ALT_CONFUSABLE = /[\u0456\u0406]/;
6062
6434
  PATTERNS = [
6063
6435
  {
6064
6436
  policyId: "prompt-injection.ignore-instructions",
@@ -6289,16 +6661,48 @@ async function mergeAdapter(cwd, content, spec, into) {
6289
6661
  } catch {}
6290
6662
  }
6291
6663
  function dedupeOverlaps(matches) {
6664
+ const trees = new Map;
6665
+ for (const match of matches) {
6666
+ const tree = trees.get(match.category) ?? { starts: [], maxEnd: [] };
6667
+ tree.starts.push(match.start);
6668
+ trees.set(match.category, tree);
6669
+ }
6670
+ for (const tree of trees.values()) {
6671
+ tree.starts = [...new Set(tree.starts)].sort((a, b) => a - b);
6672
+ tree.maxEnd = new Array(tree.starts.length + 1).fill(-Infinity);
6673
+ }
6292
6674
  const kept = [];
6293
6675
  const sorted = [...matches].sort((a, b) => b.confidence - a.confidence);
6294
6676
  for (const candidate of sorted) {
6295
- const covered = kept.some((existing) => existing.category === candidate.category && candidate.start >= existing.start && candidate.end <= existing.end);
6296
- if (!covered) {
6297
- kept.push(candidate);
6677
+ const tree = trees.get(candidate.category);
6678
+ const position = upperBound(tree.starts, candidate.start);
6679
+ let furthest = -Infinity;
6680
+ for (let i = position;i > 0; i -= i & -i) {
6681
+ furthest = Math.max(furthest, tree.maxEnd[i]);
6682
+ }
6683
+ if (furthest >= candidate.end) {
6684
+ continue;
6685
+ }
6686
+ kept.push(candidate);
6687
+ for (let i = position;i < tree.maxEnd.length; i += i & -i) {
6688
+ tree.maxEnd[i] = Math.max(tree.maxEnd[i], candidate.end);
6298
6689
  }
6299
6690
  }
6300
6691
  return kept.sort((a, b) => a.start - b.start);
6301
6692
  }
6693
+ function upperBound(sorted, value) {
6694
+ let low = 0;
6695
+ let high = sorted.length;
6696
+ while (low < high) {
6697
+ const mid = low + high >> 1;
6698
+ if (sorted[mid] <= value) {
6699
+ low = mid + 1;
6700
+ } else {
6701
+ high = mid;
6702
+ }
6703
+ }
6704
+ return low;
6705
+ }
6302
6706
  var SECURITY_MODEL_RUNTIME = "";
6303
6707
  var init_detect = __esm(() => {
6304
6708
  init_seam();
@@ -7398,16 +7802,16 @@ function isSecretRun(run) {
7398
7802
  function looksLikeSecret(text) {
7399
7803
  if (BOT_TOKEN_SHAPE.test(text))
7400
7804
  return true;
7401
- for (const run of text.match(TOKEN_RUN) ?? []) {
7805
+ for (const run of text.match(TOKEN_RUN2) ?? []) {
7402
7806
  if (isSecretRun(run))
7403
7807
  return true;
7404
7808
  }
7405
7809
  return false;
7406
7810
  }
7407
- var BOT_TOKEN_SHAPE, TOKEN_RUN, RUN_PARTS;
7811
+ var BOT_TOKEN_SHAPE, TOKEN_RUN2, RUN_PARTS;
7408
7812
  var init_secret_shape = __esm(() => {
7409
7813
  BOT_TOKEN_SHAPE = /\d{6,}:[A-Za-z0-9_-]{30,}/;
7410
- TOKEN_RUN = /[A-Za-z0-9_-]{32,}|[A-Za-z0-9+/]{40,}={0,2}/g;
7814
+ TOKEN_RUN2 = /[A-Za-z0-9_-]{32,}|[A-Za-z0-9+/]{40,}={0,2}/g;
7411
7815
  RUN_PARTS = /[A-Z][a-z]+|[a-z]+|[A-Z]+(?![a-z])|\d+|[-_]/g;
7412
7816
  });
7413
7817
 
@@ -7616,11 +8020,119 @@ function parseScalar(raw) {
7616
8020
  return unescapeToml(match[1]);
7617
8021
  return;
7618
8022
  }
8023
+ function splitDepthAware(inner) {
8024
+ const parts = [];
8025
+ let quoted = false;
8026
+ let depth = 0;
8027
+ let start = 0;
8028
+ for (let i = 0;i < inner.length; i++) {
8029
+ const ch = inner[i];
8030
+ if (ch === '"' && inner[i - 1] !== "\\")
8031
+ quoted = !quoted;
8032
+ if (quoted)
8033
+ continue;
8034
+ if (ch === "[" || ch === "{")
8035
+ depth++;
8036
+ if (ch === "]" || ch === "}")
8037
+ depth--;
8038
+ if (ch === "," && depth === 0) {
8039
+ parts.push(inner.slice(start, i));
8040
+ start = i + 1;
8041
+ }
8042
+ }
8043
+ const tail = inner.slice(start).trim();
8044
+ if (tail !== "")
8045
+ parts.push(tail);
8046
+ return parts;
8047
+ }
8048
+ function parseKeyPath(text) {
8049
+ const segments = [];
8050
+ let rest = text.trim();
8051
+ for (;; ) {
8052
+ const m = /^(?:"((?:[^"\\]|\\.)*)"|([A-Za-z0-9_-]+))\s*/.exec(rest);
8053
+ if (m === null)
8054
+ return;
8055
+ segments.push(m[1] !== undefined ? unescapeToml(m[1]) : m[2]);
8056
+ rest = rest.slice(m[0].length);
8057
+ if (rest === "")
8058
+ return segments;
8059
+ if (!rest.startsWith("."))
8060
+ return;
8061
+ rest = rest.slice(1).trimStart();
8062
+ }
8063
+ }
8064
+ function findKeyEquals(line) {
8065
+ let quoted = false;
8066
+ for (let i = 0;i < line.length; i++) {
8067
+ const ch = line[i];
8068
+ if (ch === '"' && line[i - 1] !== "\\")
8069
+ quoted = !quoted;
8070
+ if (ch === "=" && !quoted)
8071
+ return i;
8072
+ }
8073
+ return -1;
8074
+ }
8075
+ function applyField(server, key, valueText) {
8076
+ const value = valueText.trim();
8077
+ if (value.startsWith("[")) {
8078
+ const closeAt = findUnquotedClose(value);
8079
+ if (closeAt === -1 || value.slice(closeAt + 1).trim() !== "")
8080
+ return `value for "${key}" is not a single-line array`;
8081
+ const parsed = parseStringArray(value.slice(1, closeAt));
8082
+ if (parsed === undefined)
8083
+ return `value for "${key}" is not an array of strings this reader supports`;
8084
+ if (key === "command")
8085
+ return '"command" must be a string, not an array';
8086
+ if (key === "args")
8087
+ server.args = parsed;
8088
+ return;
8089
+ }
8090
+ if (value.startsWith("{")) {
8091
+ if (key === "command" || key === "args")
8092
+ return `"${key}" must not be a table`;
8093
+ return value.endsWith("}") ? undefined : `value for "${key}" is an inline table this reader cannot read`;
8094
+ }
8095
+ const scalar = parseScalar(value);
8096
+ if (scalar === undefined)
8097
+ return `value for "${key}" is a TOML form this reader does not support (${value})`;
8098
+ if (key === "command") {
8099
+ if (typeof scalar !== "string")
8100
+ return '"command" must be a string';
8101
+ server.command = scalar;
8102
+ }
8103
+ return;
8104
+ }
8105
+ function applyInlineServer(server, table) {
8106
+ const text = table.trim();
8107
+ if (!text.startsWith("{") || !text.endsWith("}"))
8108
+ return `"${server.name}" is not an inline table this reader understands`;
8109
+ for (const entry of splitDepthAware(text.slice(1, -1))) {
8110
+ const eq = findKeyEquals(entry);
8111
+ const path = eq === -1 ? undefined : parseKeyPath(entry.slice(0, eq));
8112
+ if (path === undefined || path.length !== 1)
8113
+ return `cannot read "${entry.trim()}" as a key/value in "${server.name}"`;
8114
+ const error = applyField(server, path[0], entry.slice(eq + 1));
8115
+ if (error !== undefined)
8116
+ return error;
8117
+ }
8118
+ return;
8119
+ }
7619
8120
  function parseCodexToml(text) {
7620
8121
  const servers = new Map;
7621
8122
  const order = [];
7622
8123
  let current;
8124
+ let atRoot = true;
8125
+ const serverFor = (name, line) => {
8126
+ const existing = servers.get(name);
8127
+ if (existing)
8128
+ return existing;
8129
+ const created = { name, line };
8130
+ servers.set(name, created);
8131
+ order.push(name);
8132
+ return created;
8133
+ };
7623
8134
  let pendingKey;
8135
+ let pendingServer;
7624
8136
  let pendingBuffer = "";
7625
8137
  let pendingStartLine = 0;
7626
8138
  const lines = text.split(`
@@ -7647,13 +8159,14 @@ function parseCodexToml(text) {
7647
8159
  error: `line ${pendingStartLine}: value for "${pendingKey}" is not an array of strings this reader supports`
7648
8160
  };
7649
8161
  }
7650
- if (current && pendingKey === "command") {
8162
+ if (pendingServer && pendingKey === "command") {
7651
8163
  return { ok: false, error: `line ${pendingStartLine}: "command" must be a string, not an array` };
7652
8164
  }
7653
- if (current && pendingKey === "args") {
7654
- current.args = parsedArray;
8165
+ if (pendingServer && pendingKey === "args") {
8166
+ pendingServer.args = parsedArray;
7655
8167
  }
7656
8168
  pendingKey = undefined;
8169
+ pendingServer = undefined;
7657
8170
  pendingBuffer = "";
7658
8171
  continue;
7659
8172
  }
@@ -7661,6 +8174,7 @@ function parseCodexToml(text) {
7661
8174
  if (trimmed === "")
7662
8175
  continue;
7663
8176
  if (trimmed.startsWith("[")) {
8177
+ atRoot = false;
7664
8178
  const header = HEADER.exec(trimmed);
7665
8179
  if (header !== null) {
7666
8180
  const name = header[1] ?? header[2];
@@ -7690,19 +8204,62 @@ function parseCodexToml(text) {
7690
8204
  current = undefined;
7691
8205
  continue;
7692
8206
  }
8207
+ let target = current;
8208
+ let kv;
7693
8209
  if (current === undefined) {
7694
- continue;
7695
- }
7696
- const pair = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.+)$/.exec(trimmed);
7697
- if (pair === null) {
7698
- return { ok: false, error: `line ${lineNo}: cannot read "${trimmed}" as a TOML key/value` };
8210
+ if (!atRoot)
8211
+ continue;
8212
+ const eq = findKeyEquals(trimmed);
8213
+ const path = eq === -1 ? undefined : parseKeyPath(trimmed.slice(0, eq));
8214
+ if (path === undefined) {
8215
+ if (/^"?mcp_servers"?\s*[.=]/.test(trimmed)) {
8216
+ return { ok: false, error: `line ${lineNo}: cannot read "${trimmed}" as a TOML key/value` };
8217
+ }
8218
+ continue;
8219
+ }
8220
+ if (path[0] !== "mcp_servers")
8221
+ continue;
8222
+ const valueText = trimmed.slice(eq + 1).trim();
8223
+ if (path.length === 1) {
8224
+ if (!valueText.startsWith("{") || !valueText.endsWith("}")) {
8225
+ return { ok: false, error: `line ${lineNo}: "mcp_servers" is not an inline table this reader understands` };
8226
+ }
8227
+ for (const entry of splitDepthAware(valueText.slice(1, -1))) {
8228
+ const innerEq = findKeyEquals(entry);
8229
+ const innerPath = innerEq === -1 ? undefined : parseKeyPath(entry.slice(0, innerEq));
8230
+ if (innerPath === undefined || innerPath.length !== 1) {
8231
+ return { ok: false, error: `line ${lineNo}: cannot read "${entry.trim()}" as an mcp_servers entry` };
8232
+ }
8233
+ const error = applyInlineServer(serverFor(innerPath[0], lineNo), entry.slice(innerEq + 1));
8234
+ if (error !== undefined)
8235
+ return { ok: false, error: `line ${lineNo}: ${error}` };
8236
+ }
8237
+ continue;
8238
+ }
8239
+ const server = serverFor(path[1], lineNo);
8240
+ if (path.length === 2) {
8241
+ const error = applyInlineServer(server, valueText);
8242
+ if (error !== undefined)
8243
+ return { ok: false, error: `line ${lineNo}: ${error}` };
8244
+ continue;
8245
+ }
8246
+ if (path.length > 3)
8247
+ continue;
8248
+ target = server;
8249
+ kv = { key: path[2], valueText };
8250
+ } else {
8251
+ const pair = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.+)$/.exec(trimmed);
8252
+ if (pair === null) {
8253
+ return { ok: false, error: `line ${lineNo}: cannot read "${trimmed}" as a TOML key/value` };
8254
+ }
8255
+ kv = { key: pair[1], valueText: pair[2].trim() };
7699
8256
  }
7700
- const key = pair[1];
7701
- const valueText = pair[2].trim();
8257
+ const { key, valueText } = kv;
7702
8258
  if (valueText.startsWith("[")) {
7703
8259
  const closeAt = findUnquotedClose(valueText);
7704
8260
  if (closeAt === -1) {
7705
8261
  pendingKey = key;
8262
+ pendingServer = target;
7706
8263
  pendingBuffer = `${valueText.slice(1)}
7707
8264
  `;
7708
8265
  pendingStartLine = lineNo;
@@ -7716,8 +8273,8 @@ function parseCodexToml(text) {
7716
8273
  if (parsedArray === undefined) {
7717
8274
  return { ok: false, error: `line ${lineNo}: value for "${key}" is not an array of strings this reader supports` };
7718
8275
  }
7719
- if (key === "args")
7720
- current.args = parsedArray;
8276
+ if (key === "args" && target)
8277
+ target.args = parsedArray;
7721
8278
  continue;
7722
8279
  }
7723
8280
  const scalar = parseScalar(valueText);
@@ -7728,7 +8285,8 @@ function parseCodexToml(text) {
7728
8285
  if (typeof scalar !== "string") {
7729
8286
  return { ok: false, error: `line ${lineNo}: "command" must be a string` };
7730
8287
  }
7731
- current.command = scalar;
8288
+ if (target)
8289
+ target.command = scalar;
7732
8290
  }
7733
8291
  }
7734
8292
  if (pendingKey !== undefined) {
@@ -13239,6 +13797,16 @@ function hashToolDefinition(tool) {
13239
13797
  });
13240
13798
  return createHash4("sha256").update(canonical).digest("hex");
13241
13799
  }
13800
+ function hasCommentPayload(text) {
13801
+ const lower = text.toLowerCase();
13802
+ return [
13803
+ ["<!--", "-->"],
13804
+ ["<important>", "</important>"]
13805
+ ].some(([open, close]) => {
13806
+ const at = lower.indexOf(open);
13807
+ return at !== -1 && lower.indexOf(close, at + open.length) !== -1;
13808
+ });
13809
+ }
13242
13810
  function makeMatch(category, policyId, severity, confidence, token, remediation) {
13243
13811
  return {
13244
13812
  category,
@@ -13273,30 +13841,45 @@ function parseTools(manifest) {
13273
13841
  }
13274
13842
  function toolText(tool) {
13275
13843
  const parts = [tool.description ?? ""];
13276
- const collect = (value, depth) => {
13277
- if (depth > 6 || !value || typeof value !== "object") {
13278
- return;
13844
+ const stack = [tool.inputSchema];
13845
+ let visited = 0;
13846
+ while (stack.length > 0) {
13847
+ const value = stack.pop();
13848
+ if (!value || typeof value !== "object") {
13849
+ continue;
13850
+ }
13851
+ visited += 1;
13852
+ if (visited > SCHEMA_NODE_BUDGET) {
13853
+ return { text: parts.join(`
13854
+ `), truncated: true };
13279
13855
  }
13280
13856
  for (const [key, nested] of Object.entries(value)) {
13281
- if ((key === "description" || key === "title") && typeof nested === "string") {
13282
- parts.push(nested);
13283
- } else if (nested && typeof nested === "object") {
13284
- collect(nested, depth + 1);
13857
+ if (SCHEMA_TEXT_KEYS.has(key)) {
13858
+ if (typeof nested === "string") {
13859
+ parts.push(nested);
13860
+ } else if (Array.isArray(nested)) {
13861
+ parts.push(...nested.filter((item) => typeof item === "string"));
13862
+ }
13863
+ }
13864
+ if (nested && typeof nested === "object") {
13865
+ stack.push(nested);
13285
13866
  }
13286
13867
  }
13287
- };
13288
- collect(tool.inputSchema, 0);
13289
- return parts.join(`
13290
- `);
13868
+ }
13869
+ return { text: parts.join(`
13870
+ `), truncated: false };
13291
13871
  }
13292
13872
  function scanMcpManifest(manifest, options = {}) {
13293
13873
  const tools = parseTools(manifest);
13294
13874
  const matches = [];
13295
13875
  const seenNames = new Map;
13296
13876
  for (const tool of tools) {
13297
- const text = toolText(tool);
13877
+ const { text, truncated } = toolText(tool);
13878
+ if (truncated) {
13879
+ matches.push(makeMatch("prompt-injection", "mcp.schema-too-large", "medium", 0.5, `tool:${tool.name}`, `Tool "${tool.name}" input schema is too large to scan whole (over ${SCHEMA_NODE_BUDGET} nodes); review it by hand.`));
13880
+ }
13298
13881
  for (const pattern of POISONING_PATTERNS) {
13299
- if (pattern.regex.test(text)) {
13882
+ if (pattern.test ? pattern.test(text) : pattern.regex.test(text)) {
13300
13883
  const category = pattern.policyId === "mcp.poisoning.embedded-credential" ? "secret" : pattern.policyId === "mcp.poisoning.exfil-verb" ? "egress" : "prompt-injection";
13301
13884
  matches.push(makeMatch(category, pattern.policyId, "high", pattern.confidence, `tool:${tool.name}`, `Tool "${tool.name}" description contains a tool-poisoning signal; treat MCP tool metadata as untrusted.`));
13302
13885
  }
@@ -13324,7 +13907,7 @@ function scanMcpManifest(manifest, options = {}) {
13324
13907
  }
13325
13908
  return matches;
13326
13909
  }
13327
- var POISONING_PATTERNS, LINE_JUMPING_PATTERNS, INVISIBLE_UNICODE;
13910
+ var POISONING_PATTERNS, LINE_JUMPING_PATTERNS, INVISIBLE_UNICODE, SCHEMA_TEXT_KEYS, SCHEMA_NODE_BUDGET = 1e4;
13328
13911
  var init_mcp = __esm(() => {
13329
13912
  POISONING_PATTERNS = [
13330
13913
  {
@@ -13354,7 +13937,8 @@ var init_mcp = __esm(() => {
13354
13937
  },
13355
13938
  {
13356
13939
  policyId: "mcp.poisoning.comment-payload",
13357
- regex: /<!--[\s\S]*?-->|<important>[\s\S]*?<\/important>/i,
13940
+ regex: /<!--|<important>/i,
13941
+ test: hasCommentPayload,
13358
13942
  confidence: 0.7
13359
13943
  }
13360
13944
  ];
@@ -13376,8 +13960,12 @@ var init_mcp = __esm(() => {
13376
13960
  }
13377
13961
  ];
13378
13962
  INVISIBLE_UNICODE = new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]" + "|[\\u{E0000}-\\u{E007F}]", "u");
13963
+ SCHEMA_TEXT_KEYS = new Set(["description", "title", "default", "const", "examples", "enum"]);
13379
13964
  });
13380
13965
 
13966
+ // src/lib/args.ts
13967
+ var init_args = () => {};
13968
+
13381
13969
  // src/lib/shell-syntax.ts
13382
13970
  function splitSegments(command) {
13383
13971
  const segments = [];
@@ -13494,7 +14082,7 @@ function stripAssignments(words) {
13494
14082
  return words.slice(i);
13495
14083
  }
13496
14084
  function commandWord(words) {
13497
- const first = stripAssignments(words)[0] ?? "";
14085
+ const first = (stripAssignments(words)[0] ?? "").replace(/^[({]+/, "").replace(/\\(.)/g, "$1");
13498
14086
  const base = first.split("/").pop() ?? first;
13499
14087
  return base.toLowerCase();
13500
14088
  }
@@ -13526,13 +14114,197 @@ function isCatastrophicTarget(arg) {
13526
14114
  function hasRecursive(words) {
13527
14115
  return args(words).some((a) => RECURSIVE_FLAG.test(a));
13528
14116
  }
13529
- function classifySegment(seg) {
14117
+ function gitConfigEntry(spec, fromEnv) {
14118
+ const eq = spec.indexOf("=");
14119
+ return { key: (eq < 0 ? spec : spec.slice(0, eq)).toLowerCase(), value: eq < 0 ? undefined : spec.slice(eq + 1), opaque: fromEnv || wordIsOpaque(spec) };
14120
+ }
14121
+ function parseGitInvocation(input) {
14122
+ const bare = stripAssignments(input);
14123
+ let envConfig = input.slice(0, input.length - bare.length).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w));
14124
+ const words = bare.map(unescapeWord);
14125
+ const config = [];
14126
+ let unknownGlobal = false;
14127
+ let i = 1;
14128
+ for (;i < words.length; i += 1) {
14129
+ const w = words[i];
14130
+ if (!w.startsWith("-"))
14131
+ break;
14132
+ const eq = w.startsWith("--") ? w.indexOf("=") : -1;
14133
+ const name = eq > 0 ? w.slice(0, eq) : w;
14134
+ if (GIT_GLOBAL_VALUE_OPTIONS.has(name)) {
14135
+ if (GIT_REPOSITORY_OPTIONS.has(name))
14136
+ envConfig = true;
14137
+ const value = eq > 0 ? w.slice(eq + 1) : words[++i];
14138
+ if (name === "-c" || name === "--config-env")
14139
+ config.push(gitConfigEntry(value ?? "", name === "--config-env"));
14140
+ continue;
14141
+ }
14142
+ if (!GIT_GLOBAL_FLAGS.has(name))
14143
+ unknownGlobal = true;
14144
+ }
14145
+ let rest = words.slice(i);
14146
+ const invocation = (sub, opaqueSub, shellAlias) => ({
14147
+ sub,
14148
+ args: rest.slice(1),
14149
+ config,
14150
+ opaqueSub,
14151
+ unknownGlobal,
14152
+ envConfig,
14153
+ ...shellAlias !== undefined ? { shellAlias } : {}
14154
+ });
14155
+ for (let guard = 0;guard < 8 && rest.length > 0; guard += 1) {
14156
+ const head = rest[0];
14157
+ if (wordIsOpaque(head))
14158
+ return invocation(undefined, true);
14159
+ if (GIT_COMMANDS.has(head.toLowerCase()))
14160
+ return invocation(head.toLowerCase(), false);
14161
+ const alias = [...config].reverse().find((c) => c.key === `alias.${head.toLowerCase()}`);
14162
+ if (alias === undefined)
14163
+ return invocation(head, false);
14164
+ if (alias.opaque || alias.value === undefined)
14165
+ return invocation(undefined, true);
14166
+ if (alias.value.trimStart().startsWith("!"))
14167
+ return invocation(undefined, true, [alias.value.trimStart().slice(1), ...rest.slice(1)].join(" "));
14168
+ const expanded = splitWords(alias.value);
14169
+ if (expanded.length === 0 || expanded[0].startsWith("-"))
14170
+ return invocation(undefined, true);
14171
+ rest = [...expanded, ...rest.slice(1)];
14172
+ }
14173
+ return invocation(undefined, rest.length > 0);
14174
+ }
14175
+ function resolvePushOption(name) {
14176
+ if (PUSH_LONG_OPTIONS.has(name))
14177
+ return name;
14178
+ const candidates = [...PUSH_LONG_OPTIONS.keys()].filter((option) => option.startsWith(name));
14179
+ return candidates.length === 1 ? candidates[0] : undefined;
14180
+ }
14181
+ function parsePushArgs(args) {
14182
+ const request = { force: false, del: false, mirror: false, all: false, prune: false, unknown: false, positionals: [] };
14183
+ const set = (option) => {
14184
+ if (option === "force" || option === "force-with-lease" || option === "force-if-includes")
14185
+ request.force = true;
14186
+ else if (option === "delete")
14187
+ request.del = true;
14188
+ else if (option === "mirror")
14189
+ request.mirror = true;
14190
+ else if (option === "all" || option === "branches")
14191
+ request.all = true;
14192
+ else if (option === "prune")
14193
+ request.prune = true;
14194
+ };
14195
+ let options = true;
14196
+ for (let i = 0;i < args.length; i += 1) {
14197
+ const w = args[i];
14198
+ if (!options || w === "-" || !w.startsWith("-")) {
14199
+ request.positionals.push(w);
14200
+ continue;
14201
+ }
14202
+ if (w === "--") {
14203
+ options = false;
14204
+ continue;
14205
+ }
14206
+ if (w.startsWith("--")) {
14207
+ const eq = w.indexOf("=");
14208
+ const name = eq > 0 ? w.slice(2, eq) : w.slice(2);
14209
+ if (name.startsWith("n"))
14210
+ continue;
14211
+ const option = resolvePushOption(name);
14212
+ if (option === undefined) {
14213
+ request.unknown = true;
14214
+ continue;
14215
+ }
14216
+ if (PUSH_LONG_OPTIONS.get(option) === "value" && eq < 0)
14217
+ i += 1;
14218
+ set(option);
14219
+ continue;
14220
+ }
14221
+ for (let at = 1;at < w.length; at += 1) {
14222
+ const ch = w[at];
14223
+ if (ch === "o") {
14224
+ if (at === w.length - 1)
14225
+ i += 1;
14226
+ break;
14227
+ }
14228
+ if (!PUSH_SHORT_FLAGS.has(ch)) {
14229
+ request.unknown = true;
14230
+ break;
14231
+ }
14232
+ if (ch === "f")
14233
+ request.force = true;
14234
+ if (ch === "d")
14235
+ request.del = true;
14236
+ }
14237
+ }
14238
+ return request;
14239
+ }
14240
+ function isDangerousDestination(destination) {
14241
+ const name = (destination.split("/").pop() ?? "").toLowerCase();
14242
+ return destination.includes("*") || PROTECTED_BRANCHES.includes(name) || name === "" || name === "head" || name === "@";
14243
+ }
14244
+ function refspecDestroys(spec, force, del) {
14245
+ if (wordIsOpaque(spec))
14246
+ return true;
14247
+ const plus = spec.startsWith("+");
14248
+ const body = plus ? spec.slice(1) : spec;
14249
+ if (del)
14250
+ return isDangerousDestination(body);
14251
+ if (body === ":")
14252
+ return force || plus;
14253
+ const deletes = body.startsWith(":");
14254
+ if (!(force || plus || deletes))
14255
+ return false;
14256
+ const destination = body.slice(body.lastIndexOf(":") + 1);
14257
+ if (deletes && destination !== "" && !destination.includes("*")) {
14258
+ return PROTECTED_BRANCHES.includes((destination.split("/").pop() ?? "").toLowerCase());
14259
+ }
14260
+ return isDangerousDestination(destination);
14261
+ }
14262
+ function isConfigTrue(value) {
14263
+ return value === undefined || !/^(?:false|no|off|0|)$/i.test(value.trim());
14264
+ }
14265
+ function configArmsPush(entry) {
14266
+ if (CONFIG_INCLUDE_KEY.test(entry.key))
14267
+ return true;
14268
+ const match = PUSH_SHAPING_KEY.exec(entry.key);
14269
+ if (match === null)
14270
+ return false;
14271
+ if (entry.opaque)
14272
+ return true;
14273
+ return match[1] === "mirror" ? isConfigTrue(entry.value) : refspecDestroys(entry.value ?? "", false, false);
14274
+ }
14275
+ function configWriteArmsPush(entry) {
14276
+ if (!entry.key.startsWith("alias."))
14277
+ return configArmsPush(entry);
14278
+ if (entry.opaque || entry.value === undefined)
14279
+ return true;
14280
+ const value = entry.value.trimStart();
14281
+ return /\bpush\b/i.test(value) || value.startsWith("!") && isDestructiveCommand(value.slice(1));
14282
+ }
14283
+ function gitWords(v) {
14284
+ if (v.cmd === "git")
14285
+ return v.words;
14286
+ if (!WRAPPER_COMMANDS.has(v.cmd))
14287
+ return;
14288
+ const peeled = peelWrappers(v.words);
14289
+ return peeled.inner === undefined && baseName(peeled.words[0]) === "git" ? peeled.words : undefined;
14290
+ }
14291
+ function assignsGitConfigSource(words) {
14292
+ const bare = stripAssignments(words);
14293
+ if (words.slice(0, words.length - bare.length + 1).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w)))
14294
+ return true;
14295
+ const cmd = commandWord(bare);
14296
+ if (!ASSIGNING_BUILTINS.has(cmd) && !WRAPPER_COMMANDS.has(cmd))
14297
+ return false;
14298
+ return bare.slice(1).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w));
14299
+ }
14300
+ function classifySegment(seg, configRelocated) {
13530
14301
  const view = {
13531
14302
  raw: seg.raw,
13532
14303
  cmd: head(seg.words),
13533
14304
  args: args(seg.words),
13534
14305
  positionals: positionals(seg.words),
13535
- words: seg.words
14306
+ words: seg.words,
14307
+ configRelocated
13536
14308
  };
13537
14309
  return RULES3.some((rule) => rule(view));
13538
14310
  }
@@ -13540,8 +14312,10 @@ function classifyCommand(command) {
13540
14312
  if (command.trim().length === 0)
13541
14313
  return "shell";
13542
14314
  const segments = splitSegments(command);
14315
+ let configRelocated = false;
13543
14316
  for (const seg of segments) {
13544
- if (classifySegment(seg))
14317
+ configRelocated ||= assignsGitConfigSource(seg.words);
14318
+ if (classifySegment(seg, configRelocated))
13545
14319
  return "destructive";
13546
14320
  }
13547
14321
  for (let i = 1;i < segments.length; i++) {
@@ -13560,8 +14334,78 @@ function isDestructiveCommand(command) {
13560
14334
  return classifyCommand(command) === "destructive";
13561
14335
  }
13562
14336
  function touchesAgentCredentials(command) {
13563
- const text = command.toLowerCase();
13564
- return CREDENTIAL_MARKERS.some((marker) => text.includes(marker));
14337
+ return textSpellings(command).some((text) => CREDENTIAL_MARKERS.some((marker) => text.includes(marker)));
14338
+ }
14339
+ function textSpellings(command) {
14340
+ return [command, collapseQuoting(joinContinuations(command))].map((text) => text.toLowerCase().replace(/\s+/g, " "));
14341
+ }
14342
+ function baseName(word) {
14343
+ return ((word ?? "").split("/").pop() ?? "").toLowerCase();
14344
+ }
14345
+ function unescapeWord(word) {
14346
+ return word.replace(/\\(.)/g, "$1");
14347
+ }
14348
+ function peelWrappers(input) {
14349
+ let words = stripAssignments(input.map(unescapeWord));
14350
+ for (let guard = 0;guard < 16 && words.length > 0; guard++) {
14351
+ const cmd = (words[0].split("/").pop() ?? "").toLowerCase();
14352
+ if (SHELLS.has(cmd)) {
14353
+ const at = words.findIndex((w, i) => i > 0 && /^-[a-z]*c[a-z]*$/i.test(w));
14354
+ const script = at > 0 ? words[at + 1] : undefined;
14355
+ if (script !== undefined)
14356
+ return { words, inner: script };
14357
+ return { words };
14358
+ }
14359
+ if (cmd === "eval")
14360
+ return { words, inner: words.slice(1).join(" ") };
14361
+ if (!WRAPPER_COMMANDS.has(cmd))
14362
+ return { words };
14363
+ let i = 1;
14364
+ while (i < words.length) {
14365
+ const w = words[i];
14366
+ if (w === "--") {
14367
+ i++;
14368
+ break;
14369
+ }
14370
+ if (w.startsWith("-")) {
14371
+ i += WRAPPER_FLAGS_WITH_VALUE.has(w) ? 2 : 1;
14372
+ continue;
14373
+ }
14374
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w)) {
14375
+ i++;
14376
+ continue;
14377
+ }
14378
+ if ((cmd === "timeout" || cmd === "nice") && /^[0-9.]+[smhd]?$/.test(w)) {
14379
+ i++;
14380
+ continue;
14381
+ }
14382
+ break;
14383
+ }
14384
+ words = words.slice(i);
14385
+ }
14386
+ return { words };
14387
+ }
14388
+ function decodeAnsiC(body) {
14389
+ return body.replace(/\\(?:x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|([0-7]{1,3})|(.))/gs, (_m, x, u, big, oct, ch) => {
14390
+ const code = x ?? u ?? big;
14391
+ if (code !== undefined)
14392
+ return String.fromCodePoint(Math.min(Number.parseInt(code, 16), 1114111));
14393
+ if (oct !== undefined)
14394
+ return String.fromCharCode(Number.parseInt(oct, 8) & 255);
14395
+ return ANSI_C_ESCAPES[ch ?? ""] ?? `\\${ch ?? ""}`;
14396
+ });
14397
+ }
14398
+ function joinContinuations(command) {
14399
+ return command.replace(/\\\r?\n/g, "");
14400
+ }
14401
+ function expandAnsiCQuotes(command) {
14402
+ return command.replace(/\$'((?:[^'\\]|\\.)*)'/gs, (_m, body) => `'${decodeAnsiC(body).replace(/'/g, "'\\''")}'`);
14403
+ }
14404
+ function collapseQuoting(command) {
14405
+ return expandAnsiCQuotes(command).replace(/["'\\]/g, "");
14406
+ }
14407
+ function wordIsOpaque(word) {
14408
+ return /[$`{]/.test(word);
13565
14409
  }
13566
14410
  var CATASTROPHIC_TARGETS, RECURSIVE_FLAG, PRIVILEGE, INTERPRETERS, DOWNLOADERS, CONTAINER_RUNTIMES, PROTECTED_BRANCHES, BLOCK_DEVICE, rulePrivilege = (v) => PRIVILEGE.has(v.cmd), ruleBlockDeviceRedirect = (v) => BLOCK_DEVICE.test(v.raw) && /(?:^|\s)(?:>|>>)\s*\/dev\//.test(v.raw), ruleRm = (v) => v.cmd === "rm" && v.positionals.some(isCatastrophicTarget), ruleDd = (v) => v.cmd === "dd" && v.args.some((x) => /^of=/i.test(x) && BLOCK_DEVICE.test(x)), rulePermissionSweep = (v) => (v.cmd === "chmod" || v.cmd === "chown" || v.cmd === "chgrp") && hasRecursive(v.words) && v.positionals.some(isCatastrophicTarget), ruleHostState = (v) => {
13567
14411
  if (v.cmd === "shutdown" || v.cmd === "reboot" || v.cmd === "halt" || v.cmd === "poweroff")
@@ -13572,19 +14416,52 @@ var CATASTROPHIC_TARGETS, RECURSIVE_FLAG, PRIVILEGE, INTERPRETERS, DOWNLOADERS,
13572
14416
  return false;
13573
14417
  const joined = ` ${v.args.join(" ")}`;
13574
14418
  return /--privileged\b/.test(joined) || /--pid[= ]host\b/.test(joined) || /--(?:userns|ipc|uts|network|net)[= ]host\b/.test(joined) || /docker\.sock/.test(joined) || /(?:^|\s)(?:-v|--volume)[= ]\/:/.test(joined) || /source=\/(?:,|\s|$)/.test(joined);
13575
- }, ruleForcePush = (v) => {
13576
- if (v.cmd !== "git" || !v.positionals.includes("push"))
14419
+ }, GIT_GLOBAL_VALUE_OPTIONS, GIT_GLOBAL_FLAGS, GIT_COMMANDS, GIT_CONFIG_SOURCE_ASSIGNMENT, GIT_REPOSITORY_OPTIONS, PUSH_LONG_OPTIONS, PUSH_SHORT_FLAGS, PUSH_SHAPING_KEY, CONFIG_INCLUDE_KEY, ruleForcePush = (v) => {
14420
+ const words = gitWords(v);
14421
+ if (words === undefined)
13577
14422
  return false;
13578
- const forced = v.args.some((x) => x === "-f" || x === "--force" || x.startsWith("--force-with-lease"));
13579
- if (!forced)
14423
+ const git = parseGitInvocation(words);
14424
+ if (git.shellAlias !== undefined)
14425
+ return isDestructiveCommand(git.shellAlias) || /\bpush\b/.test(git.shellAlias);
14426
+ if (git.opaqueSub)
14427
+ return true;
14428
+ if (git.unknownGlobal && positionals(words).includes("push"))
14429
+ return true;
14430
+ if (git.sub !== "push")
13580
14431
  return false;
13581
- const after = v.positionals.slice(v.positionals.indexOf("push") + 1);
13582
- if (after.length === 0)
14432
+ if (git.envConfig || v.configRelocated === true || git.args.some(wordIsOpaque) || git.config.some(configArmsPush))
13583
14433
  return true;
13584
- const named = after.map((x) => x.split("/").pop()?.toLowerCase() ?? "");
13585
- return named.some((n) => PROTECTED_BRANCHES.includes(n));
13586
- }, RULES3, CREDENTIAL_MARKERS, SYSTEMCTL_CONTROL_VERBS, SCHEDULE_VERBS, WRAPPER_COMMANDS, WRAPPER_FLAGS_WITH_VALUE, SHELLS, KERYX_ENTRY_NAMES, JS_RUNNERS, TERMINAL_DRIVERS;
14434
+ const push = parsePushArgs(git.args);
14435
+ if (push.unknown || push.mirror)
14436
+ return true;
14437
+ const refspecs = push.positionals.slice(1);
14438
+ if (push.all && (push.force || push.prune))
14439
+ return true;
14440
+ if (push.prune && (refspecs.length === 0 || refspecs.some((spec) => spec.includes("*"))))
14441
+ return true;
14442
+ if (refspecs.length === 0)
14443
+ return push.force;
14444
+ return refspecs.some((spec) => refspecDestroys(spec, push.force, push.del));
14445
+ }, ruleArmsForcePush = (v) => {
14446
+ const words = gitWords(v);
14447
+ if (words === undefined)
14448
+ return false;
14449
+ const git = parseGitInvocation(words);
14450
+ if (git.sub === "remote") {
14451
+ if (git.args[0] !== "add")
14452
+ return false;
14453
+ return git.args.some((w) => {
14454
+ const [name = "", value] = w.split("=", 2);
14455
+ return name.length > 3 && "--mirror".startsWith(name) && value !== "fetch";
14456
+ });
14457
+ }
14458
+ if (git.sub !== "config")
14459
+ return false;
14460
+ const operands = git.args.filter((w) => !w.startsWith("-"));
14461
+ return operands.some((w, i) => i + 1 < operands.length && configWriteArmsPush(gitConfigEntry(`${w}=${operands[i + 1]}`, false)));
14462
+ }, RULES3, ASSIGNING_BUILTINS, CREDENTIAL_MARKERS, SYSTEMCTL_CONTROL_VERBS, SCHEDULE_VERBS, WRAPPER_COMMANDS, WRAPPER_FLAGS_WITH_VALUE, SHELLS, KERYX_ENTRY_NAMES, JS_RUNNERS, TERMINAL_DRIVERS, ANSI_C_ESCAPES, OPERATOR_REVIEW_VERBS, UNATTENDED_REVIEW_VERBS, HUB_MACHINE_OPERATOR_VERBS, OPERATOR_COMMAND_PATHS, MAX_COMMAND_PATH, OPERATOR_PATH_WORDS;
13587
14463
  var init_command_risk = __esm(() => {
14464
+ init_args();
13588
14465
  init_shell_syntax();
13589
14466
  CATASTROPHIC_TARGETS = new Set([
13590
14467
  "/",
@@ -13631,6 +14508,255 @@ var init_command_risk = __esm(() => {
13631
14508
  CONTAINER_RUNTIMES = new Set(["docker", "podman", "nerdctl"]);
13632
14509
  PROTECTED_BRANCHES = ["main", "master", "develop", "development", "trunk"];
13633
14510
  BLOCK_DEVICE = /\/dev\/(?:sd[a-z]|nvme\d|disk\d|hd[a-z]|vd[a-z]|mmcblk\d)/i;
14511
+ GIT_GLOBAL_VALUE_OPTIONS = new Set([
14512
+ "-C",
14513
+ "-c",
14514
+ "--git-dir",
14515
+ "--work-tree",
14516
+ "--namespace",
14517
+ "--super-prefix",
14518
+ "--config-env",
14519
+ "--attr-source"
14520
+ ]);
14521
+ GIT_GLOBAL_FLAGS = new Set([
14522
+ "-p",
14523
+ "--paginate",
14524
+ "-P",
14525
+ "--no-pager",
14526
+ "--no-replace-objects",
14527
+ "--bare",
14528
+ "--literal-pathspecs",
14529
+ "--glob-pathspecs",
14530
+ "--noglob-pathspecs",
14531
+ "--icase-pathspecs",
14532
+ "--no-optional-locks",
14533
+ "--no-lazy-fetch",
14534
+ "--no-advice",
14535
+ "--html-path",
14536
+ "--man-path",
14537
+ "--info-path",
14538
+ "--exec-path",
14539
+ "--list-cmds",
14540
+ "-v",
14541
+ "--version",
14542
+ "-h",
14543
+ "--help"
14544
+ ]);
14545
+ GIT_COMMANDS = new Set([
14546
+ "add",
14547
+ "am",
14548
+ "annotate",
14549
+ "apply",
14550
+ "archimport",
14551
+ "archive",
14552
+ "backfill",
14553
+ "bisect",
14554
+ "blame",
14555
+ "branch",
14556
+ "bugreport",
14557
+ "bundle",
14558
+ "cat-file",
14559
+ "check-attr",
14560
+ "check-ignore",
14561
+ "check-mailmap",
14562
+ "check-ref-format",
14563
+ "checkout",
14564
+ "checkout--worker",
14565
+ "checkout-index",
14566
+ "cherry",
14567
+ "cherry-pick",
14568
+ "clean",
14569
+ "clone",
14570
+ "column",
14571
+ "commit",
14572
+ "commit-graph",
14573
+ "commit-tree",
14574
+ "config",
14575
+ "count-objects",
14576
+ "credential",
14577
+ "credential-cache",
14578
+ "credential-cache--daemon",
14579
+ "credential-netrc",
14580
+ "credential-osxkeychain",
14581
+ "credential-store",
14582
+ "cvsexportcommit",
14583
+ "cvsimport",
14584
+ "cvsserver",
14585
+ "daemon",
14586
+ "describe",
14587
+ "diagnose",
14588
+ "diff",
14589
+ "diff-files",
14590
+ "diff-index",
14591
+ "diff-pairs",
14592
+ "diff-tree",
14593
+ "difftool",
14594
+ "difftool--helper",
14595
+ "env--helper",
14596
+ "fast-export",
14597
+ "fast-import",
14598
+ "fetch",
14599
+ "fetch-pack",
14600
+ "filter-branch",
14601
+ "fmt-merge-msg",
14602
+ "for-each-ref",
14603
+ "for-each-repo",
14604
+ "format-patch",
14605
+ "fsck",
14606
+ "fsck-objects",
14607
+ "fsmonitor--daemon",
14608
+ "gc",
14609
+ "get-tar-commit-id",
14610
+ "grep",
14611
+ "hash-object",
14612
+ "help",
14613
+ "hook",
14614
+ "http-backend",
14615
+ "http-fetch",
14616
+ "http-push",
14617
+ "imap-send",
14618
+ "index-pack",
14619
+ "init",
14620
+ "init-db",
14621
+ "instaweb",
14622
+ "interpret-trailers",
14623
+ "log",
14624
+ "ls-files",
14625
+ "ls-remote",
14626
+ "ls-tree",
14627
+ "mailinfo",
14628
+ "mailsplit",
14629
+ "maintenance",
14630
+ "merge",
14631
+ "merge-base",
14632
+ "merge-file",
14633
+ "merge-index",
14634
+ "merge-octopus",
14635
+ "merge-one-file",
14636
+ "merge-ours",
14637
+ "merge-recursive",
14638
+ "merge-recursive-ours",
14639
+ "merge-recursive-theirs",
14640
+ "merge-resolve",
14641
+ "merge-subtree",
14642
+ "merge-tree",
14643
+ "mergetool",
14644
+ "mktag",
14645
+ "mktree",
14646
+ "multi-pack-index",
14647
+ "mv",
14648
+ "name-rev",
14649
+ "notes",
14650
+ "p4",
14651
+ "pack-objects",
14652
+ "pack-redundant",
14653
+ "pack-refs",
14654
+ "patch-id",
14655
+ "pickaxe",
14656
+ "prune",
14657
+ "prune-packed",
14658
+ "pull",
14659
+ "push",
14660
+ "quiltimport",
14661
+ "range-diff",
14662
+ "read-tree",
14663
+ "rebase",
14664
+ "rebase--interactive",
14665
+ "receive-pack",
14666
+ "reflog",
14667
+ "refs",
14668
+ "remote",
14669
+ "remote-ext",
14670
+ "remote-fd",
14671
+ "remote-ftp",
14672
+ "remote-ftps",
14673
+ "remote-http",
14674
+ "remote-https",
14675
+ "repack",
14676
+ "replace",
14677
+ "replay",
14678
+ "request-pull",
14679
+ "rerere",
14680
+ "reset",
14681
+ "restore",
14682
+ "rev-list",
14683
+ "rev-parse",
14684
+ "revert",
14685
+ "rm",
14686
+ "send-email",
14687
+ "send-pack",
14688
+ "serve",
14689
+ "sh-i18n--envsubst",
14690
+ "shell",
14691
+ "shortlog",
14692
+ "show",
14693
+ "show-branch",
14694
+ "show-index",
14695
+ "show-ref",
14696
+ "sparse-checkout",
14697
+ "stage",
14698
+ "stash",
14699
+ "status",
14700
+ "stripspace",
14701
+ "submodule",
14702
+ "submodule--helper",
14703
+ "subtree",
14704
+ "switch",
14705
+ "symbolic-ref",
14706
+ "tag",
14707
+ "unpack-file",
14708
+ "unpack-objects",
14709
+ "update-index",
14710
+ "update-ref",
14711
+ "update-server-info",
14712
+ "upload-archive",
14713
+ "upload-archive--writer",
14714
+ "upload-pack",
14715
+ "var",
14716
+ "verify-commit",
14717
+ "verify-pack",
14718
+ "verify-tag",
14719
+ "version",
14720
+ "web--browse",
14721
+ "whatchanged",
14722
+ "worktree",
14723
+ "write-tree"
14724
+ ]);
14725
+ GIT_CONFIG_SOURCE_ASSIGNMENT = /^[({]*(?:HOME|XDG_CONFIG_HOME|GIT_DIR|GIT_COMMON_DIR|GIT_WORK_TREE|GIT_CONFIG\w*)=/i;
14726
+ GIT_REPOSITORY_OPTIONS = new Set(["--git-dir", "--work-tree"]);
14727
+ PUSH_LONG_OPTIONS = new Map([
14728
+ ["verbose", "flag"],
14729
+ ["quiet", "flag"],
14730
+ ["repo", "value"],
14731
+ ["all", "flag"],
14732
+ ["branches", "flag"],
14733
+ ["mirror", "flag"],
14734
+ ["delete", "flag"],
14735
+ ["tags", "flag"],
14736
+ ["dry-run", "flag"],
14737
+ ["porcelain", "flag"],
14738
+ ["force", "flag"],
14739
+ ["force-with-lease", "optional"],
14740
+ ["force-if-includes", "flag"],
14741
+ ["recurse-submodules", "value"],
14742
+ ["thin", "flag"],
14743
+ ["receive-pack", "value"],
14744
+ ["exec", "value"],
14745
+ ["set-upstream", "flag"],
14746
+ ["progress", "flag"],
14747
+ ["prune", "flag"],
14748
+ ["no-verify", "flag"],
14749
+ ["verify", "flag"],
14750
+ ["follow-tags", "flag"],
14751
+ ["signed", "optional"],
14752
+ ["atomic", "flag"],
14753
+ ["push-option", "value"],
14754
+ ["ipv4", "flag"],
14755
+ ["ipv6", "flag"]
14756
+ ]);
14757
+ PUSH_SHORT_FLAGS = new Set(["v", "q", "d", "n", "f", "u", "4", "6"]);
14758
+ PUSH_SHAPING_KEY = /^remote\..+\.(push|mirror)$/;
14759
+ CONFIG_INCLUDE_KEY = /^include(?:if\..*)?\.path$/;
13634
14760
  RULES3 = [
13635
14761
  rulePrivilege,
13636
14762
  ruleBlockDeviceRedirect,
@@ -13640,8 +14766,10 @@ var init_command_risk = __esm(() => {
13640
14766
  ruleHostState,
13641
14767
  ruleMkfs,
13642
14768
  ruleContainerEscape,
13643
- ruleForcePush
14769
+ ruleForcePush,
14770
+ ruleArmsForcePush
13644
14771
  ];
14772
+ ASSIGNING_BUILTINS = new Set(["export", "declare", "typeset", "readonly", "local"]);
13645
14773
  CREDENTIAL_MARKERS = [
13646
14774
  "permissions.json",
13647
14775
  "auth.json",
@@ -13696,6 +14824,21 @@ var init_command_risk = __esm(() => {
13696
14824
  KERYX_ENTRY_NAMES = new Set(["keryx", "cli.ts", "cli.js"]);
13697
14825
  JS_RUNNERS = new Set(["bun", "bunx", "node", "npx", "pnpx", "tsx", "deno"]);
13698
14826
  TERMINAL_DRIVERS = new Set(["script", "screen", "tmux", "unbuffer", "dtach", "abduco", "expect", "socat", "zpty"]);
14827
+ ANSI_C_ESCAPES = { n: `
14828
+ `, t: "\t", r: "\r", a: "\x07", b: "\b", e: "\x1B", E: "\x1B", f: "\f", v: "\v", "\\": "\\", "'": "'", '"': '"', "?": "?" };
14829
+ OPERATOR_REVIEW_VERBS = new Set(["waive-mutation", "override-reviewers", "confirm-plan", "decide-premise"]);
14830
+ UNATTENDED_REVIEW_VERBS = new Set(["start", "attach", "plan"]);
14831
+ HUB_MACHINE_OPERATOR_VERBS = new Set(["pair", "allow", "configure", "unpair"]);
14832
+ OPERATOR_COMMAND_PATHS = [
14833
+ ...["accept", "promote", "graduate"].map((verb) => ["learn", verb]),
14834
+ ["agents", "external", "apply"],
14835
+ ...[...OPERATOR_REVIEW_VERBS, ...UNATTENDED_REVIEW_VERBS, "estimate"].map((verb) => ["review", verb]),
14836
+ ["review", "comments", "reply"],
14837
+ ["review", "chunks", "estimate"],
14838
+ ...[...HUB_MACHINE_OPERATOR_VERBS].map((verb) => ["hub", "machine", verb])
14839
+ ];
14840
+ MAX_COMMAND_PATH = Math.max(...OPERATOR_COMMAND_PATHS.map((path) => path.length));
14841
+ OPERATOR_PATH_WORDS = new Set(OPERATOR_COMMAND_PATHS.flat());
13699
14842
  });
13700
14843
 
13701
14844
  // src/security/audit-harness/checks.ts
@@ -13736,6 +14879,9 @@ function foldFullWidth(ch) {
13736
14879
  const code = ch.codePointAt(0);
13737
14880
  return code >= 65281 && code <= 65374 ? String.fromCodePoint(code - 65248) : ch;
13738
14881
  }
14882
+ function isCodePoint(code) {
14883
+ return Number.isInteger(code) && code >= 0 && code <= 1114111;
14884
+ }
13739
14885
  function decodeEntityStep(text) {
13740
14886
  const match = HTML_ENTITY_AT_RE.exec(text);
13741
14887
  if (!match)
@@ -13743,11 +14889,11 @@ function decodeEntityStep(text) {
13743
14889
  const body = match[1];
13744
14890
  if (body.startsWith("#x")) {
13745
14891
  const code = Number.parseInt(body.slice(2), 16);
13746
- return Number.isFinite(code) ? String.fromCodePoint(code) : undefined;
14892
+ return isCodePoint(code) ? String.fromCodePoint(code) : undefined;
13747
14893
  }
13748
14894
  if (body.startsWith("#")) {
13749
14895
  const code = Number.parseInt(body.slice(1), 10);
13750
- return Number.isFinite(code) ? String.fromCodePoint(code) : undefined;
14896
+ return isCodePoint(code) ? String.fromCodePoint(code) : undefined;
13751
14897
  }
13752
14898
  return NAMED_HTML_ENTITIES[body];
13753
14899
  }
@@ -13973,7 +15119,7 @@ function checkMissingDenyList(relativePath, settings) {
13973
15119
  return [];
13974
15120
  }
13975
15121
  const coversCredentials = deny.some((pattern) => touchesAgentCredentials(pattern));
13976
- if (coversCredentials && deny.length > 0) {
15122
+ if (coversCredentials) {
13977
15123
  return [];
13978
15124
  }
13979
15125
  const pointer = "/permissions/deny";
@@ -14023,9 +15169,13 @@ function checkBypassFlagPresent(relativePath, settings) {
14023
15169
  const findings = [];
14024
15170
  const root = asRecord2(settings);
14025
15171
  const permissions = asRecord2(root?.permissions);
14026
- const defaultMode = root?.defaultMode ?? permissions?.defaultMode;
14027
- if (defaultMode === "bypassPermissions") {
14028
- const pointer = permissions?.defaultMode === "bypassPermissions" ? "/permissions/defaultMode" : "/defaultMode";
15172
+ const modeLocations = [
15173
+ { pointer: "/permissions/defaultMode", value: permissions?.defaultMode },
15174
+ { pointer: "/defaultMode", value: root?.defaultMode }
15175
+ ];
15176
+ for (const { pointer, value } of modeLocations) {
15177
+ if (value !== "bypassPermissions")
15178
+ continue;
14029
15179
  findings.push({
14030
15180
  surface: "settings",
14031
15181
  check: "bypass-flag-present",
@@ -14037,7 +15187,7 @@ function checkBypassFlagPresent(relativePath, settings) {
14037
15187
  evidence: { category: "artifact-safety", matchedToken: "defaultMode:bypassPermissions" },
14038
15188
  internalProposal: {
14039
15189
  proposal: {
14040
- id: `reset-default-mode-${relativePath}`,
15190
+ id: pointer === "/defaultMode" ? `reset-default-mode-root-${relativePath}` : `reset-default-mode-${relativePath}`,
14041
15191
  rationale: 'Set defaultMode back to "default" so the approval gate applies.',
14042
15192
  patch: `- "defaultMode": "bypassPermissions"
14043
15193
  + "defaultMode": "default"`
@@ -14069,18 +15219,11 @@ function checkBypassFlagPresent(relativePath, settings) {
14069
15219
  function isPinnedPackageSpec(spec) {
14070
15220
  if (spec.length === 0)
14071
15221
  return false;
14072
- if (spec.startsWith("@")) {
14073
- const secondAt = spec.indexOf("@", 1);
14074
- if (secondAt === -1)
14075
- return false;
14076
- const version = spec.slice(secondAt + 1);
14077
- return version.length > 0 && version !== "latest";
14078
- }
14079
- const at = spec.indexOf("@");
15222
+ const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
14080
15223
  if (at === -1)
14081
15224
  return false;
14082
15225
  const version = spec.slice(at + 1);
14083
- return version.length > 0 && version !== "latest";
15226
+ return EXACT_VERSION_RE.test(version) || COMMIT_SHA_RE.test(version) || INTEGRITY_HASH_RE.test(version);
14084
15227
  }
14085
15228
  function packageArgFromLauncher(command, argv) {
14086
15229
  const base = command.split("/").pop() ?? command;
@@ -14498,7 +15641,7 @@ function checkAgentMissingModelTier(relativePath, content) {
14498
15641
  }
14499
15642
  ];
14500
15643
  }
14501
- var DROPPED_SINGLE_CODEPOINTS, COMBINING_MARK_RANGES, MATH_ALPHANUMERIC_LETTER_STYLE_STARTS, MATH_ALPHANUMERIC_DIGIT_STYLE_STARTS, MATH_ALPHANUMERIC_EXCEPTIONS, HTML_ENTITY_AT_RE, NAMED_HTML_ENTITIES, HOMOGLYPH_FOLD, QUOTE_CHARS_ANY, SENTENCE_END_CHARS, REPORTED_SPEECH_RE, SEVERITY_RANK, AUTO_RUN_PATTERNS, WILDCARD_ALLOW_PATTERNS, BYPASS_FLAGS, TOOL_INPUT_VAR = "(?:TOOL_INPUT|CLAUDE_TOOL_INPUT|tool_input|ARGUMENTS|1)", INTERP_VAR_RE, INSIDE_SUBSHELL_RE, INSIDE_BACKTICK_RE, INSIDE_DQUOTE_RE, NETWORK_CLIENTS, SHELL_INTERPRETERS = "sh|bash|zsh|dash|ksh|python3?|node|perl|ruby", SHELL_LIKE_INTERPRETERS = "sh|bash|zsh|dash|ksh", SCRIPT_STDIN_INTERPRETERS = "python3?|node|perl|ruby", DOWNLOAD_TOOLS = "curl|wget", ABS_PATH_PREFIX = "(?:/usr/bin/|/bin/|/usr/local/bin/)?", WRAPPER_PREFIX, TEE_HOP = `(?:tee\\s+\\S+\\s*\\|\\s*)?`, BACKTICK = "\\u0060", PIPE_TO_SHELL_RE, PIPE_TO_SCRIPT_STDIN_RE, DASH_C_COMMAND_SUB_RE, PROCESS_SUB_RE, SOURCE_PROCESS_SUB_RE, EVAL_COMMAND_SUB_RE, DOWNLOAD_THEN_EXEC_RE, POWERSHELL_IEX_RE, POWERSHELL_PIPE_IEX_RE, POWERSHELL_DOWNLOADSTRING_RE, REMOTE_EXEC_SHAPES, INTERPRETER_EXEC_FLAG_RE, REVERSE_SHELL_RE, BASE64_PIPE_SHELL_RE, XARGS_TO_SHELL_C_RE, DOWNLOAD_CHMOD_EXEC_RE, BEHAVIOR_CLASS_SHAPES, MAX_FENCE_INDENT = 3, TRAILING_EXIT0_RE, EMPTY_CATCH_RE, SCRIPT_SHELL_SUPPRESSION_RE;
15644
+ var DROPPED_SINGLE_CODEPOINTS, COMBINING_MARK_RANGES, MATH_ALPHANUMERIC_LETTER_STYLE_STARTS, MATH_ALPHANUMERIC_DIGIT_STYLE_STARTS, MATH_ALPHANUMERIC_EXCEPTIONS, HTML_ENTITY_AT_RE, NAMED_HTML_ENTITIES, HOMOGLYPH_FOLD, QUOTE_CHARS_ANY, SENTENCE_END_CHARS, REPORTED_SPEECH_RE, SEVERITY_RANK, AUTO_RUN_PATTERNS, WILDCARD_ALLOW_PATTERNS, BYPASS_FLAGS, EXACT_VERSION_RE, COMMIT_SHA_RE, INTEGRITY_HASH_RE, TOOL_INPUT_VAR = "(?:TOOL_INPUT|CLAUDE_TOOL_INPUT|tool_input|ARGUMENTS|1)", INTERP_VAR_RE, INSIDE_SUBSHELL_RE, INSIDE_BACKTICK_RE, INSIDE_DQUOTE_RE, NETWORK_CLIENTS, SHELL_INTERPRETERS = "sh|bash|zsh|dash|ksh|python3?|node|perl|ruby", SHELL_LIKE_INTERPRETERS = "sh|bash|zsh|dash|ksh", SCRIPT_STDIN_INTERPRETERS = "python3?|node|perl|ruby", DOWNLOAD_TOOLS = "curl|wget", ABS_PATH_PREFIX = "(?:/usr/bin/|/bin/|/usr/local/bin/)?", WRAPPER_PREFIX, TEE_HOP = `(?:tee\\s+\\S+\\s*\\|\\s*)?`, BACKTICK = "\\u0060", PIPE_TO_SHELL_RE, PIPE_TO_SCRIPT_STDIN_RE, DASH_C_COMMAND_SUB_RE, PROCESS_SUB_RE, SOURCE_PROCESS_SUB_RE, EVAL_COMMAND_SUB_RE, DOWNLOAD_THEN_EXEC_RE, POWERSHELL_IEX_RE, POWERSHELL_PIPE_IEX_RE, POWERSHELL_DOWNLOADSTRING_RE, REMOTE_EXEC_SHAPES, INTERPRETER_EXEC_FLAG_RE, REVERSE_SHELL_RE, BASE64_PIPE_SHELL_RE, XARGS_TO_SHELL_C_RE, DOWNLOAD_CHMOD_EXEC_RE, BEHAVIOR_CLASS_SHAPES, MAX_FENCE_INDENT = 3, TRAILING_EXIT0_RE, EMPTY_CATCH_RE, SCRIPT_SHELL_SUPPRESSION_RE;
14502
15645
  var init_checks = __esm(() => {
14503
15646
  init_injection();
14504
15647
  init_secrets();
@@ -14656,6 +15799,9 @@ var init_checks = __esm(() => {
14656
15799
  "--yolo",
14657
15800
  "--dangerously-bypass-approvals-and-sandbox"
14658
15801
  ];
15802
+ EXACT_VERSION_RE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
15803
+ COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
15804
+ INTEGRITY_HASH_RE = /^sha(?:256|384|512)[-:][A-Za-z0-9+/=_-]+$/;
14659
15805
  INTERP_VAR_RE = new RegExp(`\\$\\{?\\s*${TOOL_INPUT_VAR}\\s*\\}?`);
14660
15806
  INSIDE_SUBSHELL_RE = new RegExp(`\\$\\([^)]*\\$${TOOL_INPUT_VAR}\\b[^)]*\\)`);
14661
15807
  INSIDE_BACKTICK_RE = new RegExp(`\`[^\`]*\\$${TOOL_INPUT_VAR}\\b[^\`]*\``);
@@ -15753,12 +16899,15 @@ function ephemeralHashFn() {
15753
16899
  const key = ephemeralHmacKey;
15754
16900
  return (value) => hmacHash(value, key);
15755
16901
  }
15756
- async function analyze(cwd, input, opts = {}) {
16902
+ async function loadAnalysisContext(cwd) {
15757
16903
  const config = await loadSecurityConfig(cwd);
16904
+ return { config, hashFn: await hashFnFor(cwd) };
16905
+ }
16906
+ async function detectAndResolve(cwd, input, context, opts) {
16907
+ const { config, hashFn } = context;
15758
16908
  const original = await runDetectorsAsync(cwd, input.content, config);
15759
16909
  const contextual = opts.filesystemSourceScan && input.source === "trusted-project" && input.path ? await filterSourceMatches(input.content, input.path, original) : { matches: original };
15760
16910
  const matches = contextual.matches;
15761
- const hashFn = await hashFnFor(cwd);
15762
16911
  const buildOpts = {
15763
16912
  source: input.source,
15764
16913
  content: input.content,
@@ -15771,6 +16920,9 @@ async function analyze(cwd, input, opts = {}) {
15771
16920
  buildOpts.path = input.path;
15772
16921
  }
15773
16922
  const decision = resolveDecision(config, { ...buildOpts, matches });
16923
+ return contextual.warning !== undefined ? { decision, warning: contextual.warning } : { decision };
16924
+ }
16925
+ async function runSelfProtection(cwd, config) {
15774
16926
  const previous = await readState(cwd);
15775
16927
  const selfProtection = evaluateSelfProtection(config, previous);
15776
16928
  if (selfProtection.incidents.length > 0) {
@@ -15779,6 +16931,13 @@ async function analyze(cwd, input, opts = {}) {
15779
16931
  if (!config.configUnreadable) {
15780
16932
  await writeState(cwd, currentState(config));
15781
16933
  }
16934
+ return { findings: selfProtection.findings, warnings: selfProtection.warnings };
16935
+ }
16936
+ async function analyze(cwd, input, opts = {}) {
16937
+ const context = await loadAnalysisContext(cwd);
16938
+ const { config } = context;
16939
+ const { decision, warning } = await detectAndResolve(cwd, input, context, opts);
16940
+ const selfProtection = await runSelfProtection(cwd, config);
15782
16941
  if (selfProtection.findings.length > 0) {
15783
16942
  decision.findings.push(...selfProtection.findings);
15784
16943
  decision.gate = computeGate(decision.findings, config).gate;
@@ -15786,7 +16945,7 @@ async function analyze(cwd, input, opts = {}) {
15786
16945
  }
15787
16946
  return {
15788
16947
  decision,
15789
- warnings: [...selfProtection.warnings, ...contextual.warning ? [contextual.warning] : []],
16948
+ warnings: [...selfProtection.warnings, ...warning !== undefined ? [warning] : []],
15790
16949
  config
15791
16950
  };
15792
16951
  }
@@ -15825,32 +16984,50 @@ async function runScanPath(cwd, input) {
15825
16984
  });
15826
16985
  const findings = [];
15827
16986
  const warnings = [];
15828
- let config;
15829
16987
  const files = traversal.files.map((file) => ({ ...file }));
16988
+ const markIncomplete = () => {
16989
+ traversal.coverage.status = "incomplete";
16990
+ if (!traversal.coverage.reasons.includes("security analysis unavailable")) {
16991
+ traversal.coverage.reasons.push("security analysis unavailable");
16992
+ }
16993
+ };
16994
+ const context = await loadAnalysisContext(cwd).catch(() => {
16995
+ return;
16996
+ });
16997
+ let analyzed = 0;
15830
16998
  for (const item of traversal.contents) {
15831
16999
  try {
15832
- const result = await analyze(cwd, {
17000
+ if (context === undefined)
17001
+ throw new Error("security analysis unavailable");
17002
+ const result = await detectAndResolve(cwd, {
15833
17003
  content: item.content,
15834
17004
  source: input.source,
15835
17005
  path: item.path,
15836
17006
  ...input.target !== undefined ? { target: input.target } : {}
15837
- }, { filesystemSourceScan: true });
15838
- config ??= result.config;
17007
+ }, context, { filesystemSourceScan: true });
17008
+ analyzed += 1;
15839
17009
  findings.push(...result.decision.findings);
15840
- warnings.push(...result.warnings);
17010
+ if (result.warning !== undefined)
17011
+ warnings.push(result.warning);
15841
17012
  } catch {
15842
17013
  const file = files.find((entry) => entry.path === item.path && entry.status === "scanned");
15843
17014
  if (file !== undefined) {
15844
17015
  file.status = "failed";
15845
17016
  file.reason = "security analysis unavailable";
15846
17017
  }
15847
- traversal.coverage.status = "incomplete";
15848
- if (!traversal.coverage.reasons.includes("security analysis unavailable")) {
15849
- traversal.coverage.reasons.push("security analysis unavailable");
15850
- }
17018
+ markIncomplete();
17019
+ }
17020
+ }
17021
+ const config = context?.config ?? await loadSecurityConfig(cwd);
17022
+ if (analyzed > 0) {
17023
+ try {
17024
+ const selfProtection = await runSelfProtection(cwd, config);
17025
+ findings.push(...selfProtection.findings);
17026
+ warnings.unshift(...selfProtection.warnings);
17027
+ } catch {
17028
+ markIncomplete();
15851
17029
  }
15852
17030
  }
15853
- config ??= await loadSecurityConfig(cwd);
15854
17031
  const computed = computeGate(findings, config);
15855
17032
  const gate = computed.gate === "pass" && traversal.coverage.status === "incomplete" ? "incomplete" : computed.gate;
15856
17033
  const action = strongestAction(findings.map((finding) => finding.action));
@@ -26350,6 +27527,7 @@ __export(exports_service9, {
26350
27527
  hashVerifiedTaskOutcome: () => hashVerifiedTaskOutcome,
26351
27528
  isExternalSlateStale: () => isExternalSlateStale,
26352
27529
  isSlateSeedKind: () => isSlateSeedKind,
27530
+ isWorkspaceId: () => isWorkspaceId,
26353
27531
  listWorkspaceViews: () => listWorkspaceViews,
26354
27532
  localWorkspaceAuthorizationServer: () => localWorkspaceAuthorizationServer,
26355
27533
  lookupWorkspace: () => lookupWorkspace,
@@ -26359,6 +27537,7 @@ __export(exports_service9, {
26359
27537
  normalizeProposalLifecycleResult: () => normalizeProposalLifecycleResult,
26360
27538
  normalizeWorkspaceReference: () => normalizeWorkspaceReference,
26361
27539
  proposalNotePath: () => proposalNotePath,
27540
+ proposeFromSession: () => proposeFromSession,
26362
27541
  readExternalSlate: () => readExternalSlate,
26363
27542
  reclaimStaleExternalSlates: () => reclaimStaleExternalSlates,
26364
27543
  redactSensitiveText: () => redactSensitiveText,
@@ -42670,7 +43849,7 @@ class WorkspaceService {
42670
43849
  }
42671
43850
  }
42672
43851
  async readManifest(id) {
42673
- if (!/^[a-z][a-z0-9-]{2,63}$/.test(id))
43852
+ if (!isWorkspaceId(id))
42674
43853
  throw new WorkspaceServiceError("not_found", "workspace not found");
42675
43854
  let parsed;
42676
43855
  try {
@@ -42708,6 +43887,9 @@ function currentRoleOrRevoked(manifest, subject) {
42708
43887
  function localWorkspaceAuthorizationServer(subject = `user:local-${process.getuid?.() ?? process.pid}`) {
42709
43888
  return createSacAuthorizationServer({ authenticateRequest: async () => ({ subject, authenticationMethod: "local-os", roleRevision: "local-os-v1" }) });
42710
43889
  }
43890
+ function isWorkspaceId(id) {
43891
+ return /^[a-z][a-z0-9-]{2,63}$/.test(id);
43892
+ }
42711
43893
  function newWorkspaceId() {
42712
43894
  return `workspace-${randomUUID4().replace(/-/g, "").slice(0, 16)}`;
42713
43895
  }
@@ -45676,9 +46858,44 @@ function createLocalFwkReadService(cwd, opts) {
45676
46858
  function normalizeFwkResult(result) {
45677
46859
  return JSON.parse(JSON.stringify(result));
45678
46860
  }
46861
+ // src/sac/propose-from-session.ts
46862
+ init_guard();
46863
+ import { randomUUID as randomUUID10 } from "crypto";
46864
+ import { writeFile as writeFile25 } from "fs/promises";
46865
+ async function proposeFromSession(input) {
46866
+ const { cwd, workspaceId, kind, sessionRef } = input;
46867
+ const session = findSession(cwd, sessionRef);
46868
+ if (!session)
46869
+ return { ok: false, reason: "session-not-found", message: `no session matching "${sessionRef}" in this project` };
46870
+ const proposalId = `proposal-${randomUUID10().replace(/-/g, "").slice(0, 16)}`;
46871
+ const notePath = proposalNotePath(cwd, workspaceId, proposalId);
46872
+ const materializedNote = input.note === undefined ? undefined : prepareOutputForPersistence(await guardOutput({ cwd, content: input.note, target: "task", source: "generated", path: notePath }), input.note);
46873
+ if (materializedNote !== undefined && !materializedNote.allowed) {
46874
+ return { ok: false, reason: "note-refused", message: materializedNote.reason };
46875
+ }
46876
+ const persistedNote = materializedNote?.allowed ? materializedNote.content : undefined;
46877
+ const { service, wrapUpAuthority, authorizationServer } = createHarnessProposalLifecycleService(cwd, { workspaceId, ...persistedNote ? { note: persistedNote } : {} });
46878
+ const requestCorrelationId = randomUUID10();
46879
+ const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
46880
+ if (!actor)
46881
+ return { ok: false, reason: "no-actor", message: "trusted ActorContext is required" };
46882
+ const wrapUp = await wrapUpAuthority.issue({ actor, source: "session", sourceRef: sessionEvidenceRef(workspaceId, session.id) });
46883
+ const proposal = await service.create({
46884
+ request: undefined,
46885
+ requestCorrelationId,
46886
+ workspaceId,
46887
+ id: proposalId,
46888
+ proposalRevision: input.proposalRevision ?? "1",
46889
+ kind,
46890
+ wrapUp
46891
+ });
46892
+ if (persistedNote)
46893
+ await writeFile25(notePath, persistedNote, "utf8");
46894
+ return { ok: true, result: normalizeProposalLifecycleResult(proposal) };
46895
+ }
45679
46896
  // src/sac/collaboration-service.ts
45680
46897
  import { appendFile as appendFile9, mkdir as mkdir37, readFile as readFile75 } from "fs/promises";
45681
- import { randomUUID as randomUUID10 } from "crypto";
46898
+ import { randomUUID as randomUUID11 } from "crypto";
45682
46899
  import path145 from "path";
45683
46900
  class CollaborationServiceError extends Error {
45684
46901
  code;
@@ -45725,7 +46942,7 @@ class CollaborationService {
45725
46942
  const activity = this.validate({
45726
46943
  ...supplied,
45727
46944
  schemaVersion: "1.0",
45728
- id: `activity-${randomUUID10()}`,
46945
+ id: `activity-${randomUUID11()}`,
45729
46946
  workspaceId: manifest.id,
45730
46947
  actorSubject: actor.subject,
45731
46948
  occurredAt: (this.input.now ?? (() => new Date))().toISOString(),
@@ -45786,13 +47003,13 @@ import path147 from "path";
45786
47003
  // src/sac/machine-wrap-up.ts
45787
47004
  init_fs();
45788
47005
  init_slate();
45789
- import { createHash as createHash36, randomUUID as randomUUID12 } from "crypto";
47006
+ import { createHash as createHash36, randomUUID as randomUUID13 } from "crypto";
45790
47007
  import { mkdir as mkdir38 } from "fs/promises";
45791
47008
  import path146 from "path";
45792
47009
 
45793
47010
  // src/sac/workspace-resolve.ts
45794
47011
  init_redact();
45795
- import { randomUUID as randomUUID11 } from "crypto";
47012
+ import { randomUUID as randomUUID12 } from "crypto";
45796
47013
  var DEFAULT_MODEL_TURN_TIMEOUT_MS = 15000;
45797
47014
  var TOPIC_HINT_MAX_LENGTH = 200;
45798
47015
  function workspaceStore(cwd) {
@@ -45820,7 +47037,7 @@ async function resolveOrCreateWorkspace(input) {
45820
47037
  const store = workspaceStore(input.cwd);
45821
47038
  let existing;
45822
47039
  try {
45823
- const listed = await store.list({ request: undefined, requestCorrelationId: randomUUID11(), includeArchived: false });
47040
+ const listed = await store.list({ request: undefined, requestCorrelationId: randomUUID12(), includeArchived: false });
45824
47041
  existing = listed.filter((w) => w.status === "active").map((w) => ({ id: w.id, title: w.title, status: w.status }));
45825
47042
  } catch {
45826
47043
  return { ok: false, reason: "error" };
@@ -45886,7 +47103,7 @@ async function createWorkspace(store, title) {
45886
47103
  try {
45887
47104
  const workspace = await store.create({
45888
47105
  request: undefined,
45889
- requestCorrelationId: randomUUID11(),
47106
+ requestCorrelationId: randomUUID12(),
45890
47107
  id: newWorkspaceId(),
45891
47108
  title
45892
47109
  });
@@ -46121,7 +47338,7 @@ async function proposeOneGroup(params) {
46121
47338
  workspaceId: params.workspaceId,
46122
47339
  now: params.now
46123
47340
  });
46124
- const requestCorrelationId = randomUUID12();
47341
+ const requestCorrelationId = randomUUID13();
46125
47342
  const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
46126
47343
  if (!actor)
46127
47344
  throw new Error("trusted ActorContext is required for a machine wrap-up propose");
@@ -47360,6 +48577,12 @@ var HELP_GROUPS = [
47360
48577
  group: "External agents, ACP and MCP",
47361
48578
  summary: "Install, audit and uninstall Keryx's hooks and instructions in another coding agent (see also /integrate, unrelated)."
47362
48579
  },
48580
+ {
48581
+ kind: "cli",
48582
+ name: "herdr",
48583
+ group: "External agents, ACP and MCP",
48584
+ summary: "Register Keryx with the Herdr desktop app and herdr-core (opt-in install, remove, status)."
48585
+ },
47363
48586
  {
47364
48587
  kind: "cli",
47365
48588
  name: "serve-mcp",