atris 3.82.4 → 3.82.7

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.
@@ -27,6 +27,60 @@ const AREA_LABEL = {
27
27
  mcp: 'mcp servers',
28
28
  hidden: 'hidden instructions',
29
29
  };
30
+ // The hand-written regular expressions below stay in code, out of the
31
+ // proposer's reach. The rules file (the agents part) holds only literal
32
+ // supplements the proposer may extend: extra override substrings (matched
33
+ // lowercase with plain includes) and extra secret environment names. They
34
+ // are data, never code. They are matched with string operations.
35
+ const RULES_FILE = path.join(__dirname, 'sentinel-rules.json');
36
+
37
+ const SECRET_KEY_NAME = /(?:^|_|-)(?:api[_-]?key|key|token|secret|password|passwd|pat|credential|credentials|auth)(?:$|_|-)/i;
38
+ const DOWNLOAD = '(?:curl|wget|fetch|iwr|Invoke-WebRequest|irm|Invoke-RestMethod)\\b';
39
+ const DOWNLOAD_RUN = [
40
+ new RegExp(`${DOWNLOAD}[^\\n|;&]*\\|\\s*(?:sudo\\s+)?(?:env\\s+)?(?:ba|z|da|k|fi)?sh\\b`, 'i'),
41
+ new RegExp(`${DOWNLOAD}[^\\n|;&]*\\|\\s*(?:sudo\\s+)?(?:python[0-9.]*|node|perl|ruby|php|iex|Invoke-Expression)\\b`, 'i'),
42
+ new RegExp(`(?:ba|z|k)?sh\\s+<\\(\\s*${DOWNLOAD}`, 'i'),
43
+ new RegExp(`(?:ba|z|k)?sh\\s+-c\\s+["']?\\$\\(\\s*${DOWNLOAD}`, 'i'),
44
+ new RegExp(`source\\s+<\\(\\s*${DOWNLOAD}`, 'i'),
45
+ ];
46
+ const EVAL_FETCHED = [
47
+ new RegExp(`\\beval\\s+["']?\\$\\(\\s*${DOWNLOAD}`, 'i'),
48
+ new RegExp(`\\beval\\s+["']?\`\\s*${DOWNLOAD}`, 'i'),
49
+ /\beval\s*\(\s*(?:await\s+)?\(?\s*(?:await\s+)?fetch\s*\(/i,
50
+ /\bexec\s*\(\s*(?:urllib\.request\.)?urlopen\s*\(/i,
51
+ /\bexec\s*\(\s*requests\.get\s*\(/i,
52
+ ];
53
+ const SKIP_FLAGS = /(?:^|\s)(?:--dangerously-skip-permissions|--dangerously-bypass-approvals-and-sandbox|--yolo|--permission-mode[ =]bypassPermissions|--approval-mode[ =]yolo|--allow-dangerously-skip-permissions)(?=\s|$|["'])/;
54
+ const OVERRIDE = [
55
+ /\b(?:ignore|disregard|forget|override|bypass)\s+(?:all\s+|any\s+|every\s+)?(?:of\s+)?(?:the\s+|your\s+|my\s+)?(?:previous|prior|above|earlier|preceding|original|system|other)\s+(?:instructions?|prompts?|rules|directions|guidelines|messages)\b/i,
56
+ /\byou\s+are\s+now\s+(?:in\s+)?(?:developer|dan|jailbreak|god)\s+mode\b/i,
57
+ /\b(?:do\s+not|don't|never)\s+(?:tell|inform|show)\s+the\s+user\s+(?:about|that|what|anything|this|you\b|we\b)/i,
58
+ /\b(?:do\s+not|don't|never)\s+mention\s+(?:this|it)\s+to\s+the\s+user\b/i,
59
+ /\bwithout\s+(?:telling|informing|alerting)\s+the\s+user\b/i,
60
+ /\bhide\s+this\s+from\s+the\s+user\b/i,
61
+ /\bnew\s+system\s+prompt\s*:/i,
62
+ ];
63
+ const COMMENT_DIRECTIVE = /\b(?:ignore\s+(?:the|all|any|previous|prior|above)|disregard|secretly|silently|do\s+not\s+tell|don't\s+tell|without\s+telling|curl|wget|exfiltrate|send\s+(?:the|all|any|your)\s+\w+\s+to|system\s+prompt|you\s+are\s+(?:now|an?|the)|you\s+must|always\s+run|run\s+the\s+following|execute\s+(?:the|this))\b/i;
64
+
65
+ // The literal supplements from the rules file. A proposer may add plain
66
+ // phrases and names here; the checks below match them with string ops.
67
+ function compileRules(raw) {
68
+ const a = (raw && raw.agents) || {};
69
+ const list = (v, name) => { if (v === undefined) return []; if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) throw new Error(`rules: agents.${name} must be a list of strings`); return v; };
70
+ return {
71
+ overrideSubstrings: list(a.override_substrings, 'override_substrings').map((x) => x.toLowerCase()),
72
+ secretEnvNames: new Set(list(a.secret_env_names, 'secret_env_names')),
73
+ };
74
+ }
75
+
76
+ let DEFAULT_RULES = null;
77
+ let ACTIVE_RULES = null;
78
+ function R() {
79
+ if (ACTIVE_RULES) return ACTIVE_RULES;
80
+ if (!DEFAULT_RULES) DEFAULT_RULES = compileRules(JSON.parse(fs.readFileSync(RULES_FILE, 'utf8')));
81
+ return DEFAULT_RULES;
82
+ }
83
+
30
84
  const MAX_FILE_BYTES = 512 * 1024;
31
85
  const MAX_FILES = 4000;
32
86
  const SKIP_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build', '.trash', '.Trash']);
@@ -158,7 +212,6 @@ function secretHits(text) {
158
212
  // "API_KEY": "abc..." in JSON, and API_KEY = "abc..." in TOML. Only keys
159
213
  // whose name says secret, and only values that look real (not $VAR, not a
160
214
  // placeholder, not short).
161
- const SECRET_KEY_NAME = /(?:^|_|-)(?:api[_-]?key|key|token|secret|password|passwd|pat|credential|credentials|auth)(?:$|_|-)/i;
162
215
  function envBlockHits(text) {
163
216
  let scan;
164
217
  try { scan = require('./security-scan'); } catch { return []; }
@@ -167,7 +220,7 @@ function envBlockHits(text) {
167
220
  let m;
168
221
  while ((m = re.exec(text))) {
169
222
  const [, key, value] = m;
170
- if (!SECRET_KEY_NAME.test(key)) continue;
223
+ if (!SECRET_KEY_NAME.test(key) && !R().secretEnvNames.has(key)) continue;
171
224
  if (/^\$|\$\{|^<|^\{\{|^env:|^op:\/\/|^\/|^~|^https?:\/\//i.test(value)) continue;
172
225
  if (/\s/.test(value)) continue;
173
226
  const q = scan.secretQuality(value, {}, m[0]);
@@ -179,21 +232,6 @@ function envBlockHits(text) {
179
232
 
180
233
  // Download piped into a shell or interpreter, process substitution of a
181
234
  // download, and eval of fetched content.
182
- const DOWNLOAD = '(?:curl|wget|fetch|iwr|Invoke-WebRequest|irm|Invoke-RestMethod)\\b';
183
- const DOWNLOAD_RUN = [
184
- new RegExp(`${DOWNLOAD}[^\\n|;&]*\\|\\s*(?:sudo\\s+)?(?:env\\s+)?(?:ba|z|da|k|fi)?sh\\b`, 'i'),
185
- new RegExp(`${DOWNLOAD}[^\\n|;&]*\\|\\s*(?:sudo\\s+)?(?:python[0-9.]*|node|perl|ruby|php|iex|Invoke-Expression)\\b`, 'i'),
186
- new RegExp(`(?:ba|z|k)?sh\\s+<\\(\\s*${DOWNLOAD}`, 'i'),
187
- new RegExp(`(?:ba|z|k)?sh\\s+-c\\s+["']?\\$\\(\\s*${DOWNLOAD}`, 'i'),
188
- new RegExp(`source\\s+<\\(\\s*${DOWNLOAD}`, 'i'),
189
- ];
190
- const EVAL_FETCHED = [
191
- new RegExp(`\\beval\\s+["']?\\$\\(\\s*${DOWNLOAD}`, 'i'),
192
- new RegExp(`\\beval\\s+["']?\`\\s*${DOWNLOAD}`, 'i'),
193
- /\beval\s*\(\s*(?:await\s+)?\(?\s*(?:await\s+)?fetch\s*\(/i,
194
- /\bexec\s*\(\s*(?:urllib\.request\.)?urlopen\s*\(/i,
195
- /\bexec\s*\(\s*requests\.get\s*\(/i,
196
- ];
197
235
 
198
236
  function downloadRunHits(text) {
199
237
  const out = [];
@@ -205,7 +243,6 @@ function downloadRunHits(text) {
205
243
  return out;
206
244
  }
207
245
 
208
- const SKIP_FLAGS = /(?:^|\s)(?:--dangerously-skip-permissions|--dangerously-bypass-approvals-and-sandbox|--yolo|--permission-mode[ =]bypassPermissions|--approval-mode[ =]yolo|--allow-dangerously-skip-permissions)(?=\s|$|["'])/;
209
246
 
210
247
  function flagHits(text) {
211
248
  const out = [];
@@ -239,36 +276,23 @@ function invisibleHits(text) {
239
276
  return out;
240
277
  }
241
278
 
242
- const OVERRIDE = [
243
- /\b(?:ignore|disregard|forget|override|bypass)\s+(?:all\s+|any\s+|every\s+)?(?:of\s+)?(?:the\s+|your\s+|my\s+)?(?:previous|prior|above|earlier|preceding|original|system|other)\s+(?:instructions?|prompts?|rules|directions|guidelines|messages)\b/i,
244
- /\byou\s+are\s+now\s+(?:in\s+)?(?:developer|dan|jailbreak|god)\s+mode\b/i,
245
- // Secrecy, not ordinary guidance: "do not tell the user about this", but
246
- // not "do not tell the user to restart".
247
- /\b(?:do\s+not|don't|never)\s+(?:tell|inform|show)\s+the\s+user\s+(?:about|that|what|anything|this|you\b|we\b)/i,
248
- /\b(?:do\s+not|don't|never)\s+mention\s+(?:this|it)\s+to\s+the\s+user\b/i,
249
- /\bwithout\s+(?:telling|informing|alerting)\s+the\s+user\b/i,
250
- /\bhide\s+this\s+from\s+the\s+user\b/i,
251
- /\bnew\s+system\s+prompt\s*:/i,
252
- ];
253
279
 
254
280
  function overrideHits(text) {
255
281
  const out = [];
282
+ const extra = R().overrideSubstrings;
256
283
  text.split('\n').forEach((line, i) => {
257
- for (const re of OVERRIDE) {
258
- const m = line.match(re);
259
- if (!m) continue;
260
- // Quoted or in code: a doc about prompt attacks, most likely.
261
- const at = m.index;
262
- const before = line.slice(0, at);
263
- const quoted = /["'`“‘]\s*$/.test(before) || (before.split('`').length % 2 === 0) || /^\s*>/.test(line);
264
- out.push({ line: i + 1, rule: quoted ? 'override-phrase-quoted' : 'override-phrase' });
265
- break;
266
- }
284
+ let at = -1;
285
+ for (const re of OVERRIDE) { const m = line.match(re); if (m) { at = m.index; break; } }
286
+ if (at === -1 && extra.length) { const low = line.toLowerCase(); for (const sub of extra) { const k = low.indexOf(sub); if (k !== -1) { at = k; break; } } }
287
+ if (at === -1) return;
288
+ // Quoted or in code: a doc about prompt attacks, most likely.
289
+ const before = line.slice(0, at);
290
+ const quoted = /["'`“‘]\s*$/.test(before) || (before.split('`').length % 2 === 0) || /^\s*>/.test(line);
291
+ out.push({ line: i + 1, rule: quoted ? 'override-phrase-quoted' : 'override-phrase' });
267
292
  });
268
293
  return out;
269
294
  }
270
295
 
271
- const COMMENT_DIRECTIVE = /\b(?:ignore\s+(?:the|all|any|previous|prior|above)|disregard|secretly|silently|do\s+not\s+tell|don't\s+tell|without\s+telling|curl|wget|exfiltrate|send\s+(?:the|all|any|your)\s+\w+\s+to|system\s+prompt|you\s+are\s+(?:now|an?|the)|you\s+must|always\s+run|run\s+the\s+following|execute\s+(?:the|this))\b/i;
272
296
  function hiddenCommentHits(text) {
273
297
  const out = [];
274
298
  // Comments inside fenced code blocks are examples, not hidden text.
@@ -500,7 +524,15 @@ function grade(findings) {
500
524
  return 'pass';
501
525
  }
502
526
 
503
- function runAgentCheck(root, { home = false, homeDir = os.homedir() } = {}) {
527
+ // rules: a rules object to judge instead of the file beside this one.
528
+ function runAgentCheck(root, { home = false, homeDir = os.homedir(), rules = null } = {}) {
529
+ if (!rules) return runAgentCheckWith(root, { home, homeDir });
530
+ const prev = ACTIVE_RULES;
531
+ ACTIVE_RULES = compileRules(rules);
532
+ try { return runAgentCheckWith(root, { home, homeDir }); } finally { ACTIVE_RULES = prev; }
533
+ }
534
+
535
+ function runAgentCheckWith(root, { home, homeDir }) {
504
536
  const targets = collectTargets(root, { home, homeDir });
505
537
  const findings = [];
506
538
  for (const t of targets) {
@@ -576,6 +608,8 @@ function renderReport(result, { color = false } = {}) {
576
608
  }
577
609
 
578
610
  module.exports = {
611
+ compileRules,
612
+ RULES_FILE,
579
613
  RULES,
580
614
  checkFile,
581
615
  runAgentCheck,