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.
- package/bin/atris.js +3 -0
- package/commands/security-review.js +165 -17
- package/commands/sentinel-bench.js +172 -0
- package/commands/sentinel-fix.js +243 -0
- package/lib/security-audit.js +529 -13
- package/lib/sentinel-agents.js +75 -41
- package/lib/sentinel-bench.js +556 -0
- package/lib/sentinel-cloud.js +12 -6
- package/lib/sentinel-events.js +112 -1
- package/lib/sentinel-fix.js +777 -0
- package/lib/sentinel-guard-install.js +10 -4
- package/lib/sentinel-guard.js +168 -89
- package/lib/sentinel-improve.js +823 -0
- package/lib/sentinel-rules.js +108 -0
- package/lib/sentinel-rules.json +237 -0
- package/lib/sentinel-watch.js +12 -6
- package/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris sentinel fix [finding-id|--all-open] [path]
|
|
4
|
+
//
|
|
5
|
+
// Turns a confirmed sentinel finding into a reviewed fix on its own branch.
|
|
6
|
+
// The work lives in lib/sentinel-fix.js; this file reads flags, prices atris
|
|
7
|
+
// model calls before any spend, prints the landing and sends one event.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function tildePath(p, home = require('os').homedir()) {
|
|
13
|
+
return p && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseFixArgs(argv) {
|
|
17
|
+
const opts = { positionals: [], allOpen: false, verify: null, builder: null, verifier: null, engine: null, yes: false, pr: false, upload: true, timeoutMs: null };
|
|
18
|
+
const takes = (i, flag) => {
|
|
19
|
+
if (argv[i + 1] === undefined || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
20
|
+
return argv[i + 1];
|
|
21
|
+
};
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, null];
|
|
25
|
+
const value = () => (inline !== null ? inline : takes(i++, flag));
|
|
26
|
+
if (flag === '--all-open') opts.allOpen = true;
|
|
27
|
+
else if (flag === '--verify') opts.verify = value();
|
|
28
|
+
else if (flag === '--builder') opts.builder = value();
|
|
29
|
+
else if (flag === '--verifier') opts.verifier = value();
|
|
30
|
+
else if (flag === '--engine') opts.engine = value();
|
|
31
|
+
else if (flag === '--yes' || flag === '-y') opts.yes = true;
|
|
32
|
+
else if (flag === '--pr') opts.pr = true;
|
|
33
|
+
else if (flag === '--no-upload') opts.upload = false;
|
|
34
|
+
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
35
|
+
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
36
|
+
else opts.positionals.push(arg);
|
|
37
|
+
}
|
|
38
|
+
if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
|
|
39
|
+
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
40
|
+
if (opts.verify !== null && !opts.verify.trim()) throw new Error('--verify needs a command');
|
|
41
|
+
// fix <id> <path>, fix <path>, fix <id>, fix --all-open <path>
|
|
42
|
+
const [a, b] = opts.positionals;
|
|
43
|
+
if (opts.positionals.length > 2) throw new Error('give at most a finding id and a path');
|
|
44
|
+
if (opts.allOpen) {
|
|
45
|
+
if (opts.positionals.length > 1) throw new Error('--all-open takes only a path');
|
|
46
|
+
opts.path = a || null;
|
|
47
|
+
} else if (opts.positionals.length === 2) {
|
|
48
|
+
opts.id = a;
|
|
49
|
+
opts.path = b;
|
|
50
|
+
} else if (a !== undefined) {
|
|
51
|
+
let isDir = false;
|
|
52
|
+
try { isDir = fs.statSync(a).isDirectory(); } catch {}
|
|
53
|
+
if (isDir) opts.path = a; else opts.id = a;
|
|
54
|
+
}
|
|
55
|
+
return opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printFixHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
atris sentinel fix, a reviewed fix for a confirmed finding
|
|
61
|
+
|
|
62
|
+
atris sentinel fix list confirmed findings you can fix
|
|
63
|
+
atris sentinel fix <id> [path] fix one finding on its own branch
|
|
64
|
+
atris sentinel fix --all-open [path] fix every finding with no fix branch yet
|
|
65
|
+
atris sentinel fix <id> --verify "make test"
|
|
66
|
+
run this test command (default: npm test,
|
|
67
|
+
pytest or go test, whichever the repo uses)
|
|
68
|
+
atris sentinel fix <id> --builder claude --verifier codex
|
|
69
|
+
which engine writes the fix and which checks it
|
|
70
|
+
atris sentinel fix <id> --engine atris atris models write and check, after a price
|
|
71
|
+
atris sentinel fix <id> --pr push the branch and open a pull request
|
|
72
|
+
|
|
73
|
+
it needs a clean working tree. the fix is built in a temp copy on a new
|
|
74
|
+
branch named sentinel/fix-<id> made from the branch you are on. your own
|
|
75
|
+
checkout and branch are never changed. nothing is merged. nothing is
|
|
76
|
+
pushed unless you add --pr.
|
|
77
|
+
|
|
78
|
+
builder and checker may be codex, claude or an atris model. atris models
|
|
79
|
+
always show a price and wait for a yes.
|
|
80
|
+
|
|
81
|
+
a fix is committed only when all three hold:
|
|
82
|
+
the new test fails on the old code
|
|
83
|
+
the full test suite passes with the fix
|
|
84
|
+
a second engine reads the fixed source and finds the attack no longer works
|
|
85
|
+
|
|
86
|
+
the new test must fail on a real failed check. a missing module, a syntax
|
|
87
|
+
error or a timeout does not count. otherwise nothing is committed and the
|
|
88
|
+
attempt is saved as a patch under ~/.atris/sentinel/fixes.
|
|
89
|
+
|
|
90
|
+
the new tests run with a temp home, no tokens or keys in the environment
|
|
91
|
+
and push turned off for the temp copy. they still run as you and are not a
|
|
92
|
+
sandbox. read the new test before you run it on code you do not trust.
|
|
93
|
+
`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function askYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const rl = require('readline').createInterface({ input, output });
|
|
100
|
+
rl.question(question, (answer) => { rl.close(); resolve(/^\s*y(es)?\s*$/i.test(String(answer || ''))); });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sevOf(r) {
|
|
105
|
+
return (r.severity && r.severity.overall) || 'unrated';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function fixCommand(argv, deps = {}) {
|
|
109
|
+
if (argv[0] === 'help' || argv.includes('--help') || argv.includes('-h')) return printFixHelp();
|
|
110
|
+
const fix = deps.fixLib || require('../lib/sentinel-fix');
|
|
111
|
+
const env = deps.env || process.env;
|
|
112
|
+
let opts;
|
|
113
|
+
try { opts = parseFixArgs(argv); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
114
|
+
|
|
115
|
+
const start = path.resolve(opts.path || deps.root || process.cwd());
|
|
116
|
+
const top = fix.git(start, ['rev-parse', '--show-toplevel'], { allowFail: true });
|
|
117
|
+
if (!top) { console.error(` ${tildePath(start)} is not a git repository. a fix needs a branch to go on`); return 2; }
|
|
118
|
+
const root = top;
|
|
119
|
+
|
|
120
|
+
const { records } = fix.confirmedFindings(root);
|
|
121
|
+
if (!records.length) { console.log('\n no confirmed findings for this repository. run atris sentinel first\n'); return 0; }
|
|
122
|
+
|
|
123
|
+
if (!opts.id && !opts.allOpen) {
|
|
124
|
+
console.log('\n confirmed findings you can fix\n');
|
|
125
|
+
for (const r of records) {
|
|
126
|
+
const has = fix.branchExists(root, fix.branchFor(r));
|
|
127
|
+
console.log(` ${fix.shortId(r.fingerprint)} ${sevOf(r).padEnd(8)} ${r.title}${has ? ' (fix branch exists)' : ''}`);
|
|
128
|
+
}
|
|
129
|
+
console.log(`\n fix one with: atris sentinel fix ${fix.shortId(records[0].fingerprint)}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let targets;
|
|
134
|
+
if (opts.allOpen) {
|
|
135
|
+
targets = records.filter((r) => !fix.branchExists(root, fix.branchFor(r)));
|
|
136
|
+
if (!targets.length) { console.log('\n every confirmed finding already has a fix branch\n'); return 0; }
|
|
137
|
+
} else {
|
|
138
|
+
const r = fix.matchFinding(records, opts.id);
|
|
139
|
+
if (!r) { console.error(` no confirmed finding matches ${opts.id}. run atris sentinel fix to list them`); return 2; }
|
|
140
|
+
targets = [r];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (fix.changedPaths(root).length) { console.error(' this repository has uncommitted changes. commit or stash them first so the fix branch holds only the fix'); return 2; }
|
|
144
|
+
const from = fix.git(root, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFail: true });
|
|
145
|
+
if (!from || from === 'HEAD') { console.error(' check out a branch first. a fix branch is made from the branch you are on'); return 2; }
|
|
146
|
+
|
|
147
|
+
const testCommand = opts.verify || fix.detectTestCommand(root);
|
|
148
|
+
if (!testCommand) { console.error(' no test command found. name one with --verify "<command>"'); return 2; }
|
|
149
|
+
|
|
150
|
+
let engines;
|
|
151
|
+
try { engines = fix.resolveFixEngines({ engine: opts.engine, builder: opts.builder, verifier: opts.verifier, env, available: deps.available }); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
152
|
+
|
|
153
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
154
|
+
const hosted = fix.isHosted(engines.builder) || fix.isHosted(engines.verifier);
|
|
155
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
156
|
+
let hostedRun = deps.hostedRun || null;
|
|
157
|
+
let allowed = new Set();
|
|
158
|
+
if (hosted) {
|
|
159
|
+
if (!token) { console.error(' fixes on atris models need a login. run atris login first'); return 2; }
|
|
160
|
+
allowed = require('../lib/security-audit').hostedAllowedFiles(root, []);
|
|
161
|
+
const rows = [];
|
|
162
|
+
if (fix.isHosted(engines.builder)) {
|
|
163
|
+
const sizes = targets.map((r) => Buffer.byteLength(fix.builderPrompt(r, { testCommand, hosted: true, fileList: [...allowed].slice(0, 400), source: fix.hostedSourceBlock(root, fix.citedFiles(r), allowed, 64 * 1024) })));
|
|
164
|
+
const max = Math.max(4096, ...sizes) + 1024;
|
|
165
|
+
rows.push({ role: 'builder', model: engines.builder, calls: targets.length, input_tokens: max, output_tokens: fix.HOSTED_OUTPUT_TOKENS.builder, max_input_bytes: max });
|
|
166
|
+
}
|
|
167
|
+
if (fix.isHosted(engines.verifier)) rows.push({ role: 'verifier', model: engines.verifier, calls: targets.length, input_tokens: fix.HOSTED_CHECK_MAX_BYTES, output_tokens: fix.HOSTED_OUTPUT_TOKENS.verifier, max_input_bytes: fix.HOSTED_CHECK_MAX_BYTES });
|
|
168
|
+
let quote;
|
|
169
|
+
try { quote = await cloud.quoteEstimate({ calls: rows }, { token }); } catch (e) { console.error(` nothing was spent. ${e.message}`); return 2; }
|
|
170
|
+
console.log(`\n ${cloud.describeQuote(quote).replace(/^this review/, 'this fix')}\n`);
|
|
171
|
+
let yes = opts.yes;
|
|
172
|
+
if (!yes) {
|
|
173
|
+
const input = deps.input || process.stdin;
|
|
174
|
+
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return 1; }
|
|
175
|
+
yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: process.stdout });
|
|
176
|
+
if (!yes) { console.log(' nothing was spent'); return 1; }
|
|
177
|
+
}
|
|
178
|
+
if (!hostedRun) {
|
|
179
|
+
const spend = cloud.spendGuard({ quoteCall: deps.quoteCall });
|
|
180
|
+
spend.max = quote.credits;
|
|
181
|
+
spend.limits = quote.limits;
|
|
182
|
+
const runner = cloud.atrisJobRunner({ token, spend });
|
|
183
|
+
hostedRun = (jobs) => runner(jobs, { root, concurrency: 1, timeoutMs: opts.timeoutMs || 20 * 60 * 1000 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const log = deps.log || ((m) => console.log(` · ${m}`));
|
|
188
|
+
const ctx = {
|
|
189
|
+
from,
|
|
190
|
+
testCommand,
|
|
191
|
+
engines,
|
|
192
|
+
env,
|
|
193
|
+
allowed,
|
|
194
|
+
hostedRun,
|
|
195
|
+
runBuilder: deps.runBuilder || fix.defaultRunBuilder,
|
|
196
|
+
runChecker: deps.runChecker || fix.defaultRunChecker,
|
|
197
|
+
timeouts: { build: opts.timeoutMs || 20 * 60 * 1000, check: opts.timeoutMs || 10 * 60 * 1000, test: 20 * 60 * 1000 },
|
|
198
|
+
log,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
console.log(`\n ◉ sentinel fix · ${engines.builder} writes the fix · ${engines.verifier} checks it · tests: ${testCommand}`);
|
|
202
|
+
const results = [];
|
|
203
|
+
for (const record of targets) {
|
|
204
|
+
console.log(`\n ${fix.shortId(record.fingerprint)} · ${record.title}`);
|
|
205
|
+
const res = await fix.fixOne(root, record, ctx);
|
|
206
|
+
results.push({ record, res });
|
|
207
|
+
if (res.ok) {
|
|
208
|
+
console.log(` prepared a fix on a new branch: ${res.branch}`);
|
|
209
|
+
console.log(` the new test failed on the old code and passes with the fix. the full test suite passes. ${engines.verifier} checked from source that the attack no longer works`);
|
|
210
|
+
console.log(` review it with: git diff ${from}...${res.branch}`);
|
|
211
|
+
if (opts.pr) {
|
|
212
|
+
const pr = fix.openPr(root, res, record, { run: deps.run, verifier: engines.verifier });
|
|
213
|
+
res.pr = pr;
|
|
214
|
+
if (pr.ok) console.log(` opened a pull request${pr.url ? `: ${pr.url}` : ''}. nothing was merged`);
|
|
215
|
+
else console.log(` no pull request was opened: ${pr.reason}${pr.pushed ? '' : '. nothing was pushed'}`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(' nothing was pushed');
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
console.log(` no fix was committed: ${res.reason}`);
|
|
221
|
+
if (res.branch === null || res.branch === undefined) console.log(` you are on ${from}${res.patch ? `. the attempt is saved at ${tildePath(res.patch, env.HOME || undefined)}` : ''}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const done = results.filter((x) => x.res.ok);
|
|
226
|
+
if (done.length) {
|
|
227
|
+
const events = require('../lib/sentinel-events');
|
|
228
|
+
let upload;
|
|
229
|
+
if (!opts.upload) upload = { skipped: 'no-upload' };
|
|
230
|
+
else if (!token) upload = { skipped: 'not-logged-in' };
|
|
231
|
+
else {
|
|
232
|
+
try { upload = await events.postEvents(done.map((x) => fix.fixEvent(root, x.record, x.res)), { token, base: deps.base }); } catch (e) { upload = { ok: false, reason: e.message }; }
|
|
233
|
+
}
|
|
234
|
+
if (upload.skipped === 'no-upload') console.log('\n kept on this machine. nothing was sent (--no-upload)');
|
|
235
|
+
else if (upload.skipped === 'not-logged-in') console.log('\n not logged in. nothing was sent to your dashboard');
|
|
236
|
+
else if (!upload.ok) console.log(`\n warning: could not tell your atris dashboard: ${upload.reason}. the fix itself is done`);
|
|
237
|
+
else console.log('\n told your atris dashboard. no code was sent');
|
|
238
|
+
}
|
|
239
|
+
console.log('');
|
|
240
|
+
return results.every((x) => x.res.ok) ? 0 : 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { fixCommand, parseFixArgs };
|