@clear-capabilities/agentic-security-scanner 0.133.0 → 0.136.2

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/bin/agentic-security-audit.js +2 -1
  3. package/bin/agentic-security-consistency.js +2 -1
  4. package/bin/agentic-security.js +348 -40
  5. package/dist/113.index.js +6 -6
  6. package/dist/117.index.js +3 -1
  7. package/dist/178.index.js +1 -1
  8. package/dist/220.index.js +5 -3
  9. package/dist/238.index.js +4 -4
  10. package/dist/317.index.js +270 -0
  11. package/dist/384.index.js +1 -1
  12. package/dist/435.index.js +13 -7
  13. package/dist/499.index.js +86 -0
  14. package/dist/513.index.js +7 -3
  15. package/dist/526.index.js +6 -6
  16. package/dist/609.index.js +741 -0
  17. package/dist/637.index.js +1 -1
  18. package/dist/675.index.js +7 -5
  19. package/dist/839.index.js +4 -3
  20. package/dist/905.index.js +1173 -0
  21. package/dist/agentic-security.mjs +56 -56
  22. package/dist/agentic-security.mjs.sha256 +1 -1
  23. package/dist/compliance-frameworks/ccpa.json +32 -0
  24. package/dist/compliance-frameworks/eu-ai-act.json +51 -0
  25. package/dist/compliance-frameworks/gdpr.json +45 -0
  26. package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
  27. package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
  28. package/dist/compliance-frameworks/nist-csf-2.json +73 -0
  29. package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
  30. package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
  31. package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
  32. package/package.json +14 -6
  33. package/src/badge.js +2 -1
  34. package/src/dataflow/cross-service-taint.js +2 -1
  35. package/src/dataflow/ifds-precise.js +6 -4
  36. package/src/dataflow/incremental.js +7 -5
  37. package/src/discovery/CLAUDE.md +48 -0
  38. package/src/discovery/confirm.js +47 -0
  39. package/src/discovery/disprove.js +79 -0
  40. package/src/discovery/hunter.js +116 -0
  41. package/src/discovery/index.js +331 -0
  42. package/src/discovery/judge.js +97 -0
  43. package/src/discovery/lenses.js +69 -0
  44. package/src/discovery/llm-invoke.js +120 -0
  45. package/src/discovery/memory.js +163 -0
  46. package/src/discovery/partition.js +92 -0
  47. package/src/engine.js +163 -7
  48. package/src/integrations/tickets.js +7 -6
  49. package/src/ir/ir-stats.js +1 -1
  50. package/src/ir/parser-cpp.js +1 -1
  51. package/src/leaderboard.js +3 -2
  52. package/src/llm-validator/consistency.js +6 -2
  53. package/src/llm-validator/index.js +30 -41
  54. package/src/llm-validator/providers.js +227 -0
  55. package/src/mcp/audit.js +2 -1
  56. package/src/mcp/tools.js +4 -3
  57. package/src/posture/CLAUDE.md +102 -1
  58. package/src/posture/accuracy-scorecard.js +59 -1
  59. package/src/posture/agents-memory.js +5 -3
  60. package/src/posture/auditor-walkthrough.js +9 -7
  61. package/src/posture/auth-posture-import.js +5 -4
  62. package/src/posture/autopilot.js +225 -0
  63. package/src/posture/comparison.js +181 -0
  64. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
  65. package/src/posture/compliance-policy.js +7 -9
  66. package/src/posture/custom-rules.js +7 -5
  67. package/src/posture/cve-alert-daemon.js +6 -5
  68. package/src/posture/dep-add-guard.js +2 -1
  69. package/src/posture/deterministic.js +3 -2
  70. package/src/posture/evidence-bundle.js +246 -0
  71. package/src/posture/execution-proof.js +25 -1
  72. package/src/posture/exploitability-probability.js +2 -1
  73. package/src/posture/feature-flags.js +3 -2
  74. package/src/posture/findings-memory.js +3 -3
  75. package/src/posture/fix-history.js +5 -2
  76. package/src/posture/fix-metrics.js +5 -5
  77. package/src/posture/fix-plan.js +2 -1
  78. package/src/posture/fleet.js +0 -0
  79. package/src/posture/grader-calibration.js +3 -4
  80. package/src/posture/intent-context.js +2 -1
  81. package/src/posture/learning.js +4 -3
  82. package/src/posture/license-attributions.js +5 -7
  83. package/src/posture/license-graph.js +2 -1
  84. package/src/posture/license-policy.js +2 -1
  85. package/src/posture/logic-claims.js +266 -0
  86. package/src/posture/model-rescan.js +4 -3
  87. package/src/posture/network-policy-import.js +3 -2
  88. package/src/posture/poc-inprocess.js +404 -2
  89. package/src/posture/pqc-migration-plan.js +7 -5
  90. package/src/posture/pr-augment.js +8 -5
  91. package/src/posture/privacy-framework.js +262 -0
  92. package/src/posture/proof-artifact.js +101 -0
  93. package/src/posture/prove-findings.js +28 -4
  94. package/src/posture/risk-dollars.js +2 -2
  95. package/src/posture/router.js +5 -4
  96. package/src/posture/ruleset-version.js +2 -2
  97. package/src/posture/runtime-correlation.js +2 -1
  98. package/src/posture/sbom-diff.js +12 -3
  99. package/src/posture/sca-policy.js +7 -4
  100. package/src/posture/scan-checkpoint.js +15 -0
  101. package/src/posture/state-dir.js +34 -0
  102. package/src/posture/telemetry-ingest.js +4 -3
  103. package/src/posture/threat-model-auto.js +4 -1
  104. package/src/posture/threat-model-grounding.js +11 -1
  105. package/src/posture/time-to-fix.js +3 -2
  106. package/src/posture/triage-memory.js +3 -2
  107. package/src/posture/waf-ingest.js +6 -5
  108. package/src/posture/watch-mode.js +4 -3
  109. package/src/report/index.js +9 -0
  110. package/src/sast/code-injection-multilang.js +29 -0
  111. package/src/sca/dep-confusion.js +2 -1
