atris 3.80.0 → 3.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/auth.js +2 -2
- package/commands/computer.js +165 -2412
- package/commands/security-review.js +5 -1
- package/commands/site-deploy.js +5 -1
- package/lib/computer-core.js +2447 -0
- package/lib/security-audit-classes.js +20 -2
- package/lib/security-audit.js +31 -1
- package/package.json +9 -2
- package/utils/auth.js +18 -4
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// control that should stop them, and the concrete result across the boundary.
|
|
9
9
|
|
|
10
10
|
const CODE_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.rb', '.go', '.java', '.php', '.rs', '.sh', '.bash', '.zsh']);
|
|
11
|
-
const WEB_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.vue', '.svelte']);
|
|
11
|
+
const WEB_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.vue', '.svelte', '.erb', '.rb']);
|
|
12
12
|
const WORKFLOW_RE = /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
13
13
|
|
|
14
14
|
const CLASSES = [
|
|
@@ -23,6 +23,9 @@ const CLASSES = [
|
|
|
23
23
|
/\bsubprocess\.(?:run|call|Popen|check_output|check_call)\s*\(/,
|
|
24
24
|
/\bos\.(?:system|popen)\s*\(/,
|
|
25
25
|
/\bshell\s*:\s*true\b/,
|
|
26
|
+
// Ruby, Go, Java
|
|
27
|
+
/\b(?:system|spawn|exec)\s*\(?\s*["'][^"'\n]*#\{|%x[{(\[]|\bIO\.popen\b|\bOpen3\.\w+|`[^`\n]*#\{/,
|
|
28
|
+
/\bexec\.Command(?:Context)?\s*\(|\bRuntime\.getRuntime\(\)\.exec\b|\bnew\s+ProcessBuilder\b/,
|
|
26
29
|
],
|
|
27
30
|
guide: [
|
|
28
31
|
'Invariant: a lower-trust value never becomes shell syntax, a program name, or an option the caller did not intend.',
|
|
@@ -44,6 +47,7 @@ const CLASSES = [
|
|
|
44
47
|
/\brequire\s*\(\s*(?!['"`])[^)]/,
|
|
45
48
|
/\bimport\s*\(\s*(?!['"`])[^)]/,
|
|
46
49
|
/\b(?:exec|compile)\s*\(\s*(?!['"])/,
|
|
50
|
+
/\b(?:instance_eval|class_eval|module_eval|instance_exec)\b|\.(?:constantize|safe_constantize)\b|\bconst_get\s*\(|\.(?:public_)?send\s*\(\s*(?![:'\"])/,
|
|
47
51
|
],
|
|
48
52
|
guide: [
|
|
49
53
|
'Invariant: text from a lower-trust source is never run as code, and a module path is never chosen by it.',
|
|
@@ -61,6 +65,7 @@ const CLASSES = [
|
|
|
61
65
|
/\b(?:readFileSync|writeFileSync|readFile|writeFile|appendFileSync|createReadStream|createWriteStream|unlinkSync|rmSync|rmdirSync|renameSync|copyFileSync|symlinkSync|cpSync)\s*\(\s*(?!['"`])/,
|
|
62
66
|
/\bpath\.(?:join|resolve)\s*\([^)]*\b(?:req|params|query|body|name|slug|id|input|args?|file|target)\b/,
|
|
63
67
|
/\bextract(?:All)?\s*\(|\btar\b.*\bx\b|\bunzip\b/,
|
|
68
|
+
/\bFile\.(?:read|write|open|binread|binwrite|delete|unlink|rename|symlink)\s*\(\s*(?!['\"])|\bFileUtils\.\w+|\bsend_file\b|\bos\.(?:Open|OpenFile|Create|ReadFile|WriteFile|Remove(?:All)?)\s*\(|\bfilepath\.Join\s*\(/,
|
|
64
69
|
],
|
|
65
70
|
guide: [
|
|
66
71
|
'Invariant: a path built from a lower-trust value stays inside the directory the code means, after normalization and symlink resolution.',
|
|
@@ -81,6 +86,7 @@ const CLASSES = [
|
|
|
81
86
|
/\baxios(?:\.\w+)?\s*\(/,
|
|
82
87
|
/\brequests\.(?:get|post|put|patch|delete|request)\s*\(/,
|
|
83
88
|
/\burllib\.request\.urlopen\s*\(/,
|
|
89
|
+
/\bNet::HTTP\b|\bHTTParty\b|\bFaraday\b|\bURI\.open\b|\bRestClient\b|\bhttp\.(?:Get|Post|Head|NewRequest(?:WithContext)?)\s*\(/,
|
|
84
90
|
],
|
|
85
91
|
guide: [
|
|
86
92
|
'Invariant: a lower-trust value cannot pick the host, scheme, or port of a request made with this process\'s network position or credentials.',
|
|
@@ -100,6 +106,10 @@ const CLASSES = [
|
|
|
100
106
|
/\bcreateServer\s*\(/,
|
|
101
107
|
/\.listen\s*\(\s*(?:\d|port|PORT)/,
|
|
102
108
|
/\b(?:req|request)\.(?:headers|query|params|body)\b/,
|
|
109
|
+
/\b(?:before_action|skip_before_action|authorize!?|current_user|policy_scope|load_and_authorize_resource)\b|\bparams\[/,
|
|
110
|
+
/\bdef\s+(?:self\.)?(?:authorized\?|visible\?|scope_items\b|resolve\b)|\bfield\s+:\w+|\bargument\s+:\w+/,
|
|
111
|
+
/^\s*(?:get|post|put|patch|delete|match|resources?)\s+['\":]/,
|
|
112
|
+
/\bhttp\.HandleFunc\s*\(|\b\w+\.(?:GET|POST|PUT|PATCH|DELETE|Handle|HandleFunc)\s*\(\s*\"\//,
|
|
103
113
|
],
|
|
104
114
|
guide: [
|
|
105
115
|
'Invariant: every action that reads or changes another principal\'s data, or spends shared resources, checks who is asking and whether they may.',
|
|
@@ -119,6 +129,7 @@ const CLASSES = [
|
|
|
119
129
|
/os\.environ(?:\.get)?\s*[[(]\s*['"][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)/,
|
|
120
130
|
/\b(?:authorization|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token)\b/i,
|
|
121
131
|
/\bcredentials?\.json\b|\bkeychain\b|\.netrc\b/i,
|
|
132
|
+
/\bENV(?:\.fetch)?\s*[\[(]\s*['\"][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|\bos\.Getenv\s*\(\s*\"[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)/,
|
|
122
133
|
],
|
|
123
134
|
guide: [
|
|
124
135
|
'Invariant: a credential reaches only the party it was issued for, and never lands in logs, errors, receipts, prompts, child process environments, or files others can read.',
|
|
@@ -155,6 +166,7 @@ const CLASSES = [
|
|
|
155
166
|
/\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^;\n]*(?:\$\{|['"`]\s*\+|%s|\.format\(|\bf["'])/i,
|
|
156
167
|
/\.(?:query|execute|raw|exec)\s*\(\s*(?:`[^`]*\$\{|f["']|['"][^'"]*['"]\s*\+)/,
|
|
157
168
|
/\$where\b|\.find\s*\(\s*(?:req|request)\./,
|
|
169
|
+
/\.(?:where|order|group|having|joins|select|pluck|find_by_sql|exec_query|execute)\s*\(\s*\"[^\"\n]*#\{|\.order\s*\(\s*params\b|\.(?:Query|QueryRow|Exec)(?:Context)?\s*\([^)\n]*fmt\.Sprintf/,
|
|
158
170
|
],
|
|
159
171
|
guide: [
|
|
160
172
|
'Invariant: lower-trust values reach a query only as bound parameters, never as query text, identifiers, or operators.',
|
|
@@ -175,6 +187,7 @@ const CLASSES = [
|
|
|
175
187
|
/\bv-html\b|\{@html\b/,
|
|
176
188
|
/\bpostMessage\s*\(|addEventListener\s*\(\s*['"]message['"]/,
|
|
177
189
|
/\bhref\s*=\s*\{?\s*(?)/,
|
|
190
|
+
/\.html_safe\b|\braw\s*\(|<%==/,
|
|
178
191
|
],
|
|
179
192
|
guide: [
|
|
180
193
|
'Invariant: text from another user or an outside source renders as text, never as markup or script, and cross-window messages are accepted only from expected origins.',
|
|
@@ -194,6 +207,7 @@ const CLASSES = [
|
|
|
194
207
|
/\bunserialize\s*\(|\bMarshal\.load\b/,
|
|
195
208
|
/\b__proto__\b|\bconstructor\s*\[\s*['"]prototype/,
|
|
196
209
|
/\bObject\.assign\s*\(\s*\{\}\s*,\s*(?:req|body|input|data|JSON\.parse)/,
|
|
210
|
+
/\bYAML\.(?:load|unsafe_load)\s*\(|\bPsych\.(?:load|unsafe_load)\b|\bOj\.load\b|\bnew\s+ObjectInputStream\b|\bgob\.NewDecoder\b/,
|
|
197
211
|
],
|
|
198
212
|
guide: [
|
|
199
213
|
'Invariant: parsing lower-trust input never runs code, never changes shared object prototypes, and never produces an object the next step trusts more than the input.',
|
|
@@ -212,6 +226,8 @@ const CLASSES = [
|
|
|
212
226
|
/createHash\s*\(\s*['"](?:md5|sha1)['"]/,
|
|
213
227
|
/\b\w*(?:token|secret|signature|hmac|digest)\w*\s*(?:===|!==|==|!=)\s*\w*(?:token|secret|signature|hmac|digest|expected)/i,
|
|
214
228
|
/\bjwt\.(?:decode|verify)\s*\(|algorithms?\s*:\s*\[?\s*['"]none['"]/,
|
|
229
|
+
/\bOpenSSL::(?:Cipher|PKey|HMAC|Digest|KDF)\b|\bDigest::(?:MD5|SHA1)\b|\bSecureRandom\b|\bsecure_compare\b|\bJWT\.decode\b/,
|
|
230
|
+
/\bcrypto\.(?:createCipheriv|createDecipheriv|createSign|createVerify|timingSafeEqual)\b|\b(?:decrypt|encrypt|verify_signature|verify_raw|sign_raw)\s*[(]|\bcrypto\/(?:aes|cipher|rsa|ecdsa|ed25519|hmac|subtle)\b/,
|
|
215
231
|
],
|
|
216
232
|
guide: [
|
|
217
233
|
'Invariant: secrets, session ids, and one-time codes are unguessable, and checks of them do not leak timing or accept forged values.',
|
|
@@ -225,7 +241,7 @@ const CLASSES = [
|
|
|
225
241
|
boundary: 'install, build, and release',
|
|
226
242
|
weight: 8,
|
|
227
243
|
exts: null,
|
|
228
|
-
files: (rel) => WORKFLOW_RE.test(rel) || /(?:^|\/)package\.json$/.test(rel) || /(?:^|\/)(?:Dockerfile|Makefile|install\.sh|setup\.py|pyproject\.toml)$/.test(rel),
|
|
244
|
+
files: (rel) => WORKFLOW_RE.test(rel) || /(?:^|\/)package\.json$/.test(rel) || /(?:^|\/)(?:Dockerfile|Makefile|install\.sh|setup\.py|pyproject\.toml|Gemfile|Rakefile|go\.mod)$/.test(rel) || /\.gemspec$/.test(rel),
|
|
229
245
|
detect: [
|
|
230
246
|
/"(?:preinstall|install|postinstall|prepare|prepublish)"\s*:/,
|
|
231
247
|
/\bpull_request_target\b|\bworkflow_run\b/,
|
|
@@ -233,6 +249,7 @@ const CLASSES = [
|
|
|
233
249
|
/\bcurl\b[^|\n]*\|\s*(?:ba|z)?sh\b|\bwget\b[^|\n]*\|\s*(?:ba|z)?sh\b/,
|
|
234
250
|
/\$\{\{\s*github\.event\.(?:issue|pull_request|comment|head_commit|review)\b/,
|
|
235
251
|
/\bsecrets\.[A-Z0-9_]+/,
|
|
252
|
+
/^\s*(?:gem|source)\s+['\"][^'\"]+['\"]\s*,\s*(?:git|github|path):|\.add_(?:runtime_)?dependency\b|\bspec\.(?:files|executables)\b/,
|
|
236
253
|
],
|
|
237
254
|
guide: [
|
|
238
255
|
'Invariant: code from a lower-trust contributor or dependency cannot run with release credentials, change what ships, or execute on a user\'s machine at install time.',
|
|
@@ -277,6 +294,7 @@ const CANDIDATE_GATE = [
|
|
|
277
294
|
'Propose "confirmed" only when the trace is complete, the impact crosses a stated boundary, every condition is listed, and no layer in source prevents it.',
|
|
278
295
|
'Do not strengthen a crash into code execution, ordinary work into an outage, or a same-person action into privilege gain.',
|
|
279
296
|
'If a decisive fact is outside source (deployment, proxy, provider setting, who can write a file), propose "needs_validation" with the exact missing fact. It gets no severity.',
|
|
297
|
+
'Before calling a library version or behavior unknown, read what is installed in the target: lockfiles, venv or .venv site-packages, node_modules, vendor folders. A fact on disk is not outside source.',
|
|
280
298
|
'A missing best practice with no affected principal or resource goes in "hardening", not in candidates. A candidate disproved by source is dropped.',
|
|
281
299
|
'The fingerprint names the root cause, like "lib/foo.js:runHook/shell-from-task-title". No line numbers, no severity, no verdict in it.',
|
|
282
300
|
'Return an empty candidates array when nothing survives these rules. Most units should end "covered".',
|
package/lib/security-audit.js
CHANGED
|
@@ -761,6 +761,7 @@ function verifierPrompt({ candidate, cls }) {
|
|
|
761
761
|
'Ask who controls the input. If it is the same person who runs the tool, it is self-impact, so reject it.',
|
|
762
762
|
'"confirmed": the full trace holds, the impact crosses the stated boundary, and nothing in source prevents it. Fix the severity if it is overstated; overall can never exceed impact.',
|
|
763
763
|
'"needs_validation": the trace holds in source, but a decisive fact is outside it. Name that fact exactly and give a safe check the owner can run.',
|
|
764
|
+
'A library version or behavior is not outside source when the target has it installed. Read the lockfile, the venv or .venv site-packages, or node_modules before sending it to the owner.',
|
|
764
765
|
'"rejected": the source disproves it. Name the line that does.',
|
|
765
766
|
'Use "corrected" only for fields you would change. Read-only: do not run the target or touch the network.',
|
|
766
767
|
]),
|
|
@@ -1163,6 +1164,17 @@ function resolveOutDir(root, { out, repo }) {
|
|
|
1163
1164
|
return { dir: path.join(outRoot, `run-${next}`), outRoot };
|
|
1164
1165
|
}
|
|
1165
1166
|
|
|
1167
|
+
// Same function in the fingerprint and sinks within a few lines of each other:
|
|
1168
|
+
// one defect, even when two hunters filed it under different classes.
|
|
1169
|
+
function sameRootCause(a, b) {
|
|
1170
|
+
const scope = (fp) => String(fp || '').split('/').slice(0, -1).join('/');
|
|
1171
|
+
const sink = (c) => (Array.isArray(c.trace) && c.trace.length ? c.trace[c.trace.length - 1] : null);
|
|
1172
|
+
const sa = sink(a);
|
|
1173
|
+
const sb = sink(b);
|
|
1174
|
+
if (!sa || !sb || !scope(a.fingerprint) || scope(a.fingerprint) !== scope(b.fingerprint)) return false;
|
|
1175
|
+
return sa.file === sb.file && Math.abs(Number(sa.line) - Number(sb.line)) <= 3;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1166
1178
|
function budgetPlan(profile, total) {
|
|
1167
1179
|
const critics = profile === 'quick' ? 1 : 2;
|
|
1168
1180
|
const verifyReserve = Math.max(1, Math.ceil(0.3 * (total - critics)));
|
|
@@ -1256,7 +1268,11 @@ async function runAudit(opts = {}) {
|
|
|
1256
1268
|
|
|
1257
1269
|
const plan = budgetPlan(profile, total);
|
|
1258
1270
|
const plannedCount = ledger.filter((u) => u.status === 'planned').length;
|
|
1259
|
-
|
|
1271
|
+
// Hunters take one class in one subsystem at a time, so count batches per group.
|
|
1272
|
+
const groupSizes = new Map();
|
|
1273
|
+
for (const u of ledger) if (u.status === 'planned') { const k = `${u.attack_class}\0${u.subsystem}`; groupSizes.set(k, (groupSizes.get(k) || 0) + 1); }
|
|
1274
|
+
const huntersNeeded = [...groupSizes.values()].reduce((n, size) => n + Math.ceil(size / UNITS_PER_HUNTER[profile]), 0);
|
|
1275
|
+
meta.plan = { ...plan, planned_units: plannedCount, units_per_hunter: UNITS_PER_HUNTER[profile], hunters_needed: huntersNeeded };
|
|
1260
1276
|
|
|
1261
1277
|
const pendingRecords = [];
|
|
1262
1278
|
const finish = (status, reason, records = [], carriedPending = []) => {
|
|
@@ -1378,9 +1394,23 @@ async function runAudit(opts = {}) {
|
|
|
1378
1394
|
const pending = [];
|
|
1379
1395
|
const toVerify = [];
|
|
1380
1396
|
const seenFp = new Set();
|
|
1397
|
+
const kept = [];
|
|
1381
1398
|
for (const cand of queue) {
|
|
1382
1399
|
if (seenFp.has(cand.fingerprint)) continue;
|
|
1383
1400
|
seenFp.add(cand.fingerprint);
|
|
1401
|
+
const twin = !cand._carried && kept.find((k) => !k._carried && sameRootCause(k, cand));
|
|
1402
|
+
if (twin) {
|
|
1403
|
+
// Two hunters named one root cause from different classes. Check it once
|
|
1404
|
+
// and point the twin's areas at the surviving fingerprint.
|
|
1405
|
+
twin.coverage_ids = [...new Set([...(twin.coverage_ids || []), ...(cand.coverage_ids || [])])];
|
|
1406
|
+
for (const u of ledger) {
|
|
1407
|
+
if (Array.isArray(u.result_fingerprints) && u.result_fingerprints.includes(cand.fingerprint)) {
|
|
1408
|
+
u.result_fingerprints = [...new Set(u.result_fingerprints.map((fp) => (fp === cand.fingerprint ? twin.fingerprint : fp)))];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
kept.push(cand);
|
|
1384
1414
|
if (!cand.attack_class || !CLASS_BY_ID.has(cand.attack_class)) cand.attack_class = cand._unit_class || 'command-injection';
|
|
1385
1415
|
const errs = citationErrors(root, cand);
|
|
1386
1416
|
if (errs.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.80.
|
|
3
|
+
"version": "3.80.1",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
5
|
"mcpName": "io.github.atrislabs/atris",
|
|
6
6
|
"main": "bin/atris.js",
|
|
@@ -91,7 +91,14 @@
|
|
|
91
91
|
"developer-tools",
|
|
92
92
|
"cli",
|
|
93
93
|
"codebase-navigation",
|
|
94
|
-
"ai-agents"
|
|
94
|
+
"ai-agents",
|
|
95
|
+
"agent-memory",
|
|
96
|
+
"ai-memory",
|
|
97
|
+
"llm-memory",
|
|
98
|
+
"project-memory",
|
|
99
|
+
"context-graph",
|
|
100
|
+
"knowledge-graph",
|
|
101
|
+
"memory"
|
|
95
102
|
],
|
|
96
103
|
"author": "Keshav Rao (atrislabs)",
|
|
97
104
|
"license": "MIT",
|
package/utils/auth.js
CHANGED
|
@@ -40,9 +40,11 @@ let inputIndex = 0;
|
|
|
40
40
|
/**
|
|
41
41
|
* Prompt the user for input, handling both TTY and piped input.
|
|
42
42
|
* @param {string} question - Prompt text to display
|
|
43
|
+
* @param {{secret?: boolean}} [opts] - secret: never print the answer back
|
|
43
44
|
* @returns {Promise<string>} User's input
|
|
44
45
|
*/
|
|
45
|
-
function promptUser(question) {
|
|
46
|
+
function promptUser(question, opts = {}) {
|
|
47
|
+
const shown = (answer) => (opts.secret ? '' : answer);
|
|
46
48
|
// If stdin is not a TTY (piped input), read all lines upfront
|
|
47
49
|
if (!process.stdin.isTTY && inputLines.length === 0 && !sharedRl) {
|
|
48
50
|
return new Promise((resolve) => {
|
|
@@ -57,7 +59,7 @@ function promptUser(question) {
|
|
|
57
59
|
inputLines = data.trim().split('\n');
|
|
58
60
|
process.stdout.write(question);
|
|
59
61
|
const answer = inputLines[inputIndex++] || '';
|
|
60
|
-
console.log(answer);
|
|
62
|
+
console.log(shown(answer));
|
|
61
63
|
resolve(answer.trim());
|
|
62
64
|
});
|
|
63
65
|
process.stdin.resume();
|
|
@@ -69,22 +71,34 @@ function promptUser(question) {
|
|
|
69
71
|
return new Promise((resolve) => {
|
|
70
72
|
process.stdout.write(question);
|
|
71
73
|
const answer = inputLines[inputIndex++] || '';
|
|
72
|
-
console.log(answer);
|
|
74
|
+
console.log(shown(answer));
|
|
73
75
|
resolve(answer.trim());
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
// Interactive TTY mode - create readline per prompt
|
|
80
|
+
// A secret typed at a terminal must stay hidden even when stdout is a file:
|
|
81
|
+
// terminal mode turns off the terminal's own echo, and the prompt goes to
|
|
82
|
+
// whichever stream is still the screen.
|
|
83
|
+
const secretOut = opts.secret && !process.stdout.isTTY && process.stderr.isTTY ? process.stderr : process.stdout;
|
|
78
84
|
const rl = readline.createInterface({
|
|
79
85
|
input: process.stdin,
|
|
80
|
-
output:
|
|
86
|
+
output: secretOut,
|
|
87
|
+
...(opts.secret ? { terminal: Boolean(process.stdin.isTTY) } : {}),
|
|
81
88
|
});
|
|
82
89
|
|
|
90
|
+
// Secret answers: let the prompt through, then swallow the typed echo.
|
|
91
|
+
let muted = false;
|
|
92
|
+
if (opts.secret) {
|
|
93
|
+
rl._writeToOutput = (text) => { if (!muted) rl.output.write(text); };
|
|
94
|
+
}
|
|
83
95
|
return new Promise((resolve) => {
|
|
84
96
|
rl.question(question, (answer) => {
|
|
97
|
+
if (muted) rl.output.write('\n');
|
|
85
98
|
rl.close();
|
|
86
99
|
resolve(answer.trim());
|
|
87
100
|
});
|
|
101
|
+
muted = Boolean(opts.secret);
|
|
88
102
|
});
|
|
89
103
|
}
|
|
90
104
|
|