atris 3.82.5 → 3.82.8
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/bin/atris.js +7 -2
- package/commands/security-review.js +18 -0
- package/commands/sentinel-bench.js +172 -0
- package/commands/sentinel-fix.js +243 -0
- package/commands/sentinel-powers.js +97 -11
- package/lib/security-audit.js +3 -0
- package/lib/sentinel-agents.js +75 -41
- package/lib/sentinel-bench.js +556 -0
- package/lib/sentinel-cloud.js +2 -1
- package/lib/sentinel-events.js +32 -10
- package/lib/sentinel-fix.js +777 -0
- package/lib/sentinel-guard-install.js +10 -4
- package/lib/sentinel-guard.js +142 -83
- package/lib/sentinel-improve.js +827 -0
- package/lib/sentinel-rules.js +108 -0
- package/lib/sentinel-rules.json +237 -0
- package/lib/sentinel-watch.js +940 -98
- package/package.json +1 -1
package/lib/security-audit.js
CHANGED
package/lib/sentinel-agents.js
CHANGED
|
@@ -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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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,
|