@@ -30,6 +30,7 @@ import * as triage from '../src/posture/triage.js';
30
30
  import { buildSlackDigest, buildDiscordDigest, postWebhook, buildJiraIssue, buildPrComment, buildSiemEvent, loadIntegrationConfig } from '../src/integrations/index.js';
31
31
  import { globFiles } from '../src/util/glob.js';
32
32
 
33
+ import { stateDir, statePath } from '../src/posture/state-dir.js';
33
34
  // last-scan.json integrity helpers — implementation in posture/integrity.js
34
35
  // so the MCP server tools can share verification.
35
36
  function _verifyLastScan(body, sigFile) {
@@ -65,6 +66,13 @@ Commands:
65
66
  verify [--finding <id>] Re-run the verifier loop on last-scan findings (use --live --target <url> to execute PoCs)
66
67
  reset [--yes] [--keep ...] Right-to-delete: wipe accumulated learned state under .agentic-security/ (preserves operator-authored config)
67
68
  rule-synth [--dry-run] Auto-synthesise suppression rules from repeated FP verdicts (proposes — does not activate)
69
+ compliance [--privacy] Assess the last scan against NIST Privacy Framework 1.1
70
+ --list show bundled + BYO frameworks
71
+ --walkthrough <id> auditor narrative for any framework
72
+ --gap only the failing controls
73
+ --format cli|json|md (default cli)
74
+ --fail-on gap exit 1 when a control is failing
75
+ Reads .agentic-security/last-scan.json — run a scan first.
68
76
  version Print version
69
77
  banner [--full] Print the Patch-the-frog mascot + brand lockup
70
78
  harness [path] [--include-home] Multi-harness config audit: scans .claude/,
@@ -290,20 +298,23 @@ function _bundleSha() {
290
298
  }
291
299
 
292
300
  async function writeMachineOutput(targetAbs, scan, meta, profile) {
293
- const stateDir = path.join(targetAbs, '.agentic-security');
294
- const { isSafeStateDir: _isSafe } = await import('../src/posture/state-dir.js');
295
- if (!_isSafe(stateDir)) {
296
- if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write machine output at ${stateDir}no project marker\n`);
301
+ const stateDirPath = stateDir(targetAbs);
302
+ const { isSafeStateDir: _isSafe, stateWritesEnabled: _writesOn } = await import('../src/posture/state-dir.js');
303
+ // Read-only scan (NON_MUTATING_SCAN_PRD S1). These writes bypass
304
+ // safeWriteState because they are async, so the switch is checked herethe
305
+ // same refusal path the project-root check already uses.
306
+ if (!_writesOn() || !_isSafe(stateDirPath)) {
307
+ if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write machine output at ${stateDirPath} — no project marker\n`);
297
308
  return;
298
309
  }
299
- await fsp.mkdir(stateDir, { recursive: true });
310
+ await fsp.mkdir(stateDirPath, { recursive: true });
300
311
  // Always JSON (used by /security-fix and /security-report).
301
- await fsp.writeFile(path.join(stateDir, 'findings.json'),
312
+ await fsp.writeFile(path.join(stateDirPath, 'findings.json'),
302
313
  JSON.stringify(toJSON(scan, meta), null, 2));
303
314
  if (profile.profile === 'pro' || profile.machineOutput) {
304
- await fsp.writeFile(path.join(stateDir, 'findings.sarif'),
315
+ await fsp.writeFile(path.join(stateDirPath, 'findings.sarif'),
305
316
  JSON.stringify(toSARIF(scan, meta), null, 2));
306
- await fsp.writeFile(path.join(stateDir, 'findings.csv'), toCSV(scan));
317
+ await fsp.writeFile(path.join(stateDirPath, 'findings.csv'), toCSV(scan));
307
318
  }
308
319
  }
309
320
 
@@ -325,6 +336,11 @@ function parseArgs(argv) {
325
336
  }
326
337
 
327
338
  async function cmdScan(args) {
339
+ // NON_MUTATING_SCAN_PRD S1 — a scan is an observation; --no-state makes it one.
340
+ if (args.flags['no-state']) {
341
+ const { setStateWritesEnabled } = await import('../src/posture/state-dir.js');
342
+ setStateWritesEnabled(false);
343
+ }
328
344
  const target = args._[1] || '.';
329
345
  const targetAbs = path.resolve(target);
330
346
  // Load persona profile (R1). Persona-aware defaults flow from here.
@@ -430,7 +446,7 @@ async function cmdScan(args) {
430
446
  }
431
447
 
432
448
  // --set-baseline: save current findings as baseline for future --since-baseline filtering
433
- const baselinePath = path.join(target || '.', '.agentic-security', 'baseline.json');
449
+ const baselinePath = statePath(target || '.', 'baseline.json');
434
450
  if (args.flags['set-baseline']) {
435
451
  const { normalizeFindings } = await import('../src/report/index.js');
436
452
  const baselineIds = new Set(normalizeFindings(scan).map(f => f.stableId || f.id));
@@ -646,10 +662,10 @@ async function cmdScan(args) {
646
662
  else process.stdout.write(body + '\n');
647
663
 
648
664
  // Persist last scan for /security-fix and /security-report
649
- const { isSafeStateDir: _isSafeStateDir } = await import('../src/posture/state-dir.js');
650
- const stateDir = path.join(path.resolve(target), '.agentic-security');
651
- if (_isSafeStateDir(stateDir)) {
652
- await fsp.mkdir(stateDir, { recursive: true });
665
+ const { isSafeStateDir: _isSafeStateDir, stateWritesEnabled: _writesOnScan } = await import('../src/posture/state-dir.js');
666
+ const stateDirPath = stateDir(path.resolve(target));
667
+ if (_writesOnScan() && _isSafeStateDir(stateDirPath)) {
668
+ await fsp.mkdir(stateDirPath, { recursive: true });
653
669
  const persistedScan = toJSON(scan, meta);
654
670
  // #10 — MTTR: stamp firstSeenAt/lastSeenAt/ageDays from the PREVIOUS scan so
655
671
  // every finding carries an age, SLA breaches can be surfaced, and the fix
@@ -660,7 +676,7 @@ async function cmdScan(args) {
660
676
  const { stampFindingTimestamps, buildBaselineMap, renderSlaSummary } = await import('../src/posture/mttr.js');
661
677
  let baselineMap = new Map();
662
678
  try {
663
- const prev = JSON.parse(await fsp.readFile(path.join(stateDir, 'last-scan.json'), 'utf8'));
679
+ const prev = JSON.parse(await fsp.readFile(path.join(stateDirPath, 'last-scan.json'), 'utf8'));
664
680
  baselineMap = buildBaselineMap(prev);
665
681
  } catch { /* first run — empty baseline, everything is firstSeen now */ }
666
682
  const now = Date.now();
@@ -709,18 +725,18 @@ async function cmdScan(args) {
709
725
  } catch { /* best-effort, offline-degrading — never block a scan */ }
710
726
  }
711
727
  const lastScanBody = JSON.stringify(persistedScan, null, 2);
712
- await fsp.writeFile(path.join(stateDir, 'last-scan.json'), lastScanBody);
728
+ await fsp.writeFile(path.join(stateDirPath, 'last-scan.json'), lastScanBody);
713
729
  try {
714
- await fsp.writeFile(path.join(stateDir, 'last-scan.json.sig'), _signLastScan(lastScanBody));
730
+ await fsp.writeFile(path.join(stateDirPath, 'last-scan.json.sig'), _signLastScan(lastScanBody));
715
731
  } catch { /* non-fatal — sig file is best-effort */ }
716
732
  } else {
717
- if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write state at ${stateDir} — no project marker in ${path.resolve(target)}\n`);
733
+ if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write state at ${stateDirPath} — no project marker in ${path.resolve(target)}\n`);
718
734
  }
719
735
 
720
736
  // 0.14.0 — update streak / achievements after every full scan. Suppress
721
737
  // streak side effects when the user only wants raw JSON output (CI piping).
722
738
  try {
723
- const streak = recordScan(stateDir, persistedScan);
739
+ const streak = recordScan(stateDirPath, persistedScan);
724
740
  // Print celebration / streak line to stderr so it doesn't pollute --format json
725
741
  if (process.stderr.isTTY && format !== 'json' && format !== 'sarif') {
726
742
  const delta = formatGradeDelta(streak);
@@ -799,18 +815,18 @@ async function cmdCi(args) {
799
815
  if (packNames.length) Object.assign(scan, applyPacks(scan, packNames));
800
816
 
801
817
  // Persist the three CI artifacts.
802
- const stateDir = path.join(targetAbs, '.agentic-security');
803
- const { isSafeStateDir: _isSafeCi } = await import('../src/posture/state-dir.js');
804
- if (!_isSafeCi(stateDir)) {
805
- if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write CI artifacts at ${stateDir} — no project marker\n`);
818
+ const stateDirPath = stateDir(targetAbs);
819
+ const { isSafeStateDir: _isSafeCi, stateWritesEnabled: _writesOnCi } = await import('../src/posture/state-dir.js');
820
+ if (!_writesOnCi() || !_isSafeCi(stateDirPath)) {
821
+ if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write CI artifacts at ${stateDirPath} — no project marker\n`);
806
822
  return;
807
823
  }
808
- await fsp.mkdir(stateDir, { recursive: true });
809
- await fsp.writeFile(path.join(stateDir, 'findings.json'),
824
+ await fsp.mkdir(stateDirPath, { recursive: true });
825
+ await fsp.writeFile(path.join(stateDirPath, 'findings.json'),
810
826
  JSON.stringify(toJSON(scan, meta), null, 2));
811
- await fsp.writeFile(path.join(stateDir, 'findings.sarif'),
827
+ await fsp.writeFile(path.join(stateDirPath, 'findings.sarif'),
812
828
  JSON.stringify(toSARIF(scan, meta), null, 2));
813
- await fsp.writeFile(path.join(stateDir, 'findings.junit.xml'),
829
+ await fsp.writeFile(path.join(stateDirPath, 'findings.junit.xml'),
814
830
  toJUnit(scan, meta));
815
831
 
816
832
  const scanCode = exitCodeFor(scan);
@@ -872,7 +888,7 @@ async function cmdAccept(args) {
872
888
  const id = args.flags.finding;
873
889
  if (!id) { console.error('--finding <id> required'); return 4; }
874
890
  const reason = args.flags.reason || 'vibecoded for now';
875
- const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
891
+ const lastScanPath = statePath(target, 'findings.json');
876
892
  if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
877
893
  const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
878
894
  const f = (last.findings || []).find(x => x.id === id);
@@ -930,7 +946,7 @@ async function cmdTriage(args) {
930
946
  }
931
947
  const sub = args._[1];
932
948
  // Sync first so list reflects the latest scan.
933
- const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
949
+ const lastScanPath = statePath(target, 'findings.json');
934
950
  if (fs.existsSync(lastScanPath)) {
935
951
  const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
936
952
  triage.syncWithScan(target, last.findings || []);
@@ -1256,7 +1272,7 @@ async function cmdRule(args) {
1256
1272
  async function cmdValidatorCache(args) {
1257
1273
  const sub = args._[1] || 'help';
1258
1274
  const root = path.resolve(args._[2] || '.');
1259
- const cacheDir = path.join(root, '.agentic-security', 'llm-cache');
1275
+ const cacheDir = statePath(root, 'llm-cache');
1260
1276
  if (!fs.existsSync(cacheDir)) {
1261
1277
  console.log(`No validator cache at ${cacheDir}`);
1262
1278
  return 0;
@@ -1319,7 +1335,7 @@ async function cmdValidatorCache(args) {
1319
1335
  // FR-VER-7 fail-closed: any error → cannot-verify, never silent drop.
1320
1336
  async function cmdVerify(args) {
1321
1337
  const scanRoot = path.resolve(args.flags.root || '.');
1322
- const lastScanPath = path.join(scanRoot, '.agentic-security', 'last-scan.json');
1338
+ const lastScanPath = statePath(scanRoot, 'last-scan.json');
1323
1339
  if (!fs.existsSync(lastScanPath)) {
1324
1340
  console.error(`No prior scan found at ${lastScanPath}. Run \`agentic-security scan\` first.`);
1325
1341
  return 4;
@@ -1421,9 +1437,9 @@ async function cmdVerify(args) {
1421
1437
  // --yes to skip the confirmation prompt (for scripted use).
1422
1438
  async function cmdReset(args) {
1423
1439
  const scanRoot = path.resolve(args.flags.root || '.');
1424
- const stateDir = path.join(scanRoot, '.agentic-security');
1425
- if (!fs.existsSync(stateDir)) {
1426
- console.log(`No state to reset at ${stateDir}`);
1440
+ const stateDirPath = stateDir(scanRoot);
1441
+ if (!fs.existsSync(stateDirPath)) {
1442
+ console.log(`No state to reset at ${stateDirPath}`);
1427
1443
  return 0;
1428
1444
  }
1429
1445
  const WIPE = new Set([
@@ -1448,17 +1464,17 @@ async function cmdReset(args) {
1448
1464
  ]);
1449
1465
  const keep = new Set((args.flags.keep || '').split(',').filter(Boolean));
1450
1466
  const targets = [];
1451
- for (const entry of await fsp.readdir(stateDir, { withFileTypes: true })) {
1467
+ for (const entry of await fsp.readdir(stateDirPath, { withFileTypes: true })) {
1452
1468
  if (keep.has(entry.name)) continue;
1453
1469
  if (WIPE.has(entry.name) || WIPE_DIRS.has(entry.name)) {
1454
1470
  targets.push({ name: entry.name, dir: entry.isDirectory() });
1455
1471
  }
1456
1472
  }
1457
1473
  if (!targets.length) {
1458
- console.log(`Nothing to reset under ${stateDir}.`);
1474
+ console.log(`Nothing to reset under ${stateDirPath}.`);
1459
1475
  return 0;
1460
1476
  }
1461
- console.log(`agentic-security reset — will remove from ${stateDir}:`);
1477
+ console.log(`agentic-security reset — will remove from ${stateDirPath}:`);
1462
1478
  for (const t of targets) console.log(` ${t.name}${t.dir ? '/' : ''}`);
1463
1479
  console.log('');
1464
1480
  console.log('Preserving operator-authored config: rules.yml, rules/, license-policy.yml, trusted-keys.json, ruleset-version.json');
@@ -1468,7 +1484,7 @@ async function cmdReset(args) {
1468
1484
  return 0;
1469
1485
  }
1470
1486
  for (const t of targets) {
1471
- const p = path.join(stateDir, t.name);
1487
+ const p = path.join(stateDirPath, t.name);
1472
1488
  try {
1473
1489
  if (t.dir) await fsp.rm(p, { recursive: true, force: true });
1474
1490
  else await fsp.rm(p, { force: true });
@@ -1485,6 +1501,294 @@ async function cmdReset(args) {
1485
1501
  // FR-LEARN-6: read triage-feedback.json, group repeated FP verdicts by
1486
1502
  // (family, dir prefix), and propose a suppression YAML when ≥ threshold
1487
1503
  // (default 5) verdicts cluster. Writes to .agentic-security/rules-proposed/.
1504
+ // Languages the Layer-1 IR parses. Anything else cannot be partitioned into a
1505
+ // call-graph focus area, so feeding it to a hunter would spend tokens on files
1506
+ // the confirmation gate can never corroborate.
1507
+ const HUNT_EXTS = /\.(?:js|jsx|mjs|cjs|ts|tsx|py|java|cs|kt|go|php|rb)$/i;
1508
+ const HUNT_IGNORE = ['node_modules/**', '.git/**', 'dist/**', 'build/**', 'vendor/**', '**/.agentic-security/**'];
1509
+ const HUNT_MAX_FILES = 2000;
1510
+
1511
+ async function cmdHunt(args) {
1512
+ const scanRoot = path.resolve(args.flags.root || args._[1] || '.');
1513
+ const { listFiles } = await import('../src/util/glob.js');
1514
+ const { buildProjectIR } = await import('../src/ir/index.js');
1515
+ const { runDiscovery } = await import('../src/discovery/index.js');
1516
+ const { LENSES } = await import('../src/discovery/lenses.js');
1517
+
1518
+ const rels = (await listFiles(scanRoot, { ignore: HUNT_IGNORE })).filter(f => HUNT_EXTS.test(f));
1519
+ if (rels.length > HUNT_MAX_FILES) {
1520
+ console.error(`agentic-security: ${rels.length} source files exceeds the ${HUNT_MAX_FILES}-file hunt cap.`);
1521
+ console.error('Narrow the scope with --root <subdir>. Discovery is token-expensive and');
1522
+ console.error('unbounded fan-out on a large repository is the wrong default.');
1523
+ return 2;
1524
+ }
1525
+
1526
+ const fileContents = {};
1527
+ for (const rel of rels) {
1528
+ try { fileContents[rel] = fs.readFileSync(path.join(scanRoot, rel), 'utf8'); } catch { /* unreadable — skip */ }
1529
+ }
1530
+
1531
+ const { perFile, callGraph } = buildProjectIR(fileContents);
1532
+
1533
+ // Prior scan and triage verdicts feed the judge so a hunt does not re-report
1534
+ // what the rule engine already found or what a human already dismissed.
1535
+ let priorScan = null, triageFeedback = null;
1536
+ try { priorScan = JSON.parse(fs.readFileSync(statePath(scanRoot, 'last-scan.json'), 'utf8')); } catch {}
1537
+ try { triageFeedback = JSON.parse(fs.readFileSync(statePath(scanRoot, 'triage-feedback.json'), 'utf8')); } catch {}
1538
+
1539
+ const lenses = args.flags.lens ? String(args.flags.lens).split(',').map(s => s.trim()).filter(Boolean) : undefined;
1540
+ const intFlag = (name) => (args.flags[name] ? parseInt(args.flags[name], 10) : undefined);
1541
+ const numFlag = (name) => (args.flags[name] ? Number(args.flags[name]) : undefined);
1542
+ const report = await runDiscovery(
1543
+ { perFileIR: perFile, callGraph, fileContents, priorScan, triageFeedback },
1544
+ {
1545
+ lenses,
1546
+ maxAreas: intFlag('max-areas'),
1547
+ // PRD Phase 0 / C3. Defaults live in discovery/index.js so the library is
1548
+ // bounded even when driven by something other than this CLI.
1549
+ maxLlmCalls: intFlag('max-llm-calls'),
1550
+ maxWallMs: intFlag('max-wall-ms'),
1551
+ maxCandidates: intFlag('max-candidates'),
1552
+ maxCostUsd: numFlag('max-cost-usd'),
1553
+ // PRD C4 — persist and consult cross-run memory.
1554
+ scanRoot: args.flags['no-memory'] ? undefined : scanRoot,
1555
+ // PRD C2 — a comma-separated endpoint list turns on consensus.
1556
+ endpoints: args.flags.endpoints,
1557
+ costPerCallUsd: numFlag('cost-per-call-usd'),
1558
+ },
1559
+ );
1560
+
1561
+ const c = report.coverage;
1562
+ console.log('');
1563
+ console.log(`Discovery — ${rels.length} file(s), ${c.areasPlanned} focus area(s), ${c.lensesPerArea} lens(es) each`);
1564
+ console.log(` areas hunted: ${c.areasHunted}/${c.areasPlanned} (fully: ${c.areasFullyHunted}) degraded runs: ${c.degradedRuns}/${report.runs.length}`);
1565
+ console.log(` budget: ${c.llmCalls}/${c.maxLlmCalls} LLM calls${c.budgetExhausted ? ' ← EXHAUSTED, run is incomplete' : ''}` +
1566
+ `${c.candidatesCapped ? ` ${c.candidatesCapped} candidate(s) capped` : ''}` +
1567
+ `${c.callsPerFinding !== null && c.callsPerFinding !== undefined ? ` ${c.callsPerFinding} calls/finding` : ''}`);
1568
+ if (c.priorRuns !== null && c.priorRuns !== undefined) {
1569
+ console.log(` memory: ${c.priorRuns} prior run(s)` +
1570
+ `${c.rememberedRefutals ? `, ${c.rememberedRefutals} candidate(s) skipped as already refuted` : ''}`);
1571
+ if (c.nextWave?.summary) console.log(` next wave: ${c.nextWave.summary}`);
1572
+ }
1573
+ console.log(` confirmation: ${JSON.stringify(c.confirmedByTier)} panels: ${c.panelsRun} (undecided ${c.undecidedPanels})`);
1574
+ console.log('');
1575
+
1576
+ if (report.fresh.length === 0) {
1577
+ console.log('No new candidates survived confirmation and refutation.');
1578
+ } else {
1579
+ console.log(`${report.fresh.length} new candidate finding(s):`);
1580
+ for (const f of report.fresh) {
1581
+ console.log(` [${f.severity}] ${f.file}:${f.line} ${f.vuln}`);
1582
+ console.log(` lens=${f.discovery.lens} confirmation=${f.discovery.confirmation?.tier || 'unknown'}`);
1583
+ }
1584
+ }
1585
+ if (report.duplicates.length || report.suppressed.length || report.refutedCandidates.length) {
1586
+ console.log('');
1587
+ console.log(` (${report.duplicates.length} already known, ${report.suppressed.length} previously marked false positive, ` +
1588
+ `${report.refutedCandidates.length} refuted by the panel)`);
1589
+ }
1590
+
1591
+ // PRD C5 — variant analysis. A confirmed finding is rarely alone: the same
1592
+ // root cause is usually copy-pasted, and the siblings often do not trip a
1593
+ // rule. sweepRootCauses already does this with exact accounting for scan
1594
+ // findings; discovery findings simply never reached it until now.
1595
+ if (report.fresh.length && !args.flags['no-variants']) {
1596
+ try {
1597
+ const { sweepRootCauses, formatSweepLedger } = await import('../src/posture/root-cause-sweep.js');
1598
+ const sweep = sweepRootCauses(report.fresh, fileContents, { confirmedOnly: false });
1599
+ const ledger = formatSweepLedger(sweep);
1600
+ if (ledger) {
1601
+ console.log('');
1602
+ console.log('Variant sweep (siblings of the findings above):');
1603
+ console.log(` ${ledger}`);
1604
+ }
1605
+ } catch { /* the sweep is additive; never let it sink the report */ }
1606
+ }
1607
+
1608
+ // Degradation is part of the output. A half-failed hunt must never read as a
1609
+ // clean one, so every reason is printed rather than summarised away.
1610
+ if (c.reasons.length) {
1611
+ console.log('');
1612
+ console.log('Coverage gaps:');
1613
+ for (const r of c.reasons) console.log(` · ${r}`);
1614
+ }
1615
+ if (report.runs.length && c.degradedRuns === report.runs.length) {
1616
+ console.log('');
1617
+ console.log('EVERY hunter run degraded — this result says nothing about the code.');
1618
+ console.log('Set AGENTIC_SECURITY_LLM_ENDPOINT to enable discovery.');
1619
+ }
1620
+
1621
+ // Advisory by design: discovery never gates. Exit 0 unless the scope was bad.
1622
+ console.log('');
1623
+ console.log(`Lenses available: ${LENSES.map(l => l.key).join(', ')} (--lens a,b to narrow)`);
1624
+ return 0;
1625
+ }
1626
+
1627
+ // PRD D2 — emit signed, per-finding evidence bundles, and verify them.
1628
+ //
1629
+ // `attest` needs our private key. `verify-attestation` needs ONLY a public key,
1630
+ // which is the entire point: a buyer or auditor checks the artefact without ever
1631
+ // having had access to anything of ours.
1632
+ /**
1633
+ * `compliance` — framework assessment from the CLI.
1634
+ *
1635
+ * Reads the LAST SCAN rather than running a new one. A compliance answer is a
1636
+ * statement about a scan that happened, and silently re-scanning here would
1637
+ * make the number depend on when you asked rather than on what was measured —
1638
+ * the same class of confusion as a benchmark that mutates its own corpus. If
1639
+ * there is no scan to read, this says so and exits non-zero instead of
1640
+ * assessing an empty project, which would report controls as satisfied on the
1641
+ * strength of having looked at nothing.
1642
+ */
1643
+ async function cmdCompliance(args) {
1644
+ const scanRoot = path.resolve(args.flags.root || '.');
1645
+ const fmt = String(args.flags.format || 'cli');
1646
+ const {
1647
+ assessPrivacyFramework, PRIVACY_FRAMEWORK_ID, BUCKETS,
1648
+ } = await import('../src/posture/privacy-framework.js');
1649
+ const { listFrameworks, loadFramework, evaluateFramework, renderWalkthrough } =
1650
+ await import('../src/posture/auditor-walkthrough.js');
1651
+
1652
+ if (args.flags.list) {
1653
+ const fws = listFrameworks(scanRoot);
1654
+ if (fmt === 'json') { console.log(JSON.stringify(fws, null, 2)); return 0; }
1655
+ for (const f of fws) console.log(` ${f.id.padEnd(20)} ${f.name} [${f.source}]`);
1656
+ return 0;
1657
+ }
1658
+
1659
+ let scan;
1660
+ try { scan = JSON.parse(fs.readFileSync(statePath(scanRoot, 'last-scan.json'), 'utf8')); }
1661
+ catch {
1662
+ console.error('No .agentic-security/last-scan.json — run `agentic-security scan .` first.');
1663
+ console.error('Refusing to assess an unscanned project: every control would report as');
1664
+ console.error('unassessed, which is correct but useless, and one flag away from looking clean.');
1665
+ return 2;
1666
+ }
1667
+ // The engine records this two ways depending on the emit path; take either.
1668
+ scan.filesScanned = scan._scanMeta?.filesScanned ?? scan.scanned?.files ?? 0;
1669
+
1670
+ const wt = args.flags.walkthrough;
1671
+ if (wt && wt !== true) {
1672
+ const fw = loadFramework(scanRoot, String(wt));
1673
+ if (!fw) {
1674
+ console.error(`Unknown framework "${wt}". Try --list.`);
1675
+ return 2;
1676
+ }
1677
+ console.log(renderWalkthrough(fw, evaluateFramework(scanRoot, fw, scan), {}));
1678
+ return 0;
1679
+ }
1680
+
1681
+ // Default mode is --privacy: it is the only framework with a remediation
1682
+ // layer, so it is the only one where a CLI exit code means anything.
1683
+ const r = assessPrivacyFramework(scanRoot, scan);
1684
+ if (!r) { console.error(`Framework ${PRIVACY_FRAMEWORK_ID} could not be loaded.`); return 2; }
1685
+
1686
+ const gapsOnly = !!args.flags.gap;
1687
+ if (fmt === 'json') {
1688
+ console.log(JSON.stringify(gapsOnly ? { ...r, controls: r.controls.filter(c => c.bucket === 'gap') } : r, null, 2));
1689
+ } else if (fmt === 'md') {
1690
+ console.log(fs.readFileSync(statePath(scanRoot, 'privacy-framework.md'), 'utf8'));
1691
+ } else {
1692
+ console.log(`\n${r.frameworkName}\n`);
1693
+ console.log(` ${r.interpretation}\n`);
1694
+ for (const b of BUCKETS) {
1695
+ const rows = r.controls.filter(c => c.bucket === b);
1696
+ if (!rows.length || (gapsOnly && b !== 'gap')) continue;
1697
+ console.log(` ${b} (${rows.length})`);
1698
+ for (const c of rows) {
1699
+ console.log(` ${c.id.padEnd(11)} ${c.summary.slice(0, 84)}`);
1700
+ const f = r.findings.find(x => x.id === `privacy-framework:${c.id}`);
1701
+ if (f) console.log(` → ${f.remediation}`);
1702
+ }
1703
+ console.log('');
1704
+ }
1705
+ console.log(' A control that is "manual" or "engine-gap" is NOT evidence of compliance.');
1706
+ console.log(' This organizes scanner evidence; a licensed assessor owns the attestation.\n');
1707
+ }
1708
+
1709
+ // --fail-on gap is opt-in. A compliance opinion becomes a build failure only
1710
+ // when someone asks for it; defaulting to non-zero would break every pipeline
1711
+ // that adds this command to see the report.
1712
+ if (args.flags['fail-on'] === 'gap' && r.summary.gap > 0) return 1;
1713
+ return 0;
1714
+ }
1715
+
1716
+ async function cmdAttest(args) {
1717
+ const scanRoot = path.resolve(args.flags.root || '.');
1718
+ const {
1719
+ ensureKeyPair, buildEvidenceBundle, signEvidenceBundle,
1720
+ } = await import('../src/posture/evidence-bundle.js');
1721
+
1722
+ let scan;
1723
+ try { scan = JSON.parse(fs.readFileSync(statePath(scanRoot, 'last-scan.json'), 'utf8')); }
1724
+ catch { console.error('No .agentic-security/last-scan.json — run a scan first.'); return 2; }
1725
+
1726
+ const findings = scan.findings || [];
1727
+ const wanted = args.flags.id;
1728
+ const subset = wanted ? findings.filter(f => f.id === wanted || f.stableId === wanted) : findings;
1729
+ if (!subset.length) {
1730
+ console.error(wanted ? `No finding matching "${wanted}".` : 'No findings to attest.');
1731
+ return 2;
1732
+ }
1733
+
1734
+ const kp = ensureKeyPair();
1735
+ if (kp.created) console.error(`Generated a new signing key at ${kp.privateKey} (public: ${kp.publicKey}).`);
1736
+
1737
+ const outDir = statePath(scanRoot, 'attestations');
1738
+ fs.mkdirSync(outDir, { recursive: true });
1739
+ const meta = {
1740
+ engineVersion: scan.engineVersion || null,
1741
+ rulesetVersion: scan.rulesetVersion || null,
1742
+ bundleSha: scan.bundleSha || null,
1743
+ commit: scan.commit || null,
1744
+ };
1745
+
1746
+ let n = 0;
1747
+ for (const f of subset) {
1748
+ const bundle = signEvidenceBundle(buildEvidenceBundle(f, meta), kp.privateKeyPem);
1749
+ const name = `${(f.stableId || f.id || `finding-${n}`)}.json`.replace(/[^\w.-]/g, '_');
1750
+ fs.writeFileSync(path.join(outDir, name), JSON.stringify(bundle, null, 2) + '\n');
1751
+ n++;
1752
+ }
1753
+ console.log(`Signed ${n} evidence bundle(s) → ${path.relative(scanRoot, outDir)}/`);
1754
+ console.log(`Public key (share this with whoever verifies): ${kp.publicKey}`);
1755
+ console.log('');
1756
+ console.log('A bundle proves its contents are unmodified since signing. It does NOT');
1757
+ console.log('prove the finding is real — read evidence.proofTier for that.');
1758
+ return 0;
1759
+ }
1760
+
1761
+ async function cmdVerifyAttestation(args) {
1762
+ const { verifyEvidenceBundle, keyPaths } = await import('../src/posture/evidence-bundle.js');
1763
+ // `args._[0]` is the command name itself — the same convention cmdScan uses.
1764
+ const file = args.flags.bundle || args._[1];
1765
+ if (!file) { console.error('Usage: agentic-security verify-attestation <bundle.json> [--public-key <path>]'); return 2; }
1766
+
1767
+ let bundle;
1768
+ try { bundle = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
1769
+ catch (e) { console.error(`Could not read bundle: ${e.message}`); return 2; }
1770
+
1771
+ const keyFile = args.flags['public-key'] || keyPaths().publicKey;
1772
+ let publicKeyPem = null;
1773
+ try { publicKeyPem = fs.readFileSync(path.resolve(keyFile), 'utf8'); }
1774
+ catch { console.error(`Could not read public key at ${keyFile}. Pass --public-key <path>.`); return 2; }
1775
+
1776
+ const r = verifyEvidenceBundle(bundle, publicKeyPem);
1777
+ if (!r.ok) {
1778
+ console.error(`✗ INVALID — ${r.reason}`);
1779
+ return 1;
1780
+ }
1781
+ const f = bundle.finding || {};
1782
+ console.log('✓ VALID — the bundle is exactly what the signer attested.');
1783
+ console.log('');
1784
+ console.log(` ${f.severity ? `[${f.severity}] ` : ''}${f.vuln || '(no title)'} ${f.file || '?'}:${f.line ?? '?'}`);
1785
+ console.log(` cwe ${f.cwe || 'n/a'} parser ${f.parser || 'n/a'} proof tier ${bundle.evidence?.proofTier || 'n/a'}`);
1786
+ console.log('');
1787
+ console.log(` proves: ${bundle.proves}`);
1788
+ console.log(` does NOT prove: ${bundle.doesNotProve}`);
1789
+ return 0;
1790
+ }
1791
+
1488
1792
  async function cmdRuleSynth(args) {
1489
1793
  const scanRoot = path.resolve(args.flags.root || '.');
1490
1794
  const { synthesizeRules } = await import('../src/posture/rule-synthesis.js');
@@ -1521,7 +1825,7 @@ async function cmdPacks(args) {
1521
1825
  async function cmdDigest(args) {
1522
1826
  const target = path.resolve(args._[1] || '.');
1523
1827
  const profile = loadProfile(target);
1524
- const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
1828
+ const lastScanPath = statePath(target, 'findings.json');
1525
1829
  if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found.'); return 4; }
1526
1830
  const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
1527
1831
  const findings = (last.findings || []).filter(f => f.severity === 'critical' || f.severity === 'high');
@@ -1549,7 +1853,7 @@ async function cmdFix(args) {
1549
1853
  const isApply = !!args.flags.apply;
1550
1854
  const scanRoot = path.resolve(args.flags.root || '.');
1551
1855
  if (!id) { console.error('--finding <id> required'); return 4; }
1552
- const lastScanPath = path.join(scanRoot, '.agentic-security', 'last-scan.json');
1856
+ const lastScanPath = statePath(scanRoot, 'last-scan.json');
1553
1857
  if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
1554
1858
  const lastScanBody = await fsp.readFile(lastScanPath, 'utf8');
1555
1859
  const sigVerified = _verifyLastScan(lastScanBody, lastScanPath + '.sig');
@@ -1777,6 +2081,10 @@ async function main() {
1777
2081
  case 'validator-cache': process.exit(await cmdValidatorCache(args));
1778
2082
  case 'verify': process.exit(await cmdVerify(args));
1779
2083
  case 'reset': process.exit(await cmdReset(args));
2084
+ case 'hunt': process.exit(await cmdHunt(args));
2085
+ case 'compliance': process.exit(await cmdCompliance(args));
2086
+ case 'attest': process.exit(await cmdAttest(args));
2087
+ case 'verify-attestation': process.exit(await cmdVerifyAttestation(args));
1780
2088
  case 'rule-synth': process.exit(await cmdRuleSynth(args));
1781
2089
  case 'digest': process.exit(await cmdDigest(args));
1782
2090
  case 'setup': process.exit(await cmdSetup(args));
@@ -1799,7 +2107,7 @@ async function main() {
1799
2107
  const result = analyzeTranscript({ transcriptPath: args.flags.transcript, projectDir });
1800
2108
  if (result.ok) {
1801
2109
  try {
1802
- const dir = path.join(projectDir, '.agentic-security');
2110
+ const dir = stateDir(projectDir);
1803
2111
  fs.mkdirSync(dir, { recursive: true });
1804
2112
  fs.writeFileSync(path.join(dir, 'cache-telemetry.json'),
1805
2113
  JSON.stringify({ updatedAt: new Date().toISOString(), metrics: result.metrics, leaks: result.leaks }, null, 2));
package/dist/113.index.js CHANGED
@@ -52,7 +52,6 @@ export const modules = {
52
52
 
53
53
 
54
54
 
55
- const STATE_DIR = '.agentic-security';
56
55
  const LOG_FILE = 'fix-metrics.jsonl';
57
56
 
58
57
  // Below this many samples a percentile is an artifact of the sample, not a
@@ -65,7 +64,7 @@ const RELIABLE_N = 10;
65
64
  const FIX_STAGES = Object.freeze(['rescan', 'lint', 'tests', 'honesty', 'poc']);
66
65
 
67
66
  function _logPath(scanRoot) {
68
- return node_path__WEBPACK_IMPORTED_MODULE_1__.join(scanRoot, STATE_DIR, LOG_FILE);
67
+ return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, LOG_FILE);
69
68
  }
70
69
 
71
70
  /**
@@ -77,9 +76,10 @@ function _logPath(scanRoot) {
77
76
  function recordFixAttempt(scanRoot, record) {
78
77
  if (!scanRoot || !record || typeof record !== 'object') return false;
79
78
  try {
80
- const dir = node_path__WEBPACK_IMPORTED_MODULE_1__.join(scanRoot, STATE_DIR);
79
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
81
80
  if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
82
- node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
81
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
82
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
83
83
  // One writeSync of one newline-terminated line: a concurrent reader sees
84
84
  // whole records or nothing, and a torn tail is dropped on read.
85
85
  node_fs__WEBPACK_IMPORTED_MODULE_0__.appendFileSync(_logPath(scanRoot), JSON.stringify(record) + '\n', 'utf8');
@@ -406,8 +406,8 @@ var external_node_child_process_ = __webpack_require__(1421);
406
406
  var external_node_fs_ = __webpack_require__(3024);
407
407
  // EXTERNAL MODULE: external "node:path"
408
408
  var external_node_path_ = __webpack_require__(6760);
409
- // EXTERNAL MODULE: ./src/engine.js + 600 modules
410
- var engine = __webpack_require__(5865);
409
+ // EXTERNAL MODULE: ./src/engine.js + 499 modules
410
+ var engine = __webpack_require__(2912);
411
411
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
412
412
  // Deterministic honesty gates on fix / finding output (#7).
413
413
  //
package/dist/117.index.js CHANGED
@@ -12,6 +12,7 @@ export const modules = {
12
12
  /* unused harmony exports badgeFromScanRoot, renderSvg, _internal */
13
13
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
14
14
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
15
+ /* harmony import */ var _posture_state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
15
16
  // Live SVG badge generator (v0.72).
16
17
  //
17
18
  // Every repo can drop a badge in its README pulling from the latest scan:
@@ -42,6 +43,7 @@ export const modules = {
42
43
 
43
44
 
44
45
 
46
+
45
47
  const COLORS = {
46
48
  critical: '#e05d44', // red
47
49
  high: '#fe7d37', // orange
@@ -56,7 +58,7 @@ const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
56
58
 
57
59
  function _readLastScan(scanRoot) {
58
60
  if (!scanRoot) return null;
59
- const fp = node_path__WEBPACK_IMPORTED_MODULE_1__.join(scanRoot, '.agentic-security', 'last-scan.json');
61
+ const fp = (0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, 'last-scan.json');
60
62
  if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(fp)) return null;
61
63
  try { return JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8')); }
62
64
  catch { return null; }
package/dist/178.index.js CHANGED
@@ -13,7 +13,7 @@ export const modules = {
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
14
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
15
15
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
16
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5865);
16
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2912);
17
17
  // Time-travel + counterfactual scanning (v0.68).
18
18
  //
19
19
  // Two new modes that exploit the pure-input shape of runFullScan: