atris 3.79.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/README.md +6 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/auth.js +2 -2
- package/commands/business.js +44 -1984
- package/commands/computer.js +165 -2412
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/security-review.js +129 -0
- package/commands/site-deploy.js +5 -1
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +318 -0
- package/lib/security-audit.js +1549 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +9 -2
- package/utils/auth.js +18 -4
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
// atris security-review --deep model-review prompt + evidence
|
|
16
16
|
// atris security-review --update-baseline accept current findings
|
|
17
17
|
// atris security-review hook install a pre-commit gate
|
|
18
|
+
// atris security-review audit sentinel audit: engines hunt, a
|
|
19
|
+
// different model verifies (see
|
|
20
|
+
// lib/security-audit.js)
|
|
18
21
|
//
|
|
19
22
|
// Exit code: 0 = clean, 1 = active findings at/over the fail threshold, 2 = bad usage.
|
|
20
23
|
|
|
@@ -123,6 +126,7 @@ function securityReviewCommand(argv = []) {
|
|
|
123
126
|
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) return printHelp();
|
|
124
127
|
if (sub === 'rules') return printRules();
|
|
125
128
|
if (sub === 'hook' || sub === 'install-hook') return installHook();
|
|
129
|
+
if (sub === 'audit') return auditCommand(argv.slice(1));
|
|
126
130
|
|
|
127
131
|
const root = process.cwd();
|
|
128
132
|
let opts;
|
|
@@ -397,6 +401,129 @@ function installHook() {
|
|
|
397
401
|
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
398
402
|
}
|
|
399
403
|
|
|
404
|
+
function parseAuditArgs(argv) {
|
|
405
|
+
const opts = { profile: 'standard', scope: [], json: false, planOnly: false, budget: null, out: null, hunter: null, verifier: null, timeoutMs: null };
|
|
406
|
+
const takes = (i, flag) => {
|
|
407
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
408
|
+
return argv[i + 1];
|
|
409
|
+
};
|
|
410
|
+
for (let i = 0; i < argv.length; i++) {
|
|
411
|
+
const arg = argv[i];
|
|
412
|
+
const [flag, inline] = arg.includes('=') && arg.startsWith('--') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, null];
|
|
413
|
+
const value = () => (inline !== null ? inline : takes(i++, flag));
|
|
414
|
+
if (flag === '--json') opts.json = true;
|
|
415
|
+
else if (flag === '--plan' || flag === '--dry-run') opts.planOnly = true;
|
|
416
|
+
else if (flag === '--quick' || flag === '--standard' || flag === '--deep') opts.profile = flag.slice(2);
|
|
417
|
+
else if (flag === '--profile') opts.profile = value();
|
|
418
|
+
else if (flag === '--budget') opts.budget = Number(value());
|
|
419
|
+
else if (flag === '--out') opts.out = value();
|
|
420
|
+
else if (flag === '--hunter') opts.hunter = value();
|
|
421
|
+
else if (flag === '--verifier') opts.verifier = value();
|
|
422
|
+
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
423
|
+
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
424
|
+
else opts.scope.push(arg);
|
|
425
|
+
}
|
|
426
|
+
const { PROFILES } = require('../lib/security-audit');
|
|
427
|
+
if (!PROFILES.includes(opts.profile)) throw new Error(`--profile must be one of ${PROFILES.join(', ')}`);
|
|
428
|
+
if (opts.budget !== null && (!Number.isInteger(opts.budget) || opts.budget < 1 || opts.budget > 500)) throw new Error('--budget must be a whole number from 1 to 500');
|
|
429
|
+
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
430
|
+
return opts;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function tildePath(p) {
|
|
434
|
+
const home = require('os').homedir();
|
|
435
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// The audit landing: what was found, how much was read, what is left.
|
|
439
|
+
function renderAuditLanding({ dir, meta, records }) {
|
|
440
|
+
const confirmed = records.filter((r) => r.verdict === 'confirmed');
|
|
441
|
+
const needs = records.filter((r) => r.verdict === 'needs_validation');
|
|
442
|
+
const rejected = records.filter((r) => r.verdict === 'rejected');
|
|
443
|
+
const sev = meta.counts.severity;
|
|
444
|
+
const cov = meta.coverage;
|
|
445
|
+
const sevText = ['critical', 'high', 'medium', 'low', 'informational'].filter((s) => sev[s]).map((s) => `${sev[s]} ${s}`).join(', ');
|
|
446
|
+
const L = ['', ` ◉ sentinel security audit · ${meta.profile}${meta.scope_paths.length ? ` · ${meta.scope_paths.join(', ')}` : ''}`, ''];
|
|
447
|
+
if (meta.incomplete_reason === 'plan_only') {
|
|
448
|
+
L.push(` plan only: ${cov.in_scope} areas to review, no engines called`);
|
|
449
|
+
const { hunters, hunters_needed: needed, units_per_hunter: per, verifyReserve } = meta.plan;
|
|
450
|
+
const sending = Math.min(hunters, needed);
|
|
451
|
+
L.push(` a ${meta.profile} run with budget ${meta.budget} sends about ${sending} hunter${sending === 1 ? '' : 's'} (up to ${per} area${per === 1 ? '' : 's'} each) and keeps ${verifyReserve} checks for verifying`);
|
|
452
|
+
if (hunters < 1) L.push(' this budget is too small to send a hunter; try --budget 8 or more');
|
|
453
|
+
else L.push(needed <= hunters ? ' one run can read every area' : ` about ${Math.ceil(needed / hunters)} runs to read every area`);
|
|
454
|
+
} else {
|
|
455
|
+
L.push(` ${confirmed.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
|
|
456
|
+
L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
|
|
457
|
+
L.push(` found by ${meta.engines.hunter}, checked by ${meta.engines.verifier}${meta.engines.hunter === meta.engines.verifier ? ' (same engine)' : ''} · ${meta.invocations.used} engine calls of ${meta.budget}`);
|
|
458
|
+
for (const r of confirmed.slice(0, 6)) {
|
|
459
|
+
const sink = r.trace[r.trace.length - 1];
|
|
460
|
+
L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
|
|
461
|
+
}
|
|
462
|
+
if (meta.run_status === 'incomplete') L.push(` incomplete: ${String(meta.incomplete_reason).replace(/_/g, ' ')}`);
|
|
463
|
+
}
|
|
464
|
+
L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
|
|
465
|
+
return L.join('\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function auditCommand(argv) {
|
|
469
|
+
if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
|
|
470
|
+
const audit = require('../lib/security-audit');
|
|
471
|
+
if (argv[0] === 'status' || argv[0] === 'last') {
|
|
472
|
+
const last = audit.latestRun(process.cwd());
|
|
473
|
+
if (!last || !last.meta) { console.log('\n no sentinel audit has run for this repo yet\n'); return 0; }
|
|
474
|
+
if (argv.includes('--json')) { console.log(JSON.stringify({ dir: last.dir, meta: last.meta, findings: last.records }, null, 2)); return 0; }
|
|
475
|
+
console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
let opts;
|
|
479
|
+
try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
480
|
+
const log = opts.json ? () => {} : (msg) => console.log(` · ${msg}`);
|
|
481
|
+
let result;
|
|
482
|
+
try {
|
|
483
|
+
result = await audit.runAudit({
|
|
484
|
+
root: process.cwd(), profile: opts.profile, scope: opts.scope, budget: opts.budget ?? undefined, out: opts.out,
|
|
485
|
+
hunter: opts.hunter || undefined, verifier: opts.verifier || undefined, planOnly: opts.planOnly,
|
|
486
|
+
timeoutMs: opts.timeoutMs || undefined, log,
|
|
487
|
+
});
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.error(`security-review audit: ${e.message}`);
|
|
490
|
+
return 2;
|
|
491
|
+
}
|
|
492
|
+
const fail = audit.shouldFailAudit(result.records);
|
|
493
|
+
if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
|
|
494
|
+
else console.log(renderAuditLanding(result));
|
|
495
|
+
return fail ? 1 : 0;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function printAuditHelp() {
|
|
499
|
+
console.log(`
|
|
500
|
+
atris security-review audit, the sentinel's full security audit
|
|
501
|
+
|
|
502
|
+
atris security-review audit standard audit of the repo
|
|
503
|
+
atris security-review audit src/ audit only these paths
|
|
504
|
+
atris security-review audit --quick one wave, coarse areas, cheap
|
|
505
|
+
atris security-review audit --deep more waves, smaller areas
|
|
506
|
+
atris security-review audit --plan map areas and budget, call no engine
|
|
507
|
+
atris security-review audit --budget 30 most engine calls this run may spend
|
|
508
|
+
atris security-review audit --hunter codex --verifier claude
|
|
509
|
+
which engines hunt and which verify
|
|
510
|
+
atris security-review audit --out DIR write the run here (outside the repo)
|
|
511
|
+
atris security-review audit status the last run's landing
|
|
512
|
+
|
|
513
|
+
How it works: every source file x attack class with a reason to be read
|
|
514
|
+
becomes an area in a coverage ledger. Read-only engine hunters each own a
|
|
515
|
+
few areas. A coverage critic names gaps. Every candidate goes to a different
|
|
516
|
+
model that tries to disprove it, and cited lines must exist before any spend.
|
|
517
|
+
Runs add up: unchanged areas are skipped next time.
|
|
518
|
+
|
|
519
|
+
Output: ~/.atris/security-audit/<repo>/run-<N>/ (REPORT.md,
|
|
520
|
+
FINDINGS-DETAIL.md, NEEDS-VALIDATION.md, findings.json, coverage-ledger.json).
|
|
521
|
+
Nothing is written inside the repo. Exit 1 when a confirmed finding is high
|
|
522
|
+
or critical.
|
|
523
|
+
`);
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
400
527
|
function printHelp() {
|
|
401
528
|
console.log(`
|
|
402
529
|
atris security-review, deterministic secrets / PII / privacy scan (no LLM)
|
|
@@ -416,6 +543,8 @@ function printHelp() {
|
|
|
416
543
|
ignore .security-review.baseline.json
|
|
417
544
|
atris security-review rules list the active detectors
|
|
418
545
|
atris security-review hook install a pre-commit gate
|
|
546
|
+
atris security-review audit sentinel audit: engines hunt, another model
|
|
547
|
+
verifies (atris security-review audit help)
|
|
419
548
|
|
|
420
549
|
Scans for real-looking secrets, API keys, personal data, tracked sensitive
|
|
421
550
|
files, and code-exec review evidence. exit 0 = clean, 1 = found. Wire into
|
package/commands/site-deploy.js
CHANGED
|
@@ -113,13 +113,17 @@ function validateSlug(slug) {
|
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
117
|
+
|
|
116
118
|
function validateApiBase(value) {
|
|
117
119
|
try {
|
|
118
120
|
const parsed = new URL(value);
|
|
119
121
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('bad protocol');
|
|
122
|
+
// The login token rides on every request, so plain http is only for this machine.
|
|
123
|
+
if (parsed.protocol === 'http:' && !LOOPBACK_HOSTS.has(parsed.hostname)) throw new Error('bad protocol');
|
|
120
124
|
return parsed.toString().replace(/\/$/, '');
|
|
121
125
|
} catch {
|
|
122
|
-
throw new Error('--api-base must be
|
|
126
|
+
throw new Error('--api-base must be an https url (http only for localhost, 127.0.0.1, or [::1])');
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
|