@clear-capabilities/agentic-security-scanner 0.78.0 → 0.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.
Files changed (126) hide show
  1. package/bin/.agentic-security/findings.json +16 -16
  2. package/bin/.agentic-security/last-scan.json +16 -16
  3. package/bin/.agentic-security/last-scan.json.sig +1 -1
  4. package/bin/.agentic-security/scan-history.json +51 -0
  5. package/bin/.agentic-security/streak.json +5 -5
  6. package/bin/agentic-security.js +22 -7
  7. package/dist/178.index.js +1 -1
  8. package/dist/333.index.js +283 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/476.index.js +5 -5
  11. package/dist/637.index.js +1 -1
  12. package/dist/700.index.js +138 -0
  13. package/dist/718.index.js +53 -0
  14. package/dist/838.index.js +1 -1
  15. package/dist/985.index.js +95 -1
  16. package/dist/agentic-security.mjs +83 -83
  17. package/dist/agentic-security.mjs.sha256 +1 -1
  18. package/package.json +6 -4
  19. package/src/.agentic-security/findings.json +29799 -7803
  20. package/src/.agentic-security/last-scan.json +29799 -7803
  21. package/src/.agentic-security/last-scan.json.sig +1 -1
  22. package/src/.agentic-security/scan-history.json +5119 -2611
  23. package/src/.agentic-security/streak.json +6 -6
  24. package/src/dataflow/.agentic-security/findings.json +2879 -308
  25. package/src/dataflow/.agentic-security/last-scan.json +2879 -308
  26. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
  27. package/src/dataflow/.agentic-security/scan-history.json +68 -520
  28. package/src/dataflow/.agentic-security/streak.json +6 -7
  29. package/src/dataflow/cross-service-taint.js +201 -0
  30. package/src/dataflow/engine.js +52 -8
  31. package/src/dataflow/formal-verify.js +204 -0
  32. package/src/dataflow/ifds-precise.js +222 -0
  33. package/src/dataflow/k2-summary-cache.js +153 -0
  34. package/src/dataflow/lib-taint-summaries.js +198 -0
  35. package/src/dataflow/privacy-taint.js +205 -0
  36. package/src/dataflow/smt-feasibility.js +189 -0
  37. package/src/engine.js +890 -132
  38. package/src/integrations/index.js +2 -1
  39. package/src/ir/.agentic-security/findings.json +240 -6
  40. package/src/ir/.agentic-security/last-scan.json +240 -6
  41. package/src/ir/.agentic-security/last-scan.json.sig +1 -1
  42. package/src/ir/.agentic-security/scan-history.json +16 -594
  43. package/src/ir/.agentic-security/streak.json +8 -9
  44. package/src/ir/callgraph.js +27 -7
  45. package/src/ir/cpp-preprocessor.js +142 -0
  46. package/src/ir/csharp-ir.js +604 -0
  47. package/src/ir/universal-ir.js +403 -0
  48. package/src/llm-validator/index.js +7 -5
  49. package/src/mcp/.agentic-security/findings.json +8632 -0
  50. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  51. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  52. package/src/mcp/.agentic-security/scan-history.json +143 -0
  53. package/src/mcp/.agentic-security/streak.json +20 -0
  54. package/src/mcp/audit.js +5 -0
  55. package/src/mcp/tools.js +90 -1
  56. package/src/posture/.agentic-security/findings.json +16809 -4367
  57. package/src/posture/.agentic-security/last-scan.json +16809 -4367
  58. package/src/posture/.agentic-security/last-scan.json.sig +1 -1
  59. package/src/posture/.agentic-security/scan-history.json +6689 -177
  60. package/src/posture/.agentic-security/streak.json +8 -7
  61. package/src/posture/api-contract.js +193 -0
  62. package/src/posture/attack-taxonomy.js +227 -0
  63. package/src/posture/calibration-drift.js +2 -1
  64. package/src/posture/calibration.js +3 -2
  65. package/src/posture/compliance-policy.js +218 -0
  66. package/src/posture/composite-risk.js +122 -0
  67. package/src/posture/csharp-analysis.js +330 -0
  68. package/src/posture/exploit-bundle.js +210 -0
  69. package/src/posture/federated-learning.js +172 -0
  70. package/src/posture/fix-history.js +8 -2
  71. package/src/posture/license-attributions.js +94 -0
  72. package/src/posture/license-graph.js +238 -0
  73. package/src/posture/pqc-migration-plan.js +158 -0
  74. package/src/posture/profile.js +4 -5
  75. package/src/posture/reachability-filter.js +33 -2
  76. package/src/posture/realtime-cve-monitor.js +214 -0
  77. package/src/posture/rule-overrides.js +2 -3
  78. package/src/posture/rule-pack-signing.js +2 -3
  79. package/src/posture/rule-synthesis.js +5 -6
  80. package/src/posture/runtime-correlation.js +174 -0
  81. package/src/posture/sbom-diff.js +171 -0
  82. package/src/posture/sca-policy.js +235 -0
  83. package/src/posture/sca-upgrade.js +259 -0
  84. package/src/posture/security-trend.js +4 -7
  85. package/src/posture/state-dir.js +124 -0
  86. package/src/posture/streak.js +3 -0
  87. package/src/posture/suppressions.js +5 -8
  88. package/src/posture/threat-model-auto.js +268 -0
  89. package/src/posture/triage-learning.js +170 -0
  90. package/src/posture/triage.js +29 -6
  91. package/src/posture/validator-metrics.js +3 -6
  92. package/src/sast/.agentic-security/findings.json +996 -32
  93. package/src/sast/.agentic-security/last-scan.json +996 -32
  94. package/src/sast/.agentic-security/last-scan.json.sig +1 -1
  95. package/src/sast/.agentic-security/scan-history.json +565 -32
  96. package/src/sast/.agentic-security/streak.json +10 -8
  97. package/src/sast/_secret-entropy.js +145 -0
  98. package/src/sast/cloud-iam.js +312 -0
  99. package/src/sast/cpp.js +138 -4
  100. package/src/sast/crypto-protocol.js +388 -0
  101. package/src/sast/csharp-tokenizer.js +392 -0
  102. package/src/sast/csharp.js +924 -138
  103. package/src/sast/dapp-frontend.js +200 -0
  104. package/src/sast/db-taint.js +24 -0
  105. package/src/sast/k8s-admission.js +271 -0
  106. package/src/sast/llm-app.js +272 -0
  107. package/src/sast/ml-supply-chain.js +259 -0
  108. package/src/sast/mobile.js +224 -0
  109. package/src/sast/post-quantum-crypto.js +348 -0
  110. package/src/sast/rust.js +26 -0
  111. package/src/sast/web3-advanced.js +375 -0
  112. package/src/sca/.agentic-security/findings.json +6044 -171
  113. package/src/sca/.agentic-security/last-scan.json +6044 -171
  114. package/src/sca/.agentic-security/last-scan.json.sig +1 -1
  115. package/src/sca/.agentic-security/scan-history.json +83 -6
  116. package/src/sca/.agentic-security/streak.json +9 -9
  117. package/src/sca/CLAUDE.md +161 -0
  118. package/src/sca/binary-metadata.js +146 -0
  119. package/src/sca/py-package-functions.js +118 -0
  120. package/src/sca/sigstore-verify.js +215 -0
  121. package/src/sca/vendor-detect.js +53 -0
  122. package/src/report/.agentic-security/findings.json +0 -80
  123. package/src/report/.agentic-security/last-scan.json +0 -80
  124. package/src/report/.agentic-security/last-scan.json.sig +0 -1
  125. package/src/report/.agentic-security/scan-history.json +0 -35
  126. package/src/report/.agentic-security/streak.json +0 -22
@@ -0,0 +1,238 @@
1
+ // License-graph supply-chain analyzer — Item #5 of the world-class+3 plan.
2
+ //
3
+ // Extends posture/license-policy.js (per-component allow/deny/review) with:
4
+ //
5
+ // 1. TRANSITIVE COPYLEFT CONTAMINATION
6
+ // An MIT-licensed direct dep that pulls in a GPL/AGPL transitive
7
+ // dep. Today's per-component check passes because the direct dep is
8
+ // MIT — but the GPL is the actual exposure.
9
+ //
10
+ // 2. RELICENSING-RISK LICENSES
11
+ // BSL, SSPL, Elastic 2.0, Common Clause, Server Side Public License,
12
+ // Functional Source License, Sustainable Use License, etc.
13
+ // Permissive at first install — later versions or downstream usage
14
+ // may breach the additional terms.
15
+ //
16
+ // 3. DISTRIBUTION-MODE-AWARE POLICY
17
+ // "SaaS" vs "Binary" vs "Library" have radically different obligations.
18
+ // AGPL is fine for proprietary internal usage but kills SaaS;
19
+ // GPL is fine for a SaaS web app but kills a published library;
20
+ // LGPL static-link concerns only apply to native binary distribution.
21
+ //
22
+ // 4. DUAL-LICENSE TRAP DETECTION
23
+ // Packages declared as `(MIT OR Apache-2.0)` are usually fine, but
24
+ // `(GPL-2.0 OR Commercial)` are a contractual trap — the open option
25
+ // auto-converts your code to GPL unless you've signed a commercial
26
+ // agreement.
27
+ //
28
+ // 5. LICENSE-CHANGE DETECTION
29
+ // Packages known to have relicensed (Elastic, Redis, Sentry,
30
+ // HashiCorp Terraform, MongoDB) — flag the boundary version.
31
+ //
32
+ // Default mode: SaaS. Override via
33
+ // .agentic-security/license-policy.yml `distributionMode:`.
34
+ //
35
+ // Opt-out: AGENTIC_SECURITY_NO_LICENSE_GRAPH=1
36
+
37
+ import * as fs from 'node:fs';
38
+ import * as path from 'node:path';
39
+
40
+ // ── License taxonomy ───────────────────────────────────────────────────────
41
+
42
+ const LICENSE_FAMILIES = {
43
+ permissive: new Set(['MIT', 'APACHE-2.0', 'BSD-2-CLAUSE', 'BSD-3-CLAUSE', 'BSD-4-CLAUSE', 'ISC', '0BSD', 'CC0-1.0', 'UNLICENSE', 'WTFPL', 'ZLIB', 'PSF-2.0', 'PYTHON-2.0', 'POSTGRESQL', 'OPENSSL', 'X11']),
44
+ weak_copyleft: new Set(['LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0', 'LGPL-2.1-OR-LATER', 'LGPL-3.0-OR-LATER', 'MPL-1.1', 'MPL-2.0', 'EPL-1.0', 'EPL-2.0', 'CDDL-1.0', 'CDDL-1.1']),
45
+ strong_copyleft: new Set(['GPL-2.0', 'GPL-2.0-ONLY', 'GPL-2.0-OR-LATER', 'GPL-3.0', 'GPL-3.0-ONLY', 'GPL-3.0-OR-LATER']),
46
+ network_copyleft: new Set(['AGPL-1.0', 'AGPL-3.0', 'AGPL-3.0-ONLY', 'AGPL-3.0-OR-LATER']),
47
+ source_available: new Set(['BSL-1.1', 'SSPL-1.0', 'ELASTIC-2.0', 'ELASTIC-1.0', 'COMMONS-CLAUSE', 'FSL-1.0', 'FSL-1.1', 'CONFLUENT-COMMUNITY', 'BUSL-1.1', 'PARITY-7.0.0', 'POLYFORM-NONCOMMERCIAL', 'POLYFORM-PERIMETER', 'POLYFORM-INTERNAL-USE']),
48
+ proprietary: new Set(['UNLICENSED', 'NOLICENSE', 'PROPRIETARY', 'COMMERCIAL']),
49
+ };
50
+
51
+ const KNOWN_RELICENSED = [
52
+ // pkg-name regex → { from, to, atVersion, ecosystem }
53
+ { pkg: /^elasticsearch$/i, from: 'Apache-2.0', to: 'Elastic-2.0 / SSPL', atVersion: '>=7.11.0', ecosystem: 'java/npm' },
54
+ { pkg: /^@elastic\/elasticsearch$/i, from: 'Apache-2.0', to: 'Elastic-2.0 / SSPL', atVersion: '>=8.0.0', ecosystem: 'npm' },
55
+ { pkg: /^redis$/i, from: 'BSD-3-Clause', to: 'RSALv2 / SSPL', atVersion: '>=7.4', ecosystem: 'multi' },
56
+ { pkg: /^@sentry\/.*$/i, from: 'BSD-3-Clause', to: 'FSL-1.1', atVersion: '>=8.0.0', ecosystem: 'npm' },
57
+ { pkg: /^terraform.*$/i, from: 'MPL-2.0', to: 'BSL-1.1', atVersion: '>=1.6.0', ecosystem: 'multi' },
58
+ { pkg: /^mongodb$/i, from: 'AGPL-3.0', to: 'SSPL-1.0', atVersion: '>=4.4', ecosystem: 'multi' },
59
+ { pkg: /^vault$/i, from: 'MPL-2.0', to: 'BSL-1.1', atVersion: '>=1.15.0', ecosystem: 'go' },
60
+ { pkg: /^consul$/i, from: 'MPL-2.0', to: 'BSL-1.1', atVersion: '>=1.17.0', ecosystem: 'go' },
61
+ { pkg: /^cockroachdb?$/i, from: 'Apache-2.0', to: 'CCL (modified)', atVersion: '>=19.2', ecosystem: 'go' },
62
+ ];
63
+
64
+ function _normLicense(s) { return String(s || '').toUpperCase().replace(/[()]/g, '').trim(); }
65
+
66
+ function _classify(license) {
67
+ if (!license) return 'unknown';
68
+ const l = _normLicense(license);
69
+ // Compound: pick worst family.
70
+ if (/\s(?:AND|OR|WITH)\s/i.test(l)) {
71
+ const parts = l.split(/\s+(?:AND|OR|WITH)\s+/i).map(p => p.trim()).filter(Boolean);
72
+ const families = parts.map(_classify);
73
+ // Worst → best: source_available > network_copyleft > strong > weak > permissive > unknown.
74
+ for (const f of ['source_available', 'network_copyleft', 'strong_copyleft', 'weak_copyleft', 'permissive']) {
75
+ if (families.includes(f)) return f;
76
+ }
77
+ return 'unknown';
78
+ }
79
+ for (const [fam, set] of Object.entries(LICENSE_FAMILIES)) {
80
+ if (set.has(l)) return fam;
81
+ }
82
+ return 'unknown';
83
+ }
84
+
85
+ // ── Distribution-mode policy ───────────────────────────────────────────────
86
+
87
+ const DEFAULT_DIST_MODE = 'saas';
88
+
89
+ const DIST_MODE_MATRIX = {
90
+ saas: {
91
+ permissive: { verdict: 'allow', why: 'Compatible with SaaS distribution.' },
92
+ weak_copyleft: { verdict: 'allow', why: 'Weak-copyleft (LGPL/MPL/CDDL/EPL) — SaaS distribution does not trigger reciprocity for unmodified usage.' },
93
+ strong_copyleft: { verdict: 'review', why: 'GPL/CGPL is compatible with SaaS (no distribution of binary) but propagates if you ever publish the source or a derived binary. Confirm internal-only.' },
94
+ network_copyleft: { verdict: 'deny', why: 'AGPL "network use as distribution" — SaaS deployment triggers source-disclosure obligations.' },
95
+ source_available: { verdict: 'deny', why: 'Source-available licenses (BSL/SSPL/Elastic/CommonsClause) restrict competitive SaaS offerings.' },
96
+ proprietary: { verdict: 'deny', why: 'Component has no license / declares proprietary — cannot redistribute.' },
97
+ unknown: { verdict: 'review', why: 'Unknown license — verify via upstream repo.' },
98
+ },
99
+ binary: {
100
+ permissive: { verdict: 'allow', why: 'Compatible with binary distribution.' },
101
+ weak_copyleft: { verdict: 'review', why: 'LGPL has static-linking obligations; MPL has file-level reciprocity. Confirm linkage model.' },
102
+ strong_copyleft: { verdict: 'deny', why: 'GPL copyleft propagates to the entire distributed binary.' },
103
+ network_copyleft: { verdict: 'deny', why: 'AGPL is even more restrictive than GPL for distribution.' },
104
+ source_available: { verdict: 'deny', why: 'Source-available licenses (BSL/SSPL/Elastic/CommonsClause) impose use restrictions that often conflict with binary distribution to customers.' },
105
+ proprietary: { verdict: 'deny', why: 'Component has no license / declares proprietary — cannot bundle.' },
106
+ unknown: { verdict: 'review', why: 'Unknown license — verify via upstream repo.' },
107
+ },
108
+ library: {
109
+ permissive: { verdict: 'allow', why: 'Compatible with library publishing.' },
110
+ weak_copyleft: { verdict: 'review', why: 'LGPL/MPL transitive deps complicate downstream users of YOUR library.' },
111
+ strong_copyleft: { verdict: 'deny', why: 'GPL locks all downstream users of your library into GPL.' },
112
+ network_copyleft: { verdict: 'deny', why: 'AGPL forces downstream users into AGPL.' },
113
+ source_available: { verdict: 'deny', why: 'Source-available licenses block downstream commercial use of your library.' },
114
+ proprietary: { verdict: 'deny', why: 'Component has no license — cannot redistribute via your library.' },
115
+ unknown: { verdict: 'review', why: 'Unknown license — verify via upstream repo.' },
116
+ },
117
+ };
118
+
119
+ // ── Transitive walker ──────────────────────────────────────────────────────
120
+
121
+ function _depPathLabel(c) {
122
+ return `${c.ecosystem || '?'}:${c.name}@${c.version || '?'}`;
123
+ }
124
+
125
+ /**
126
+ * Build the dep graph + collect transitive contamination paths.
127
+ *
128
+ * components — list of component objects (already produced by the engine).
129
+ * expects: { ecosystem, name, version, license, transitive (boolean),
130
+ * importedBy: string[] (optional) }.
131
+ */
132
+ export function analyzeLicenseGraph(components, options) {
133
+ const opts = options || {};
134
+ const mode = (opts.distributionMode || DEFAULT_DIST_MODE).toLowerCase();
135
+ const matrix = DIST_MODE_MATRIX[mode] || DIST_MODE_MATRIX[DEFAULT_DIST_MODE];
136
+ if (!Array.isArray(components) || components.length === 0) {
137
+ return { findings: [], summary: { total: 0, deny: 0, review: 0, allow: 0, unknown: 0 }, distributionMode: mode };
138
+ }
139
+ const byKey = new Map();
140
+ for (const c of components) byKey.set(_depPathLabel(c), c);
141
+ const findings = [];
142
+ const summary = { total: components.length, deny: 0, review: 0, allow: 0, unknown: 0 };
143
+
144
+ for (const c of components) {
145
+ const family = _classify(c.license);
146
+ const verdict = matrix[family] || matrix.unknown;
147
+ summary[verdict.verdict] = (summary[verdict.verdict] || 0) + 1;
148
+ if (verdict.verdict === 'allow') continue;
149
+
150
+ const isTransitive = !!c.transitive;
151
+ let path = [_depPathLabel(c)];
152
+ if (isTransitive && Array.isArray(c.importedBy) && c.importedBy.length) {
153
+ // Walk up the graph (one hop in v1 — sufficient for "direct dep that
154
+ // pulled in this offender").
155
+ path = [c.importedBy[0], _depPathLabel(c)];
156
+ }
157
+
158
+ findings.push({
159
+ id: `license-graph:${_depPathLabel(c)}:${family}:${verdict.verdict}`,
160
+ kind: 'license', family: 'license-graph',
161
+ severity: verdict.verdict === 'deny' ? 'high' : 'low',
162
+ file: c.filePath || 'package.json', line: 0,
163
+ vuln: `${verdict.verdict === 'deny' ? 'License-incompatible' : 'License-review-needed'}: ${c.name}@${c.version || '?'} (${c.license || 'no license'}) under ${mode} distribution mode`,
164
+ description: verdict.why + (isTransitive ? ` Transitive dep pulled in via ${path.slice(0, -1).join(' → ')}.` : ''),
165
+ remediation: verdict.verdict === 'deny'
166
+ ? `Replace ${c.name}@${c.version || '?'} with a permissively-licensed alternative, OR switch to a different distribution mode (set distributionMode: in .agentic-security/license-policy.yml), OR negotiate a commercial license with the upstream.`
167
+ : `Have legal review confirm ${c.license} compatibility with ${mode} distribution. Once approved, add ${c.name} to the policy allow-list.`,
168
+ package: c.name,
169
+ version: c.version,
170
+ ecosystem: c.ecosystem,
171
+ license: c.license || null,
172
+ licenseFamily: family,
173
+ distributionMode: mode,
174
+ isTransitive,
175
+ depPath: path,
176
+ });
177
+ }
178
+
179
+ // ── Dual-license trap detection ─────────────────────────────────────────
180
+ for (const c of components) {
181
+ if (!c.license) continue;
182
+ const lic = _normLicense(c.license);
183
+ if (!/\bOR\b/i.test(lic)) continue;
184
+ const atoms = lic.split(/\s+OR\s+/i).map(s => s.trim());
185
+ const hasCommercial = atoms.some(a => /COMMERCIAL|PROPRIETARY|ENTERPRISE/.test(a));
186
+ const hasStrongCopyleft = atoms.some(a => LICENSE_FAMILIES.strong_copyleft.has(a) || LICENSE_FAMILIES.network_copyleft.has(a));
187
+ if (hasCommercial && hasStrongCopyleft) {
188
+ findings.push({
189
+ id: `license-graph:dual-license-trap:${_depPathLabel(c)}`,
190
+ kind: 'license', family: 'license-dual-trap',
191
+ severity: 'high',
192
+ file: c.filePath || 'package.json', line: 0,
193
+ vuln: `Dual-license trap: ${c.name}@${c.version} offers ${c.license} — the open option is copyleft, the alternative requires a commercial agreement`,
194
+ description: 'Dual GPL-OR-Commercial licensing means: if you have not signed a commercial agreement with the upstream, your usage falls under GPL/AGPL and propagates to your codebase. Common pattern with Qt LGPL/Commercial, MongoDB AGPL/Commercial pre-SSPL, GraalVM.',
195
+ remediation: 'Verify with legal whether a commercial agreement is in place. If not, you are bound by the copyleft option — propagate that to your distribution mode policy.',
196
+ package: c.name, version: c.version, license: c.license,
197
+ });
198
+ summary.deny = (summary.deny || 0) + 1;
199
+ }
200
+ }
201
+
202
+ // ── Known relicensed packages ───────────────────────────────────────────
203
+ for (const c of components) {
204
+ for (const r of KNOWN_RELICENSED) {
205
+ if (!r.pkg.test(c.name)) continue;
206
+ findings.push({
207
+ id: `license-graph:relicensed:${_depPathLabel(c)}`,
208
+ kind: 'license', family: 'license-relicense',
209
+ severity: 'medium',
210
+ file: c.filePath || 'package.json', line: 0,
211
+ vuln: `${c.name}@${c.version}: upstream relicensed from ${r.from} → ${r.to} (boundary ${r.atVersion})`,
212
+ description: `Upstream relicensing event for ${c.name}. Older versions were ${r.from}; ${r.atVersion} and later are ${r.to}. Verify which side of the boundary your version is on, and update your policy.`,
213
+ remediation: `Pin to a pre-relicense version if the new terms are unacceptable, OR adopt a fork (e.g. OpenSearch for Elasticsearch, Valkey for Redis, OpenTofu for Terraform).`,
214
+ package: c.name, version: c.version, license: c.license, relicenseInfo: r,
215
+ });
216
+ }
217
+ }
218
+
219
+ return { findings, summary, distributionMode: mode };
220
+ }
221
+
222
+ // ── Policy file loader (extends posture/license-policy.js shape) ───────────
223
+
224
+ export function loadLicenseGraphPolicy(scanRoot) {
225
+ if (!scanRoot) return { distributionMode: DEFAULT_DIST_MODE };
226
+ const fp = path.join(scanRoot, '.agentic-security', 'license-policy.yml');
227
+ if (!fs.existsSync(fp)) return { distributionMode: DEFAULT_DIST_MODE };
228
+ try {
229
+ const raw = fs.readFileSync(fp, 'utf8');
230
+ const m = /\bdistributionMode\s*:\s*['"]?(saas|binary|library)['"]?/i.exec(raw);
231
+ return { distributionMode: m ? m[1].toLowerCase() : DEFAULT_DIST_MODE };
232
+ } catch { return { distributionMode: DEFAULT_DIST_MODE }; }
233
+ }
234
+
235
+ export const _internals = {
236
+ LICENSE_FAMILIES, DIST_MODE_MATRIX, KNOWN_RELICENSED,
237
+ _classify, _normLicense,
238
+ };
@@ -0,0 +1,158 @@
1
+ // PQC migration-plan artifact emitter.
2
+ //
3
+ // Aggregates pqc-migration findings (emitted by sast/post-quantum-crypto.js)
4
+ // into a structured plan suitable for an engineering organization to use as
5
+ // a project tracker:
6
+ //
7
+ // .agentic-security/pqc-migration-plan.json
8
+ // .agentic-security/pqc-migration-plan.md
9
+ //
10
+ // Buckets findings by:
11
+ // - HNDL criticality (high-priority — data captured today is harvest-now-decrypt-later
12
+ // exposure when a CRQC arrives)
13
+ // - Use case (signing / encryption / KEX) — drives the replacement primitive
14
+ // - Recommended replacement (ML-KEM-768, ML-DSA-65, etc.)
15
+ // - File / package locality so the plan can be carved into milestones.
16
+ //
17
+ // Cleartext markdown summarises the top recommendations and milestone
18
+ // suggestions; JSON-LD-shaped structured output is consumable by Vanta /
19
+ // Drata / SecureFrame or any custom rollup dashboard.
20
+
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+
24
+ function _byHndl(findings) {
25
+ return {
26
+ hndlCritical: findings.filter(f => f.hndlCritical),
27
+ standard: findings.filter(f => !f.hndlCritical),
28
+ };
29
+ }
30
+
31
+ function _byUseCase(findings) {
32
+ const map = new Map();
33
+ for (const f of findings) {
34
+ const k = f.pqcRecommendation?.primary || 'unspecified';
35
+ if (!map.has(k)) map.set(k, []);
36
+ map.get(k).push(f);
37
+ }
38
+ return map;
39
+ }
40
+
41
+ function _byFile(findings) {
42
+ const map = new Map();
43
+ for (const f of findings) {
44
+ const k = f.file || 'unknown';
45
+ if (!map.has(k)) map.set(k, []);
46
+ map.get(k).push(f);
47
+ }
48
+ return map;
49
+ }
50
+
51
+ export function buildMigrationPlan(allFindings) {
52
+ const pqc = (allFindings || []).filter(f => f.family === 'pqc-migration');
53
+ if (!pqc.length) return null;
54
+ const bySev = _byHndl(pqc);
55
+ const byPrimitive = _byUseCase(pqc);
56
+ const byFile = _byFile(pqc);
57
+ const summary = {
58
+ total: pqc.length,
59
+ hndlCritical: bySev.hndlCritical.length,
60
+ standard: bySev.standard.length,
61
+ filesAffected: byFile.size,
62
+ primitivesNeeded: Array.from(byPrimitive.keys()),
63
+ };
64
+ const milestones = [
65
+ {
66
+ id: 'M1',
67
+ title: 'Inventory & policy',
68
+ target: '90 days',
69
+ owner: 'security',
70
+ items: [
71
+ 'Confirm scanner findings against design docs',
72
+ 'Adopt PQC migration policy (CNSA 2.0 / NIST IR 8547 alignment)',
73
+ 'Establish KMS support for hybrid keys',
74
+ ],
75
+ },
76
+ {
77
+ id: 'M2',
78
+ title: 'HNDL-critical paths to PQ-hybrid',
79
+ target: '180 days',
80
+ owner: 'platform',
81
+ items: bySev.hndlCritical.slice(0, 25).map(f => ({
82
+ finding: f.id, file: f.file, line: f.line,
83
+ replacement: f.pqcRecommendation?.hybrid || f.pqcRecommendation?.primary,
84
+ })),
85
+ },
86
+ {
87
+ id: 'M3',
88
+ title: 'Standard signing/KEX migration',
89
+ target: '12 months',
90
+ owner: 'platform',
91
+ items: bySev.standard.slice(0, 50).map(f => ({
92
+ finding: f.id, file: f.file, line: f.line,
93
+ replacement: f.pqcRecommendation?.primary,
94
+ })),
95
+ },
96
+ {
97
+ id: 'M4',
98
+ title: 'Deprecate classical primitives',
99
+ target: '24 months',
100
+ owner: 'security',
101
+ items: ['Remove dual-stack libraries once peers are PQ-capable', 'Rotate root CA / long-lived signing keys to ML-DSA'],
102
+ },
103
+ ];
104
+ return {
105
+ generatedAt: new Date().toISOString(),
106
+ summary,
107
+ milestones,
108
+ perFile: Object.fromEntries(
109
+ Array.from(byFile.entries()).map(([file, fs]) => [file, {
110
+ count: fs.length,
111
+ subfamilies: Array.from(new Set(fs.map(f => f.subfamily))),
112
+ hndlCritical: fs.some(f => f.hndlCritical),
113
+ }]),
114
+ ),
115
+ };
116
+ }
117
+
118
+ export function persistMigrationPlan(scanRoot, plan) {
119
+ if (!plan) return null;
120
+ try { fs.mkdirSync(path.join(scanRoot, '.agentic-security'), { recursive: true }); } catch {}
121
+ try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'pqc-migration-plan.json'), JSON.stringify(plan, null, 2)); } catch {}
122
+ try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'pqc-migration-plan.md'), _markdown(plan)); } catch {}
123
+ return plan;
124
+ }
125
+
126
+ function _markdown(plan) {
127
+ const lines = [];
128
+ lines.push('# Post-quantum cryptography migration plan');
129
+ lines.push('');
130
+ lines.push(`Generated ${plan.generatedAt.slice(0, 10)}.`);
131
+ lines.push('');
132
+ lines.push(`**${plan.summary.total}** pre-quantum primitive sites across **${plan.summary.filesAffected}** files. `);
133
+ lines.push(`HNDL-critical: **${plan.summary.hndlCritical}** | Standard: **${plan.summary.standard}**`);
134
+ lines.push('');
135
+ lines.push('## Recommended PQ primitives');
136
+ for (const p of plan.summary.primitivesNeeded) lines.push(`- ${p}`);
137
+ lines.push('');
138
+ for (const m of plan.milestones) {
139
+ lines.push(`## ${m.id} — ${m.title} (target ${m.target}, owner ${m.owner})`);
140
+ if (Array.isArray(m.items) && m.items.length) {
141
+ for (const it of m.items.slice(0, 20)) {
142
+ if (typeof it === 'string') lines.push(`- ${it}`);
143
+ else lines.push(`- \`${it.file}:${it.line}\` → ${it.replacement || '(see finding)'}`);
144
+ }
145
+ if (m.items.length > 20) lines.push(`- … ${m.items.length - 20} more`);
146
+ }
147
+ lines.push('');
148
+ }
149
+ lines.push('## References');
150
+ lines.push('- NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA)');
151
+ lines.push('- NIST IR 8547 — Transition to Post-Quantum Cryptographic Standards');
152
+ lines.push('- CNSA 2.0 — Commercial National Security Algorithm Suite, Sept 2022');
153
+ lines.push('- RFC 9794 — X25519MLKEM768 hybrid key exchange for TLS 1.3');
154
+ lines.push('- Open Quantum Safe project (liboqs, oqs-provider for OpenSSL 3)');
155
+ return lines.join('\n');
156
+ }
157
+
158
+ export const _internals = { _byHndl, _byUseCase, _byFile, _markdown };
@@ -6,6 +6,7 @@
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import * as yaml from 'js-yaml';
9
+ import { statePath, safeWriteState, resolveProjectRoot } from './state-dir.js';
9
10
 
10
11
  export const PROFILES = ['vibecoder', 'pro'];
11
12
 
@@ -35,7 +36,7 @@ export const DEFAULTS = {
35
36
  };
36
37
 
37
38
  function _profilePath(scanRoot) {
38
- return path.join(scanRoot || process.cwd(), '.agentic-security', 'profile.yml');
39
+ return statePath(scanRoot, 'profile.yml');
39
40
  }
40
41
 
41
42
  export function loadProfile(scanRoot) {
@@ -52,10 +53,8 @@ export function loadProfile(scanRoot) {
52
53
 
53
54
  export function saveProfile(scanRoot, updates) {
54
55
  const fp = _profilePath(scanRoot);
55
- fs.mkdirSync(path.dirname(fp), { recursive: true });
56
56
  const current = loadProfile(scanRoot);
57
57
  const next = { ...current, ...updates };
58
- // Strip values equal to defaults so the file stays minimal.
59
58
  const defaults = DEFAULTS[next.profile];
60
59
  const out = {};
61
60
  for (const k of Object.keys(next)) {
@@ -63,7 +62,7 @@ export function saveProfile(scanRoot, updates) {
63
62
  out[k] = next[k];
64
63
  }
65
64
  if (!('profile' in out)) out.profile = next.profile;
66
- fs.writeFileSync(fp, yaml.dump(out));
65
+ safeWriteState(fp, yaml.dump(out));
67
66
  return next;
68
67
  }
69
68
 
@@ -71,7 +70,7 @@ export function saveProfile(scanRoot, updates) {
71
70
  // Returns 'pro' if the repo has signals indicating professional security work,
72
71
  // otherwise 'vibecoder'. Run only on first scan.
73
72
  export function detectProfile(scanRoot) {
74
- const root = scanRoot || process.cwd();
73
+ const root = resolveProjectRoot(scanRoot);
75
74
  const signals = ['SECURITY.md', '.github/workflows/security.yml', '.semgrep.yml',
76
75
  '.snyk', 'codeql-config.yml', 'compliance/', 'docs/threat-model.md'];
77
76
  for (const s of signals) {
@@ -5,6 +5,11 @@
5
5
  // module turns that signal into a precision lever: findings marked reachable=
6
6
  // false are demoted to severity 'info' with f.unreachable = true.
7
7
  //
8
+ // Phase 2 / Item 4 (SCA improvement plan): also demote SCA findings whose
9
+ // reachabilityTier indicates the vulnerable code is not reached by any route
10
+ // handler. Critical+manifest-only on a 500-dep transitive graph would
11
+ // otherwise drown out real reachable bugs.
12
+ //
8
13
  // Disabled when scanRoot/--include-unreachable signals are present, or when
9
14
  // AGENTIC_SECURITY_INCLUDE_UNREACHABLE=1 is set.
10
15
 
@@ -15,6 +20,19 @@ const SEVERITY_DEMOTE = {
15
20
  low: 'info',
16
21
  };
17
22
 
23
+ // SCA reachability tiers, ordered from highest urgency to lowest. A tier
24
+ // in DEMOTE_SCA_TIERS triggers severity demotion; a tier NOT in the set
25
+ // keeps full severity. route-reachable-via-function and function-reachable
26
+ // both keep full severity because the vulnerable function is provably
27
+ // called; import-reachable also keeps full severity (imported = uncertain
28
+ // but plausible). The lower three tiers get demoted.
29
+ const DEMOTE_SCA_TIERS = new Set([
30
+ 'unreachable', // function never called from project
31
+ 'build-only', // dev/build-time dependency only
32
+ 'manifest-only', // declared, but no use observed
33
+ 'transitive-only', // transitive dep, scope unclear
34
+ ]);
35
+
18
36
  export function demoteUnreachable(findings, opts = {}) {
19
37
  if (!Array.isArray(findings)) return;
20
38
  if (opts.includeUnreachable || process.env.AGENTIC_SECURITY_INCLUDE_UNREACHABLE === '1') return;
@@ -26,9 +44,22 @@ export function demoteUnreachable(findings, opts = {}) {
26
44
  if (!haveRoutes) return;
27
45
  for (const f of findings) {
28
46
  if (!f || typeof f !== 'object') continue;
29
- if (f.reachable !== false) continue;
30
- if (f.type === 'vulnerable_dep') continue;
31
47
  if (f.unreachable) continue;
48
+ // SCA findings: demote based on reachabilityTier instead of f.reachable
49
+ // (the latter isn't meaningful for an SCA finding — components don't
50
+ // have call sites in the SAST sense).
51
+ if (f.type === 'vulnerable_dep') {
52
+ if (!DEMOTE_SCA_TIERS.has(f.reachabilityTier)) continue;
53
+ const beforeSca = f.severity;
54
+ const afterSca = SEVERITY_DEMOTE[beforeSca];
55
+ if (!afterSca || beforeSca === afterSca) continue;
56
+ f.severity = afterSca;
57
+ f.unreachable = true;
58
+ f._reachabilityDemoted = beforeSca;
59
+ f._reachabilityDemoteReason = `tier:${f.reachabilityTier}`;
60
+ continue;
61
+ }
62
+ if (f.reachable !== false) continue;
32
63
  // Source has an explicit HTTP/DOM/Form/URL category → engine is confident
33
64
  // it's a user-input source even though no route was linked. Don't demote.
34
65
  if (f.source && f.source.category && /HTTP|DOM|Form|URL|Query/i.test(f.source.category)) continue;