@clear-capabilities/agentic-security-scanner 0.84.1 → 0.118.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 (125) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/dist/178.index.js +1 -1
  3. package/dist/384.index.js +1 -1
  4. package/dist/637.index.js +1 -1
  5. package/dist/838.index.js +1 -1
  6. package/dist/agentic-security.mjs +84 -84
  7. package/dist/agentic-security.mjs.sha256 +1 -1
  8. package/package.json +12 -6
  9. package/src/.agentic-security/dpia.md +26 -0
  10. package/src/.agentic-security/pqc-migration-plan.json +80 -0
  11. package/src/.agentic-security/pqc-migration-plan.md +32 -0
  12. package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  13. package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  14. package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  15. package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  16. package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  17. package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  18. package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  19. package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  20. package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  21. package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  22. package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  23. package/src/.agentic-security/threat-model.json +3702 -0
  24. package/src/.agentic-security/threat-model.md +118 -0
  25. package/src/dataflow/.agentic-security/dpia.md +26 -0
  26. package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
  27. package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  28. package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  29. package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
  30. package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  31. package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  32. package/src/dataflow/.agentic-security/threat-model.json +161 -0
  33. package/src/dataflow/.agentic-security/threat-model.md +43 -0
  34. package/src/dataflow/CLAUDE.md +17 -5
  35. package/src/dataflow/catalog.js +9 -0
  36. package/src/dataflow/engine.js +30 -4
  37. package/src/dataflow/index.js +8 -0
  38. package/src/dataflow/proof-gate.js +95 -0
  39. package/src/dataflow/summaries.js +44 -5
  40. package/src/engine.js +199 -5
  41. package/src/ir/.agentic-security/dpia.md +26 -0
  42. package/src/ir/.agentic-security/exploit-bundles.json +77 -0
  43. package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  44. package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
  45. package/src/ir/.agentic-security/threat-model.json +143 -0
  46. package/src/ir/.agentic-security/threat-model.md +39 -0
  47. package/src/ir/CLAUDE.md +1 -0
  48. package/src/ir/callgraph.js +12 -1
  49. package/src/ir/tree-sitter-loader.js +108 -0
  50. package/src/posture/.agentic-security/dpia.md +26 -0
  51. package/src/posture/.agentic-security/pqc-migration-plan.json +65 -0
  52. package/src/posture/.agentic-security/pqc-migration-plan.md +30 -0
  53. package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
  54. package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
  55. package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  56. package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  57. package/src/posture/.agentic-security/threat-model.json +2038 -0
  58. package/src/posture/.agentic-security/threat-model.md +73 -0
  59. package/src/posture/auditor-walkthrough.js +1 -1
  60. package/src/posture/corpus-status.js +74 -0
  61. package/src/posture/coverage-report.js +113 -0
  62. package/src/posture/holdout-eval.js +57 -0
  63. package/src/posture/pr-augment.js +1 -1
  64. package/src/posture/router.js +37 -7
  65. package/src/report/.agentic-security/dpia.md +26 -0
  66. package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
  67. package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
  68. package/src/report/.agentic-security/threat-model.json +7 -0
  69. package/src/report/.agentic-security/threat-model.md +22 -0
  70. package/src/report/index.js +18 -1
  71. package/src/sast/.agentic-security/dpia.md +26 -0
  72. package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
  73. package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
  74. package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
  75. package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  76. package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
  77. package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  78. package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  79. package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
  80. package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  81. package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
  82. package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  83. package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  84. package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  85. package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
  86. package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
  87. package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  88. package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  89. package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
  90. package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
  91. package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
  92. package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  93. package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
  94. package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
  95. package/src/sast/.agentic-security/threat-model.json +816 -0
  96. package/src/sast/.agentic-security/threat-model.md +91 -0
  97. package/src/sast/CLAUDE.md +9 -3
  98. package/src/sast/code-injection-multilang.js +117 -0
  99. package/src/sast/crypto-protocol.js +43 -1
  100. package/src/sast/csharp-structural.js +65 -0
  101. package/src/sast/csharp.js +3 -0
  102. package/src/sast/csrf.js +24 -3
  103. package/src/sast/csv-injection.js +84 -0
  104. package/src/sast/frontend-hygiene.js +106 -0
  105. package/src/sast/go-structural.js +60 -0
  106. package/src/sast/java-structural.js +81 -0
  107. package/src/sast/js-framework-structural.js +115 -0
  108. package/src/sast/kotlin.js +72 -0
  109. package/src/sast/ldap-injection.js +73 -17
  110. package/src/sast/open-redirect.js +47 -5
  111. package/src/sast/php.js +35 -0
  112. package/src/sast/python-structural.js +83 -0
  113. package/src/sast/redos-nfa.js +75 -0
  114. package/src/sast/response-splitting.js +48 -19
  115. package/src/sast/ruby.js +38 -0
  116. package/src/sast/secret-concat.js +60 -0
  117. package/src/sast/ssrf-cloud-metadata.js +13 -0
  118. package/src/sast/stored-taint.js +88 -0
  119. package/src/sast/tree-sitter-sinks.js +62 -0
  120. package/src/sast/weak-password-hash.js +28 -0
  121. package/src/sast/weak-randomness.js +46 -5
  122. package/src/sast/wrong-context-sanitizer.js +94 -0
  123. package/src/sast/xpath-injection.js +30 -6
  124. package/src/sast/xss-reflected-multilang.js +116 -0
  125. package/src/sast/xxe.js +67 -0
@@ -0,0 +1,73 @@
1
+ # Threat Model (auto-generated)
2
+
3
+ Generated by agentic-security on 2026-05-31.
4
+
5
+ This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.
6
+
7
+ ## Entities + boundaries
8
+
9
+ ```mermaid
10
+ flowchart TB
11
+ subgraph External
12
+ route_GET__api_users_2["http-route: GET /api/users/2"]
13
+ route_GET__api_admin_users["http-route: GET /api/admin/users"]
14
+ route_GET__api_health["http-route: GET /api/health"]
15
+ end
16
+ subgraph Application
17
+ end
18
+ External --> route_GET__api_users_2
19
+ External --> route_GET__api_admin_users
20
+ External --> route_GET__api_health
21
+ ```
22
+
23
+ ## Assets
24
+
25
+
26
+ ## STRIDE threats
27
+
28
+ ### Tampering (148)
29
+
30
+ - [medium] **dos-sync-io** (CWE-400) at `deploy-platform.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
31
+ - [low] **mass-assignment** (CWE-915) at `integrity.js:undefined` — Mass Assignment (req.body Direct to Model)
32
+ - [low] **dos-sync-io** (CWE-400) at `agents-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
33
+ - [low] **dos-sync-io** (CWE-400) at `api-contract.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
34
+ - [low] **dos-sync-io** (CWE-400) at `auditor-walkthrough.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
35
+ - [low] **dos-sync-io** (CWE-400) at `auth-posture-import.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
36
+ - [low] **dos-sync-io** (CWE-400) at `blast-radius.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
37
+ - [low] **dos-sync-io** (CWE-400) at `calibration-drift.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
38
+ - [low] **dos-sync-io** (CWE-400) at `calibration.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
39
+ - [low] **dos-sync-io** (CWE-400) at `compliance-policy.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
40
+ - [low] **dos-sync-io** (CWE-400) at `cross-repo-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
41
+ - [low] **dos-sync-io** (CWE-400) at `custom-rules.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
42
+ - [low] **dos-sync-io** (CWE-400) at `cve-alert-daemon.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
43
+ - [low] **dos-sync-io** (CWE-400) at `cve-lookup.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
44
+ - [low] **dos-sync-io** (CWE-400) at `dep-add-guard.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
45
+ - [low] **dos-sync-io** (CWE-400) at `deterministic.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
46
+ - [low] **dos-sync-io** (CWE-400) at `epss.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
47
+ - [low] **dos-sync-io** (CWE-400) at `exploitability-probability.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
48
+ - [low] **dos-sync-io** (CWE-400) at `feature-flags.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
49
+ - [low] **dos-sync-io** (CWE-400) at `federated-learning.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
50
+ - [low] **dos-sync-io** (CWE-400) at `findings-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
51
+ - [low] **dos-sync-io** (CWE-400) at `fix-history.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
52
+ - [low] **dos-sync-io** (CWE-400) at `fix-plan.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
53
+ - [low] **dos-sync-io** (CWE-400) at `fix-style-mirror.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
54
+ - [low] **dos-sync-io** (CWE-400) at `fix-verify-loop.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
55
+ - … and 123 more
56
+
57
+ ### Information Disclosure (5)
58
+
59
+ - [low] **ssrf** (CWE-918) at `attack-playbooks.js:72` — SSRF: explicit reference to cloud instance-metadata endpoint
60
+ - [low] **ssrf** (CWE-918) at `defender-agent.js:41` — SSRF: explicit reference to cloud instance-metadata endpoint
61
+ - [low] **ssrf** (CWE-918) at `flow-narration.js:24` — SSRF: explicit reference to cloud instance-metadata endpoint
62
+ - [low] **pqc-migration** (CWE-327) at `rule-pack-signing.js:187` — Pre-quantum ED25519 (ed25519) — replace with ML-DSA-65 before CRQC arrives
63
+ - [low] **ssrf** (CWE-918) at `verifier.js:55` — SSRF: explicit reference to cloud instance-metadata endpoint
64
+
65
+ ### Elevation of Privilege (5)
66
+
67
+ - [low] **ssrf** (CWE-918) at `attack-playbooks.js:72` — SSRF: explicit reference to cloud instance-metadata endpoint
68
+ - [low] **prototype-pollution** (CWE-1321) at `adversarial-self-test.js:60` — Prototype Pollution: Direct write to __proto__ / constructor.prototype
69
+ - [low] **ssrf** (CWE-918) at `defender-agent.js:41` — SSRF: explicit reference to cloud instance-metadata endpoint
70
+ - [low] **ssrf** (CWE-918) at `flow-narration.js:24` — SSRF: explicit reference to cloud instance-metadata endpoint
71
+ - [low] **ssrf** (CWE-918) at `verifier.js:55` — SSRF: explicit reference to cloud instance-metadata endpoint
72
+
73
+ ## Attack trees
@@ -230,7 +230,7 @@ export function renderWalkthrough(fw, evaluation, opts = {}) {
230
230
  lines.push('');
231
231
  }
232
232
  if (ev.status === 'absent' || ev.status === 'partial') {
233
- lines.push(`**Remediation:** address the bullet(s) above, then re-run \`/auditor-walkthrough ${fw.id}\` to update this report.`);
233
+ lines.push(`**Remediation:** address the bullet(s) above, then re-run \`/compliance --walkthrough ${fw.id}\` to update this report.`);
234
234
  lines.push('');
235
235
  }
236
236
  }
@@ -0,0 +1,74 @@
1
+ // Corpus coverage / gap reporter (roadmap #10).
2
+ //
3
+ // "Scale the ground-truth corpus" is a data-acquisition effort, not a code one
4
+ // — but you can't prioritize what you can't see. This turns the CVE-replay
5
+ // corpus into an actionable coverage map: progress toward the target size, and
6
+ // exactly which CWE×language cells have zero ground-truth entries. It measures
7
+ // the existing corpus only; it never invents entries.
8
+
9
+ // The corpus's declared target families/languages (mirrors bench manifest).
10
+ export const TARGET_CWES = [
11
+ 'CWE-22', 'CWE-78', 'CWE-79', 'CWE-89', 'CWE-90', 'CWE-94', 'CWE-113',
12
+ 'CWE-327', 'CWE-329', 'CWE-338', 'CWE-352', 'CWE-502', 'CWE-601', 'CWE-611',
13
+ 'CWE-643', 'CWE-798', 'CWE-916', 'CWE-918', 'CWE-1321', 'CWE-1333',
14
+ ];
15
+ export const TARGET_LANGUAGES = [
16
+ 'javascript', 'python', 'java', 'go', 'ruby', 'php', 'csharp', 'kotlin',
17
+ ];
18
+
19
+ // Pure analyzer. `entries` = [{ cwe, language, family, cve, tier? }, …].
20
+ export function analyzeCorpus(entries, opts = {}) {
21
+ const target = opts.target || 500;
22
+ const cwes = opts.cweFamilies || TARGET_CWES;
23
+ const langs = opts.languages || TARGET_LANGUAGES;
24
+ const list = Array.isArray(entries) ? entries : [];
25
+
26
+ const byLanguage = {}, byCwe = {}, matrix = {};
27
+ for (const e of list) {
28
+ const lang = (e && e.language) || 'unknown';
29
+ const cwe = (e && e.cwe) || 'unknown';
30
+ byLanguage[lang] = (byLanguage[lang] || 0) + 1;
31
+ byCwe[cwe] = (byCwe[cwe] || 0) + 1;
32
+ (matrix[cwe] || (matrix[cwe] = {}))[lang] = (matrix[cwe][lang] || 0) + 1;
33
+ }
34
+
35
+ // Empty cells in the target CWE×language matrix = prioritized gaps.
36
+ const gaps = [];
37
+ for (const cwe of cwes) {
38
+ for (const lang of langs) {
39
+ if (!(matrix[cwe] && matrix[cwe][lang])) gaps.push({ cwe, language: lang });
40
+ }
41
+ }
42
+ const cellsTotal = cwes.length * langs.length;
43
+ return {
44
+ total: list.length,
45
+ target,
46
+ progressPct: Math.round((list.length / target) * 100),
47
+ remainingToTarget: Math.max(0, target - list.length),
48
+ byLanguage,
49
+ byCwe,
50
+ matrix,
51
+ cellsTotal,
52
+ cellsCovered: cellsTotal - gaps.length,
53
+ gapCount: gaps.length,
54
+ gaps,
55
+ };
56
+ }
57
+
58
+ export function summarizeCorpusStatus(r) {
59
+ if (!r) return 'corpus: (no data)';
60
+ const lines = [
61
+ `corpus: ${r.total}/${r.target} entries (${r.progressPct}%, ${r.remainingToTarget} to target)`,
62
+ `matrix: ${r.cellsCovered}/${r.cellsTotal} CWE×language cells covered (${r.gapCount} gaps)`,
63
+ ];
64
+ // Top under-covered languages (fewest entries among target languages).
65
+ const langCounts = Object.entries(r.byLanguage).sort((a, b) => a[1] - b[1]);
66
+ if (langCounts.length) {
67
+ lines.push('thinnest languages: ' + langCounts.slice(0, 3).map(([l, n]) => `${l}=${n}`).join(', '));
68
+ }
69
+ // A few example gaps to act on first.
70
+ if (r.gaps.length) {
71
+ lines.push('example gaps: ' + r.gaps.slice(0, 6).map(g => `${g.cwe}/${g.language}`).join(', '));
72
+ }
73
+ return lines.join('\n');
74
+ }
@@ -0,0 +1,113 @@
1
+ // Analysis-coverage honesty report (roadmap #5 + #6).
2
+ //
3
+ // A "perfect" SAST scanner must publish its blind spots, not hide them. This
4
+ // module turns three things the engine already knows internally into an
5
+ // explicit, user-facing coverage summary:
6
+ //
7
+ // 1. Per-language ANALYSIS TIER — which languages in this scan got the full
8
+ // IR + taint engine vs. only pattern (regex) detectors. The taint engine
9
+ // is wired for js/ts/py/java/cs/kt/go/php/rb; everything else
10
+ // (c/c++/rust/swift/solidity/dart/…) is pattern-only today.
11
+ // 2. Files SKIPPED and why (too large, too dense, timed out).
12
+ // 3. UNMODELED-SINK candidates (#5) — calls that match a dangerous shape but
13
+ // have NO finding at their line, i.e. likely recall blind spots to verify.
14
+ //
15
+ // All of this is informational; it never changes a finding's severity.
16
+
17
+ // Languages that flow through the Layer-1 IR + Layer-2 taint engine.
18
+ export const IR_TAINT_LANGS = new Set(['js', 'ts', 'py', 'java', 'cs', 'kt', 'go', 'php', 'rb']);
19
+
20
+ const EXT_TO_LANG = {
21
+ js: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'ts',
22
+ py: 'py', pyw: 'py', java: 'java', go: 'go', rb: 'rb', php: 'php',
23
+ cs: 'cs', kt: 'kt', kts: 'kt', swift: 'swift', rs: 'rs',
24
+ c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
25
+ sol: 'sol', dart: 'dart', scala: 'scala', m: 'objc',
26
+ };
27
+
28
+ export function langOfFile(file) {
29
+ if (typeof file !== 'string') return null;
30
+ const ext = file.split('.').pop().toLowerCase();
31
+ return EXT_TO_LANG[ext] || null;
32
+ }
33
+
34
+ // Given the list of scanned files, report which languages got IR-taint vs.
35
+ // pattern-only analysis, with a per-language file count.
36
+ export function computeAnalysisTiers(fileList) {
37
+ const counts = {};
38
+ for (const f of fileList || []) {
39
+ const l = langOfFile(f);
40
+ if (!l) continue;
41
+ counts[l] = (counts[l] || 0) + 1;
42
+ }
43
+ const irTaint = {}, patternOnly = {};
44
+ for (const [lang, n] of Object.entries(counts)) {
45
+ (IR_TAINT_LANGS.has(lang) ? irTaint : patternOnly)[lang] = n;
46
+ }
47
+ return {
48
+ irTaint, // { js: 12, py: 4 }
49
+ patternOnly, // { rs: 3, sol: 1 } → flow analysis NOT applied
50
+ languages: Object.keys(counts).sort(),
51
+ };
52
+ }
53
+
54
+ // Dangerous-call shapes worth surfacing when no finding lands on their line.
55
+ // Deliberately conservative + cross-language; this is a "did we miss a sink?"
56
+ // prompt for the user, emitted at info level only.
57
+ const DANGER_TOKENS = [
58
+ [/\beval\s*\(/, 'eval'],
59
+ [/\bnew\s+Function\s*\(/, 'Function-constructor'],
60
+ [/\bvm\.\s*runIn\w+\s*\(/, 'vm.runIn*'],
61
+ [/\bchild_process\b|\bexecSync?\s*\(|\bspawnSync?\s*\(/, 'child_process'],
62
+ [/\bdeserialize\s*\(|readObject\s*\(|ObjectInputStream\b/, 'deserialize'],
63
+ [/\byaml\.\s*load\s*\(|\bsafe_load\b/, 'yaml.load'],
64
+ [/\bpickle\.\s*loads?\s*\(/, 'pickle.load'],
65
+ [/\bMarshal\.\s*load\b|\bunserialize\s*\(/, 'unserialize'],
66
+ [/\bRuntime\b[\s\S]{0,40}\.exec\s*\(|\bProcessBuilder\b/, 'Runtime.exec/ProcessBuilder'],
67
+ [/\bos\.\s*system\s*\(|\bsubprocess\.\s*(?:call|run|Popen)\s*\(/, 'os.system/subprocess'],
68
+ [/\.innerHTML\s*=|\bdangerouslySetInnerHTML\b/, 'innerHTML'],
69
+ ];
70
+
71
+ // Scan file contents for danger tokens that have NO finding on their line.
72
+ // findings: array with {file,line}. fc: { path: content }. Returns a capped
73
+ // example list + total count. Lines are 1-based to match finding lines.
74
+ export function countUnmodeledSinkCandidates(fc, findings, opts = {}) {
75
+ const cap = opts.cap || 12;
76
+ const covered = new Set();
77
+ for (const f of findings || []) {
78
+ if (f && f.file && typeof f.line === 'number') covered.add(`${f.file}:${f.line}`);
79
+ }
80
+ const examples = [];
81
+ let count = 0;
82
+ for (const [file, content] of Object.entries(fc || {})) {
83
+ if (typeof content !== 'string') continue;
84
+ const lines = content.split('\n');
85
+ for (let i = 0; i < lines.length; i++) {
86
+ for (const [re, token] of DANGER_TOKENS) {
87
+ if (!re.test(lines[i])) continue;
88
+ if (covered.has(`${file}:${i + 1}`)) continue; // already flagged → modelled
89
+ count++;
90
+ if (examples.length < cap) examples.push({ file, line: i + 1, token });
91
+ break; // one token per line is enough
92
+ }
93
+ }
94
+ }
95
+ return { count, examples };
96
+ }
97
+
98
+ // One-line CLI summary of an analysis-coverage object.
99
+ export function summarizeCoverage(meta) {
100
+ if (!meta) return 'coverage: (none)';
101
+ const t = meta.analysisTier || {};
102
+ const irLangs = Object.keys(t.irTaint || {});
103
+ const patLangs = Object.keys(t.patternOnly || {});
104
+ const parts = [
105
+ `scanned=${meta.filesScanned ?? '?'}`,
106
+ `skipped=${(meta.filesSkipped || 0) + (meta.filesDenseSkipped || 0)}`,
107
+ `timedOut=${meta.filesTimedOut || 0}`,
108
+ `ir-taint=[${irLangs.join(',')}]`,
109
+ ];
110
+ if (patLangs.length) parts.push(`pattern-only=[${patLangs.join(',')}]`);
111
+ if (meta.unmodeledSinkCandidates?.count) parts.push(`unmodeled-sink-candidates=${meta.unmodeledSinkCandidates.count}`);
112
+ return parts.join(' · ');
113
+ }
@@ -23,6 +23,25 @@ import { brierScore, computeBrierOnHeldOut, wilsonInterval } from './calibration
23
23
 
24
24
  const ECE_BINS_DEFAULT = 10;
25
25
 
26
+ // Map a labeled sample to a source language. Prefer an explicit `language`
27
+ // field; otherwise derive it from the `file` extension. Keeps per-language
28
+ // metrics meaningful even for older corpora that only carry a file path.
29
+ const EXT_TO_LANG = {
30
+ js: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'ts',
31
+ py: 'py', java: 'java', go: 'go', rb: 'rb', php: 'php',
32
+ cs: 'cs', kt: 'kt', kts: 'kt', swift: 'swift', rs: 'rs',
33
+ c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
34
+ sol: 'sol', dart: 'dart',
35
+ };
36
+ export function languageOf(o) {
37
+ if (o && typeof o.language === 'string' && o.language) return o.language;
38
+ if (o && typeof o.file === 'string') {
39
+ const ext = o.file.split('.').pop().toLowerCase();
40
+ if (EXT_TO_LANG[ext]) return EXT_TO_LANG[ext];
41
+ }
42
+ return 'unknown';
43
+ }
44
+
26
45
  export function parseLabeledJsonl(text) {
27
46
  if (typeof text !== 'string' || !text.length) return [];
28
47
  const out = [];
@@ -39,6 +58,7 @@ export function parseLabeledJsonl(text) {
39
58
  if (p === null || a === null) continue;
40
59
  out.push({
41
60
  family: typeof o.family === 'string' ? o.family : 'unknown',
61
+ language: languageOf(o),
42
62
  stableId: typeof o.stableId === 'string' ? o.stableId : null,
43
63
  predicted: Math.max(0, Math.min(1, p)),
44
64
  actual: a,
@@ -98,6 +118,27 @@ export function perFamily(samples) {
98
118
  return fams;
99
119
  }
100
120
 
121
+ // Per-language precision/recall breakdown (roadmap #9). The "perfect
122
+ // multi-language SAST" claim is only defensible with per-language ground
123
+ // truth — a corpus that's 90% JS can hide poor Ruby precision behind a
124
+ // healthy aggregate. Mirrors perFamily, keyed by sample.language, and adds
125
+ // a per-language precision so a regression in one language is visible.
126
+ export function perLanguage(samples) {
127
+ const langs = {};
128
+ for (const s of samples) {
129
+ const l = s.language || 'unknown';
130
+ if (!langs[l]) langs[l] = { tp: 0, fp: 0, n: 0, precision: 0 };
131
+ langs[l].n++;
132
+ if (s.actual === 1) langs[l].tp++;
133
+ else if (s.actual === 0) langs[l].fp++;
134
+ }
135
+ for (const l of Object.keys(langs)) {
136
+ const { tp, fp } = langs[l];
137
+ langs[l].precision = (tp + fp) > 0 ? Number((tp / (tp + fp)).toFixed(4)) : 0;
138
+ }
139
+ return langs;
140
+ }
141
+
101
142
  // One-shot evaluation: Brier + ECE + per-family TP/FP + overall precision.
102
143
  // Returns null only when there's truly no data; never returns a tautological
103
144
  // zero.
@@ -124,14 +165,30 @@ export function evaluateHeldOut(samples) {
124
165
  precision,
125
166
  precisionCi95: ci,
126
167
  perFamily: fams,
168
+ perLanguage: perLanguage(samples),
127
169
  notes: [
128
170
  ...(samples.length < 100 ? ['n<100: Brier and ECE have wide confidence; treat as directional, not decision-grade.'] : []),
129
171
  ...(brierR.brier !== null && brierR.brier > 0.10 ? [`brier=${brierR.brier.toFixed(3)} exceeds PRD target 0.10`] : []),
130
172
  ...(ece && ece.ece > 0.05 ? [`ece=${ece.ece.toFixed(3)} exceeds 0.05 calibration target`] : []),
173
+ // Per-language regression surfacing: a language with enough samples
174
+ // whose precision trails the aggregate by >0.15 is the failure the
175
+ // aggregate would otherwise mask.
176
+ ...Object.entries(perLanguage(samples))
177
+ .filter(([, m]) => m.n >= 20 && precision - m.precision > 0.15)
178
+ .map(([lang, m]) => `language '${lang}' precision=${m.precision.toFixed(3)} (n=${m.n}) trails aggregate ${precision.toFixed(3)}`),
131
179
  ],
132
180
  };
133
181
  }
134
182
 
183
+ // Per-language dashboard line: one row per language, sorted by sample count.
184
+ export function summarizePerLanguage(result) {
185
+ if (!result || !result.ok || !result.perLanguage) return 'per-language: no data';
186
+ return Object.entries(result.perLanguage)
187
+ .sort((a, b) => b[1].n - a[1].n)
188
+ .map(([lang, m]) => `${lang}: precision=${m.precision.toFixed(3)} (tp=${m.tp} fp=${m.fp} n=${m.n})`)
189
+ .join('\n');
190
+ }
191
+
135
192
  // CLI-friendly summary line.
136
193
  export function summarize(result) {
137
194
  if (!result || !result.ok) return `held-out: ${result?.reason || 'unknown error'}`;
@@ -141,7 +141,7 @@ export function augmentPrBody(scanRoot, opts = {}) {
141
141
  lines.push('');
142
142
 
143
143
  if (!baseline) {
144
- lines.push(`> Baseline against \`${baselineRef}\` not found — showing the full current scan as added. Run \`/pr-augment --persist-baseline ${baselineRef}\` from \`${baselineRef}\` to enable diff mode.`);
144
+ lines.push(`> Baseline against \`${baselineRef}\` not found — showing the full current scan as added. Run \`/compliance --pr --persist-baseline ${baselineRef}\` from \`${baselineRef}\` to enable diff mode.`);
145
145
  lines.push('');
146
146
  }
147
147
 
@@ -7,8 +7,8 @@
7
7
  // Decision tree (cheap, no scan):
8
8
  // - No prior scan? → run /scan first
9
9
  // - Prior scan, criticals open? → run /fix --all --critical
10
- // - Prior scan, highs open? → /fix --all --high OR /show-findings
11
- // - Prior scan, only mediums? → /report-card
10
+ // - Prior scan, highs open? → /fix --all --high OR /triage --show
11
+ // - Prior scan, only mediums? → /posture --report-card
12
12
  // - All clean? → /security-badge (celebrate + share)
13
13
  // - Pre-deploy intent (--launch flag, or no scan in 7 days)? → /launch-check
14
14
  //
@@ -27,7 +27,34 @@ function ageHours(fp) {
27
27
  return (Date.now() - fs.statSync(fp).mtimeMs) / 3_600_000;
28
28
  }
29
29
 
30
- export function decide({ scanRoot, intent }) {
30
+ // Compare the two most-recent scan-history entries to tell the user whether
31
+ // they're trending safer or more exposed. Returns {} when there isn't enough
32
+ // history to say anything honest. (premortem: never invent a trend from one
33
+ // data point.)
34
+ export function computeScanTrend(scanRoot) {
35
+ const hist = readJson(path.join(scanRoot, '.agentic-security', 'scan-history.json'));
36
+ if (!Array.isArray(hist) || hist.length < 2) return {};
37
+ const cur = hist[hist.length - 1];
38
+ const prev = hist[hist.length - 2];
39
+ const sevOf = (e) => (e?.critical || 0) + (e?.high || 0);
40
+ const delta = sevOf(cur) - sevOf(prev);
41
+ let trend = 'flat';
42
+ if (delta < 0) trend = 'improving';
43
+ else if (delta > 0) trend = 'regressing';
44
+ const n = Math.abs(delta);
45
+ const whatChanged =
46
+ trend === 'improving' ? `${n} fewer critical+high than last scan — keep going.`
47
+ : trend === 'regressing' ? `${n} more critical+high than last scan — new risk crept in.`
48
+ : 'No change in critical+high since last scan.';
49
+ return { trend, whatChanged };
50
+ }
51
+
52
+ export function decide(opts) {
53
+ const base = baseDecision(opts);
54
+ return { ...base, ...computeScanTrend(opts.scanRoot) };
55
+ }
56
+
57
+ function baseDecision({ scanRoot, intent }) {
31
58
  const stateDir = path.join(scanRoot, '.agentic-security');
32
59
  const lastScan = readJson(path.join(stateDir, 'last-scan.json'));
33
60
  const scanAge = ageHours(path.join(stateDir, 'last-scan.json'));
@@ -71,7 +98,7 @@ export function decide({ scanRoot, intent }) {
71
98
  if (sev.high > 0) {
72
99
  return {
73
100
  action: 'review-high',
74
- command: 'claude /show-findings',
101
+ command: 'claude /triage --show',
75
102
  reason: `${sev.high} high finding(s). Review and triage before fixing.`,
76
103
  };
77
104
  }
@@ -85,7 +112,7 @@ export function decide({ scanRoot, intent }) {
85
112
  if (sev.medium > 0) {
86
113
  return {
87
114
  action: 'report-card',
88
- command: 'claude /report-card',
115
+ command: 'claude /posture --report-card',
89
116
  reason: `Only mediums remain. Get a letter-grade snapshot and pick what's worth fixing.`,
90
117
  };
91
118
  }
@@ -102,8 +129,11 @@ export function explain(decision) {
102
129
  ``,
103
130
  ` Action: ${decision.action}`,
104
131
  ` Why: ${decision.reason}`,
105
- ` Run: ${decision.command}`,
106
- ``,
107
132
  ];
133
+ if (decision.trend) {
134
+ const arrow = decision.trend === 'improving' ? '↓' : decision.trend === 'regressing' ? '↑' : '→';
135
+ lines.push(` Trend: ${arrow} ${decision.whatChanged}`);
136
+ }
137
+ lines.push(` Run: ${decision.command}`, ``);
108
138
  return lines.join('\n');
109
139
  }
@@ -0,0 +1,26 @@
1
+ # Data Protection Impact Assessment (DPIA)
2
+
3
+ Generated by agentic-security scanner on 2026-05-31.
4
+
5
+ This is an automated DPIA scaffold derived from static analysis.
6
+ It must be reviewed and completed by a privacy officer before use.
7
+
8
+ ## Data classes identified
9
+
10
+ ## Privacy-related findings
11
+
12
+ | Severity | File:Line | Class → Sink | Description |
13
+ |---|---|---|---|
14
+
15
+ ## Regulatory framework mapping
16
+
17
+ - **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
18
+ - **CCPA §1798.130** — Notice + access rights for collected personal information.
19
+
20
+ ## Reviewer checklist
21
+
22
+ - [ ] Confirm each PII field's collection has a documented lawful basis
23
+ - [ ] Confirm retention period for each class is documented
24
+ - [ ] Confirm DSAR (data subject access request) workflow exists
25
+ - [ ] Confirm encryption at rest + in transit for each class
26
+ - [ ] Confirm logging of PII access for audit (where applicable)
@@ -0,0 +1,6 @@
1
+ {
2
+ "sha": "7d45b5e03804aac084b4a2b4dc8c6f10107d2005",
3
+ "ts": "2026-05-30T05:08:01.641Z",
4
+ "componentCount": 0,
5
+ "components": []
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sha": "f94be839aa6a1011f53da016416a561698b8f338",
3
+ "ts": "2026-05-31T16:06:10.902Z",
4
+ "componentCount": 0,
5
+ "components": []
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "entities": [],
3
+ "boundaries": [],
4
+ "assets": [],
5
+ "threats": [],
6
+ "attackTrees": []
7
+ }
@@ -0,0 +1,22 @@
1
+ # Threat Model (auto-generated)
2
+
3
+ Generated by agentic-security on 2026-05-31.
4
+
5
+ This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.
6
+
7
+ ## Entities + boundaries
8
+
9
+ ```mermaid
10
+ flowchart TB
11
+ subgraph External
12
+ end
13
+ subgraph Application
14
+ end
15
+ ```
16
+
17
+ ## Assets
18
+
19
+
20
+ ## STRIDE threats
21
+
22
+ ## Attack trees
@@ -973,7 +973,7 @@ export function toShipVerdict(scan, options = {}) {
973
973
  // CONFIRMED: surface validator-confirmed criticals as a trust signal —
974
974
  // distinguishes "tool said so" from "tool built a PoC and it ran."
975
975
  if (confirmedCount > 0) {
976
- lines.push(c(` ✓ ${confirmedCount} CONFIRMED (PoC built by /validate-findings)`, '\x1b[1;32m'));
976
+ lines.push(c(` ✓ ${confirmedCount} CONFIRMED (PoC built by /triage --validate)`, '\x1b[1;32m'));
977
977
  }
978
978
  lines.push('');
979
979
 
@@ -1001,6 +1001,23 @@ export function toShipVerdict(scan, options = {}) {
1001
1001
  } else if (advisoryCount > 0) {
1002
1002
  lines.push(c(` ${advisoryCount} advisory item${advisoryCount === 1 ? '' : 's'} — run /security-scan-all --firehose to see them.`, DIM));
1003
1003
  }
1004
+ // Coverage-honesty line (#5/#6): show the scan's blind spots — which
1005
+ // languages got flow analysis vs pattern-only, what was skipped, and how
1006
+ // many dangerous-looking calls had no finding. One concise line, not bloat.
1007
+ const _covMeta = scan._scanMeta;
1008
+ if (_covMeta && _covMeta.analysisTier) {
1009
+ const t = _covMeta.analysisTier;
1010
+ const pat = Object.keys(t.patternOnly || {});
1011
+ const segs = [`${_covMeta.filesScanned ?? '?'} files`];
1012
+ const ir = Object.keys(t.irTaint || {});
1013
+ if (ir.length) segs.push(`flow=[${ir.join(',')}]`);
1014
+ if (pat.length) segs.push(`pattern-only=[${pat.join(',')}]`);
1015
+ const skipped = (_covMeta.filesSkipped || 0) + (_covMeta.filesDenseSkipped || 0);
1016
+ if (skipped) segs.push(`${skipped} skipped`);
1017
+ if (_covMeta.unmodeledSinkCandidates?.count) segs.push(`${_covMeta.unmodeledSinkCandidates.count} unmodeled-sink candidate${_covMeta.unmodeledSinkCandidates.count === 1 ? '' : 's'}`);
1018
+ lines.push('');
1019
+ lines.push(c(' Coverage: ' + segs.join(' · '), DIM));
1020
+ }
1004
1021
  lines.push('');
1005
1022
  lines.push(c(' 🛡 agentic-security · created by ClearCapabilities.Com', DIM));
1006
1023
  return lines.join('\n');
@@ -0,0 +1,26 @@
1
+ # Data Protection Impact Assessment (DPIA)
2
+
3
+ Generated by agentic-security scanner on 2026-06-01.
4
+
5
+ This is an automated DPIA scaffold derived from static analysis.
6
+ It must be reviewed and completed by a privacy officer before use.
7
+
8
+ ## Data classes identified
9
+
10
+ ## Privacy-related findings
11
+
12
+ | Severity | File:Line | Class → Sink | Description |
13
+ |---|---|---|---|
14
+
15
+ ## Regulatory framework mapping
16
+
17
+ - **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
18
+ - **CCPA §1798.130** — Notice + access rights for collected personal information.
19
+
20
+ ## Reviewer checklist
21
+
22
+ - [ ] Confirm each PII field's collection has a documented lawful basis
23
+ - [ ] Confirm retention period for each class is documented
24
+ - [ ] Confirm DSAR (data subject access request) workflow exists
25
+ - [ ] Confirm encryption at rest + in transit for each class
26
+ - [ ] Confirm logging of PII access for audit (where applicable)
@@ -0,0 +1,65 @@
1
+ {
2
+ "generatedAt": "2026-06-01T03:15:31.819Z",
3
+ "summary": {
4
+ "total": 1,
5
+ "hndlCritical": 1,
6
+ "standard": 0,
7
+ "filesAffected": 1,
8
+ "primitivesNeeded": [
9
+ "ML-KEM-768"
10
+ ]
11
+ },
12
+ "milestones": [
13
+ {
14
+ "id": "M1",
15
+ "title": "Inventory & policy",
16
+ "target": "90 days",
17
+ "owner": "security",
18
+ "items": [
19
+ "Confirm scanner findings against design docs",
20
+ "Adopt PQC migration policy (CNSA 2.0 / NIST IR 8547 alignment)",
21
+ "Establish KMS support for hybrid keys"
22
+ ]
23
+ },
24
+ {
25
+ "id": "M2",
26
+ "title": "HNDL-critical paths to PQ-hybrid",
27
+ "target": "180 days",
28
+ "owner": "platform",
29
+ "items": [
30
+ {
31
+ "finding": "pqc-rsa-encrypt:post-quantum-crypto.js:136",
32
+ "file": "post-quantum-crypto.js",
33
+ "line": 136,
34
+ "replacement": "X25519+ML-KEM-768 (CNSA 2.0 transitional)"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "id": "M3",
40
+ "title": "Standard signing/KEX migration",
41
+ "target": "12 months",
42
+ "owner": "platform",
43
+ "items": []
44
+ },
45
+ {
46
+ "id": "M4",
47
+ "title": "Deprecate classical primitives",
48
+ "target": "24 months",
49
+ "owner": "security",
50
+ "items": [
51
+ "Remove dual-stack libraries once peers are PQ-capable",
52
+ "Rotate root CA / long-lived signing keys to ML-DSA"
53
+ ]
54
+ }
55
+ ],
56
+ "perFile": {
57
+ "post-quantum-crypto.js": {
58
+ "count": 1,
59
+ "subfamilies": [
60
+ "pqc-rsa-encrypt"
61
+ ],
62
+ "hndlCritical": true
63
+ }
64
+ }
65
+ }