atris 3.79.0 → 3.80.0

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,125 @@ 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
+ L.push(` a ${meta.profile} run with budget ${meta.budget} sends ${meta.plan.hunters} hunters (${meta.plan.units_per_hunter} area${meta.plan.units_per_hunter === 1 ? '' : 's'} each) and keeps ${meta.plan.verifyReserve} checks for verifying`);
450
+ } else {
451
+ L.push(` ${confirmed.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
452
+ L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
453
+ 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}`);
454
+ for (const r of confirmed.slice(0, 6)) {
455
+ const sink = r.trace[r.trace.length - 1];
456
+ L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
457
+ }
458
+ if (meta.run_status === 'incomplete') L.push(` incomplete: ${String(meta.incomplete_reason).replace(/_/g, ' ')}`);
459
+ }
460
+ L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
461
+ return L.join('\n');
462
+ }
463
+
464
+ async function auditCommand(argv) {
465
+ if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
466
+ const audit = require('../lib/security-audit');
467
+ if (argv[0] === 'status' || argv[0] === 'last') {
468
+ const last = audit.latestRun(process.cwd());
469
+ if (!last || !last.meta) { console.log('\n no sentinel audit has run for this repo yet\n'); return 0; }
470
+ if (argv.includes('--json')) { console.log(JSON.stringify({ dir: last.dir, meta: last.meta, findings: last.records }, null, 2)); return 0; }
471
+ console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
472
+ return 0;
473
+ }
474
+ let opts;
475
+ try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
476
+ const log = opts.json ? () => {} : (msg) => console.log(` · ${msg}`);
477
+ let result;
478
+ try {
479
+ result = await audit.runAudit({
480
+ root: process.cwd(), profile: opts.profile, scope: opts.scope, budget: opts.budget ?? undefined, out: opts.out,
481
+ hunter: opts.hunter || undefined, verifier: opts.verifier || undefined, planOnly: opts.planOnly,
482
+ timeoutMs: opts.timeoutMs || undefined, log,
483
+ });
484
+ } catch (e) {
485
+ console.error(`security-review audit: ${e.message}`);
486
+ return 2;
487
+ }
488
+ const fail = audit.shouldFailAudit(result.records);
489
+ if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
490
+ else console.log(renderAuditLanding(result));
491
+ return fail ? 1 : 0;
492
+ }
493
+
494
+ function printAuditHelp() {
495
+ console.log(`
496
+ atris security-review audit, the sentinel's full security audit
497
+
498
+ atris security-review audit standard audit of the repo
499
+ atris security-review audit src/ audit only these paths
500
+ atris security-review audit --quick one wave, coarse areas, cheap
501
+ atris security-review audit --deep more waves, smaller areas
502
+ atris security-review audit --plan map areas and budget, call no engine
503
+ atris security-review audit --budget 30 most engine calls this run may spend
504
+ atris security-review audit --hunter codex --verifier claude
505
+ which engines hunt and which verify
506
+ atris security-review audit --out DIR write the run here (outside the repo)
507
+ atris security-review audit status the last run's landing
508
+
509
+ How it works: every source file x attack class with a reason to be read
510
+ becomes an area in a coverage ledger. Read-only engine hunters each own a
511
+ few areas. A coverage critic names gaps. Every candidate goes to a different
512
+ model that tries to disprove it, and cited lines must exist before any spend.
513
+ Runs add up: unchanged areas are skipped next time.
514
+
515
+ Output: ~/.atris/security-audit/<repo>/run-<N>/ (REPORT.md,
516
+ FINDINGS-DETAIL.md, NEEDS-VALIDATION.md, findings.json, coverage-ledger.json).
517
+ Nothing is written inside the repo. Exit 1 when a confirmed finding is high
518
+ or critical.
519
+ `);
520
+ return 0;
521
+ }
522
+
400
523
  function printHelp() {
401
524
  console.log(`
402
525
  atris security-review, deterministic secrets / PII / privacy scan (no LLM)
@@ -416,6 +539,8 @@ function printHelp() {
416
539
  ignore .security-review.baseline.json
417
540
  atris security-review rules list the active detectors
418
541
  atris security-review hook install a pre-commit gate
542
+ atris security-review audit sentinel audit: engines hunt, another model
543
+ verifies (atris security-review audit help)
419
544
 
420
545
  Scans for real-looking secrets, API keys, personal data, tracked sensitive
421
546
  files, and code-exec review evidence. exit 0 = clean, 1 = found. Wire into