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.
@@ -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
@@ -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 a valid http or https url');
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