kirograph 0.16.1 → 0.19.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 (365) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +103 -1605
  3. package/dist/architecture/layers/dart.js +112 -0
  4. package/dist/architecture/layers/dart.js.map +7 -0
  5. package/dist/architecture/layers/index.js +3 -1
  6. package/dist/architecture/layers/index.js.map +2 -2
  7. package/dist/bin/banner.js +74 -6
  8. package/dist/bin/banner.js.map +2 -2
  9. package/dist/bin/commands/affected.js +40 -5
  10. package/dist/bin/commands/affected.js.map +2 -2
  11. package/dist/bin/commands/attack-surface.js +157 -0
  12. package/dist/bin/commands/attack-surface.js.map +7 -0
  13. package/dist/bin/commands/benchmark.js +206 -0
  14. package/dist/bin/commands/benchmark.js.map +7 -0
  15. package/dist/bin/commands/budget.js +70 -0
  16. package/dist/bin/commands/budget.js.map +7 -0
  17. package/dist/bin/commands/communities.js +77 -0
  18. package/dist/bin/commands/communities.js.map +7 -0
  19. package/dist/bin/commands/data.js +665 -0
  20. package/dist/bin/commands/data.js.map +7 -0
  21. package/dist/bin/commands/dep-confusion.js +119 -0
  22. package/dist/bin/commands/dep-confusion.js.map +7 -0
  23. package/dist/bin/commands/export.js +304 -10
  24. package/dist/bin/commands/export.js.map +2 -2
  25. package/dist/bin/commands/flows.js +97 -0
  26. package/dist/bin/commands/flows.js.map +7 -0
  27. package/dist/bin/commands/help.js +386 -100
  28. package/dist/bin/commands/help.js.map +2 -2
  29. package/dist/bin/commands/install.js +90 -8
  30. package/dist/bin/commands/install.js.map +2 -2
  31. package/dist/bin/commands/licenses.js +201 -0
  32. package/dist/bin/commands/licenses.js.map +7 -0
  33. package/dist/bin/commands/reachability.js +160 -0
  34. package/dist/bin/commands/reachability.js.map +7 -0
  35. package/dist/bin/commands/read.js +100 -0
  36. package/dist/bin/commands/read.js.map +7 -0
  37. package/dist/bin/commands/refactor.js +116 -0
  38. package/dist/bin/commands/refactor.js.map +7 -0
  39. package/dist/bin/commands/remediation.js +133 -0
  40. package/dist/bin/commands/remediation.js.map +7 -0
  41. package/dist/bin/commands/sbom.js +76 -0
  42. package/dist/bin/commands/sbom.js.map +7 -0
  43. package/dist/bin/commands/security-ci-report.js +267 -0
  44. package/dist/bin/commands/security-ci-report.js.map +7 -0
  45. package/dist/bin/commands/security-export.js +1349 -0
  46. package/dist/bin/commands/security-export.js.map +7 -0
  47. package/dist/bin/commands/security-flows.js +126 -0
  48. package/dist/bin/commands/security-flows.js.map +7 -0
  49. package/dist/bin/commands/security-secrets.js +170 -0
  50. package/dist/bin/commands/security-secrets.js.map +7 -0
  51. package/dist/bin/commands/security.js +193 -0
  52. package/dist/bin/commands/security.js.map +7 -0
  53. package/dist/bin/commands/staleness.js +144 -0
  54. package/dist/bin/commands/staleness.js.map +7 -0
  55. package/dist/bin/commands/status.js +102 -2
  56. package/dist/bin/commands/status.js.map +2 -2
  57. package/dist/bin/commands/supply-chain.js +138 -0
  58. package/dist/bin/commands/supply-chain.js.map +7 -0
  59. package/dist/bin/commands/uninit.js +109 -60
  60. package/dist/bin/commands/uninit.js.map +2 -2
  61. package/dist/bin/commands/vex.js +76 -0
  62. package/dist/bin/commands/vex.js.map +7 -0
  63. package/dist/bin/commands/vuln-suppress.js +95 -0
  64. package/dist/bin/commands/vuln-suppress.js.map +7 -0
  65. package/dist/bin/commands/vulns.js +382 -0
  66. package/dist/bin/commands/vulns.js.map +7 -0
  67. package/dist/bin/installer/auto-detect.js +125 -0
  68. package/dist/bin/installer/auto-detect.js.map +7 -0
  69. package/dist/bin/installer/cli-agent.js +28 -5
  70. package/dist/bin/installer/cli-agent.js.map +2 -2
  71. package/dist/bin/installer/common.js +47 -0
  72. package/dist/bin/installer/common.js.map +2 -2
  73. package/dist/bin/installer/config-prompt.js +39 -1
  74. package/dist/bin/installer/config-prompt.js.map +2 -2
  75. package/dist/bin/installer/detect.js +309 -0
  76. package/dist/bin/installer/detect.js.map +7 -0
  77. package/dist/bin/installer/index.js +36 -1
  78. package/dist/bin/installer/index.js.map +2 -2
  79. package/dist/bin/installer/instructions.js +195 -3
  80. package/dist/bin/installer/instructions.js.map +2 -2
  81. package/dist/bin/installer/steering.js +440 -0
  82. package/dist/bin/installer/steering.js.map +2 -2
  83. package/dist/bin/installer/targets/aider.js +3 -3
  84. package/dist/bin/installer/targets/aider.js.map +2 -2
  85. package/dist/bin/installer/targets/amp.js +3 -3
  86. package/dist/bin/installer/targets/amp.js.map +2 -2
  87. package/dist/bin/installer/targets/antigravity.js +35 -6
  88. package/dist/bin/installer/targets/antigravity.js.map +2 -2
  89. package/dist/bin/installer/targets/augment.js +3 -3
  90. package/dist/bin/installer/targets/augment.js.map +2 -2
  91. package/dist/bin/installer/targets/claude.js +49 -5
  92. package/dist/bin/installer/targets/claude.js.map +2 -2
  93. package/dist/bin/installer/targets/cline.js +32 -6
  94. package/dist/bin/installer/targets/cline.js.map +2 -2
  95. package/dist/bin/installer/targets/codex.js +64 -12
  96. package/dist/bin/installer/targets/codex.js.map +2 -2
  97. package/dist/bin/installer/targets/continue.js +2 -2
  98. package/dist/bin/installer/targets/continue.js.map +2 -2
  99. package/dist/bin/installer/targets/copilot-cli.js +101 -0
  100. package/dist/bin/installer/targets/copilot-cli.js.map +7 -0
  101. package/dist/bin/installer/targets/copilot.js +31 -5
  102. package/dist/bin/installer/targets/copilot.js.map +2 -2
  103. package/dist/bin/installer/targets/cursor.js +4 -4
  104. package/dist/bin/installer/targets/cursor.js.map +2 -2
  105. package/dist/bin/installer/targets/devin.js +2 -2
  106. package/dist/bin/installer/targets/devin.js.map +2 -2
  107. package/dist/bin/installer/targets/gemini-cli.js +2 -2
  108. package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
  109. package/dist/bin/installer/targets/generic.js +2 -14
  110. package/dist/bin/installer/targets/generic.js.map +2 -2
  111. package/dist/bin/installer/targets/goose.js +3 -3
  112. package/dist/bin/installer/targets/goose.js.map +2 -2
  113. package/dist/bin/installer/targets/index.js +250 -0
  114. package/dist/bin/installer/targets/index.js.map +2 -2
  115. package/dist/bin/installer/targets/junie.js +2 -2
  116. package/dist/bin/installer/targets/junie.js.map +2 -2
  117. package/dist/bin/installer/targets/kilo.js +2 -2
  118. package/dist/bin/installer/targets/kilo.js.map +2 -2
  119. package/dist/bin/installer/targets/kiro.js +3 -3
  120. package/dist/bin/installer/targets/kiro.js.map +2 -2
  121. package/dist/bin/installer/targets/opencode.js +2 -2
  122. package/dist/bin/installer/targets/opencode.js.map +2 -2
  123. package/dist/bin/installer/targets/openhands.js +3 -3
  124. package/dist/bin/installer/targets/openhands.js.map +2 -2
  125. package/dist/bin/installer/targets/qoder.js +91 -0
  126. package/dist/bin/installer/targets/qoder.js.map +7 -0
  127. package/dist/bin/installer/targets/qwen.js +96 -0
  128. package/dist/bin/installer/targets/qwen.js.map +7 -0
  129. package/dist/bin/installer/targets/replit.js +3 -3
  130. package/dist/bin/installer/targets/replit.js.map +2 -2
  131. package/dist/bin/installer/targets/roo.js +2 -2
  132. package/dist/bin/installer/targets/roo.js.map +2 -2
  133. package/dist/bin/installer/targets/tabnine.js +3 -3
  134. package/dist/bin/installer/targets/tabnine.js.map +2 -2
  135. package/dist/bin/installer/targets/trae.js +5 -5
  136. package/dist/bin/installer/targets/trae.js.map +2 -2
  137. package/dist/bin/installer/targets/warp.js +2 -2
  138. package/dist/bin/installer/targets/warp.js.map +2 -2
  139. package/dist/bin/installer/targets/windsurf.js +35 -6
  140. package/dist/bin/installer/targets/windsurf.js.map +2 -2
  141. package/dist/bin/kirograph.js +74 -6
  142. package/dist/bin/kirograph.js.map +3 -3
  143. package/dist/bin/progress.js +30 -0
  144. package/dist/bin/progress.js.map +2 -2
  145. package/dist/compression/naive-cost.js +25 -0
  146. package/dist/compression/naive-cost.js.map +2 -2
  147. package/dist/compression/tracker.js +91 -2
  148. package/dist/compression/tracker.js.map +2 -2
  149. package/dist/compression/types.js.map +1 -1
  150. package/dist/config.js +126 -3
  151. package/dist/config.js.map +2 -2
  152. package/dist/core/pipeline.js +92 -4
  153. package/dist/core/pipeline.js.map +2 -2
  154. package/dist/data/filters.js +105 -0
  155. package/dist/data/filters.js.map +7 -0
  156. package/dist/data/indexer.js +225 -0
  157. package/dist/data/indexer.js.map +7 -0
  158. package/dist/data/linker.js +150 -0
  159. package/dist/data/linker.js.map +7 -0
  160. package/dist/data/lint.js +109 -0
  161. package/dist/data/lint.js.map +7 -0
  162. package/dist/data/parsers/csv.js +132 -0
  163. package/dist/data/parsers/csv.js.map +7 -0
  164. package/dist/data/parsers/excel.js +88 -0
  165. package/dist/data/parsers/excel.js.map +7 -0
  166. package/dist/data/parsers/index.js +79 -0
  167. package/dist/data/parsers/index.js.map +7 -0
  168. package/dist/data/parsers/json-array.js +89 -0
  169. package/dist/data/parsers/json-array.js.map +7 -0
  170. package/dist/data/parsers/jsonl.js +84 -0
  171. package/dist/data/parsers/jsonl.js.map +7 -0
  172. package/dist/data/parsers/parquet.js +95 -0
  173. package/dist/data/parsers/parquet.js.map +7 -0
  174. package/dist/data/profiler.js +182 -0
  175. package/dist/data/profiler.js.map +7 -0
  176. package/dist/data/queries.js +512 -0
  177. package/dist/data/queries.js.map +7 -0
  178. package/dist/data/types.js +17 -0
  179. package/dist/data/types.js.map +7 -0
  180. package/dist/db/data-schema.sql +61 -0
  181. package/dist/db/database.js +67 -5
  182. package/dist/db/database.js.map +2 -2
  183. package/dist/db/memory-schema.sql +5 -1
  184. package/dist/db/schema.sql +3 -1
  185. package/dist/db/security-schema.sql +72 -0
  186. package/dist/extraction/extractor.js +81 -0
  187. package/dist/extraction/extractor.js.map +2 -2
  188. package/dist/extraction/grammars.js +32 -8
  189. package/dist/extraction/grammars.js.map +2 -2
  190. package/dist/extraction/languages.js +43 -1
  191. package/dist/extraction/languages.js.map +2 -2
  192. package/dist/extraction/notebook.js +300 -0
  193. package/dist/extraction/notebook.js.map +7 -0
  194. package/dist/extraction/wasm/tree-sitter-astro.wasm +0 -0
  195. package/dist/extraction/wasm/tree-sitter-gdscript.wasm +0 -0
  196. package/dist/extraction/wasm/tree-sitter-julia.wasm +0 -0
  197. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  198. package/dist/extraction/wasm/tree-sitter-perl.wasm +0 -0
  199. package/dist/extraction/wasm/tree-sitter-powershell.wasm +0 -0
  200. package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
  201. package/dist/extraction/wasm/tree-sitter-sql.wasm +0 -0
  202. package/dist/extraction/wasm/tree-sitter-verilog.wasm +0 -0
  203. package/dist/frameworks/flutter.js +130 -0
  204. package/dist/frameworks/flutter.js.map +7 -0
  205. package/dist/frameworks/index.js +7 -1
  206. package/dist/frameworks/index.js.map +3 -3
  207. package/dist/graph/communities.js +317 -0
  208. package/dist/graph/communities.js.map +7 -0
  209. package/dist/graph/flows.js +138 -0
  210. package/dist/graph/flows.js.map +7 -0
  211. package/dist/graph/refactor.js +155 -0
  212. package/dist/graph/refactor.js.map +7 -0
  213. package/dist/index.js +1 -1
  214. package/dist/index.js.map +2 -2
  215. package/dist/mcp/cache.js +172 -0
  216. package/dist/mcp/cache.js.map +7 -0
  217. package/dist/mcp/read-modes.js +295 -0
  218. package/dist/mcp/read-modes.js.map +7 -0
  219. package/dist/mcp/server.js +2 -1
  220. package/dist/mcp/server.js.map +2 -2
  221. package/dist/mcp/tool-names.js +29 -1
  222. package/dist/mcp/tool-names.js.map +2 -2
  223. package/dist/mcp/tools.js +1588 -6
  224. package/dist/mcp/tools.js.map +3 -3
  225. package/dist/memory/database.js +32 -2
  226. package/dist/memory/database.js.map +2 -2
  227. package/dist/memory/types.js.map +1 -1
  228. package/dist/resolution/bridges/android-rn.js +298 -0
  229. package/dist/resolution/bridges/android-rn.js.map +7 -0
  230. package/dist/resolution/bridges/expo-modules.js +155 -0
  231. package/dist/resolution/bridges/expo-modules.js.map +7 -0
  232. package/dist/resolution/bridges/flutter-channel.js +385 -0
  233. package/dist/resolution/bridges/flutter-channel.js.map +7 -0
  234. package/dist/resolution/bridges/index.js +80 -0
  235. package/dist/resolution/bridges/index.js.map +7 -0
  236. package/dist/resolution/bridges/native-events.js +188 -0
  237. package/dist/resolution/bridges/native-events.js.map +7 -0
  238. package/dist/resolution/bridges/native-views.js +244 -0
  239. package/dist/resolution/bridges/native-views.js.map +7 -0
  240. package/dist/resolution/bridges/react-native.js +161 -0
  241. package/dist/resolution/bridges/react-native.js.map +7 -0
  242. package/dist/resolution/bridges/swift-objc.js +227 -0
  243. package/dist/resolution/bridges/swift-objc.js.map +7 -0
  244. package/dist/resolution/bridges/turbomodules.js +216 -0
  245. package/dist/resolution/bridges/turbomodules.js.map +7 -0
  246. package/dist/resolution/index.js +61 -2
  247. package/dist/resolution/index.js.map +2 -2
  248. package/dist/security/attack-surface.js +164 -0
  249. package/dist/security/attack-surface.js.map +7 -0
  250. package/dist/security/context-warnings.js +123 -0
  251. package/dist/security/context-warnings.js.map +7 -0
  252. package/dist/security/context-warnings.test.js +300 -0
  253. package/dist/security/context-warnings.test.js.map +7 -0
  254. package/dist/security/data-flows.js +228 -0
  255. package/dist/security/data-flows.js.map +7 -0
  256. package/dist/security/dep-confusion.js +255 -0
  257. package/dist/security/dep-confusion.js.map +7 -0
  258. package/dist/security/errors.js +51 -0
  259. package/dist/security/errors.js.map +7 -0
  260. package/dist/security/export/fix-suggestions.js +58 -0
  261. package/dist/security/export/fix-suggestions.js.map +7 -0
  262. package/dist/security/export/fix-suggestions.test.js +74 -0
  263. package/dist/security/export/fix-suggestions.test.js.map +7 -0
  264. package/dist/security/export/sbom.js +230 -0
  265. package/dist/security/export/sbom.js.map +7 -0
  266. package/dist/security/export/sbom.test.js +288 -0
  267. package/dist/security/export/sbom.test.js.map +7 -0
  268. package/dist/security/export/serialization.js +142 -0
  269. package/dist/security/export/serialization.js.map +7 -0
  270. package/dist/security/export/serialization.test.js +310 -0
  271. package/dist/security/export/serialization.test.js.map +7 -0
  272. package/dist/security/export/vex.js +220 -0
  273. package/dist/security/export/vex.js.map +7 -0
  274. package/dist/security/export/vex.test.js +278 -0
  275. package/dist/security/export/vex.test.js.map +7 -0
  276. package/dist/security/index.js +67 -0
  277. package/dist/security/index.js.map +7 -0
  278. package/dist/security/integrator-transitives.test.js +359 -0
  279. package/dist/security/integrator-transitives.test.js.map +7 -0
  280. package/dist/security/integrator.js +491 -0
  281. package/dist/security/integrator.js.map +7 -0
  282. package/dist/security/integrator.test.js +237 -0
  283. package/dist/security/integrator.test.js.map +7 -0
  284. package/dist/security/license.js +70 -0
  285. package/dist/security/license.js.map +7 -0
  286. package/dist/security/manifest/adapter.js +243 -0
  287. package/dist/security/manifest/adapter.js.map +7 -0
  288. package/dist/security/manifest/adapter.test.js +272 -0
  289. package/dist/security/manifest/adapter.test.js.map +7 -0
  290. package/dist/security/manifest/parser.js +376 -0
  291. package/dist/security/manifest/parser.js.map +7 -0
  292. package/dist/security/manifest/parser.test.js +110 -0
  293. package/dist/security/manifest/parser.test.js.map +7 -0
  294. package/dist/security/manifest/plugins/cargo.js +233 -0
  295. package/dist/security/manifest/plugins/cargo.js.map +7 -0
  296. package/dist/security/manifest/plugins/cargo.test.js +304 -0
  297. package/dist/security/manifest/plugins/cargo.test.js.map +7 -0
  298. package/dist/security/manifest/plugins/composer.js +152 -0
  299. package/dist/security/manifest/plugins/composer.js.map +7 -0
  300. package/dist/security/manifest/plugins/go.js +146 -0
  301. package/dist/security/manifest/plugins/go.js.map +7 -0
  302. package/dist/security/manifest/plugins/go.test.js +341 -0
  303. package/dist/security/manifest/plugins/go.test.js.map +7 -0
  304. package/dist/security/manifest/plugins/gradle.js +159 -0
  305. package/dist/security/manifest/plugins/gradle.js.map +7 -0
  306. package/dist/security/manifest/plugins/hex.js +159 -0
  307. package/dist/security/manifest/plugins/hex.js.map +7 -0
  308. package/dist/security/manifest/plugins/maven.js +128 -0
  309. package/dist/security/manifest/plugins/maven.js.map +7 -0
  310. package/dist/security/manifest/plugins/maven.test.js +389 -0
  311. package/dist/security/manifest/plugins/maven.test.js.map +7 -0
  312. package/dist/security/manifest/plugins/npm.js +284 -0
  313. package/dist/security/manifest/plugins/npm.js.map +7 -0
  314. package/dist/security/manifest/plugins/npm.test.js +298 -0
  315. package/dist/security/manifest/plugins/npm.test.js.map +7 -0
  316. package/dist/security/manifest/plugins/nuget.js +186 -0
  317. package/dist/security/manifest/plugins/nuget.js.map +7 -0
  318. package/dist/security/manifest/plugins/pip.js +123 -0
  319. package/dist/security/manifest/plugins/pip.js.map +7 -0
  320. package/dist/security/manifest/plugins/pip.test.js +208 -0
  321. package/dist/security/manifest/plugins/pip.test.js.map +7 -0
  322. package/dist/security/manifest/plugins/pubspec.js +212 -0
  323. package/dist/security/manifest/plugins/pubspec.js.map +7 -0
  324. package/dist/security/manifest/plugins/pyproject.js +261 -0
  325. package/dist/security/manifest/plugins/pyproject.js.map +7 -0
  326. package/dist/security/manifest/plugins/rubygems.js +185 -0
  327. package/dist/security/manifest/plugins/rubygems.js.map +7 -0
  328. package/dist/security/manifest/plugins/swift.js +200 -0
  329. package/dist/security/manifest/plugins/swift.js.map +7 -0
  330. package/dist/security/owasp.js +104 -0
  331. package/dist/security/owasp.js.map +7 -0
  332. package/dist/security/pipeline.js +125 -0
  333. package/dist/security/pipeline.js.map +7 -0
  334. package/dist/security/reachability.js +276 -0
  335. package/dist/security/reachability.js.map +7 -0
  336. package/dist/security/reachability.test.js +394 -0
  337. package/dist/security/reachability.test.js.map +7 -0
  338. package/dist/security/remediation.js +126 -0
  339. package/dist/security/remediation.js.map +7 -0
  340. package/dist/security/secrets.js +176 -0
  341. package/dist/security/secrets.js.map +7 -0
  342. package/dist/security/staleness.js +231 -0
  343. package/dist/security/staleness.js.map +7 -0
  344. package/dist/security/supply-chain.js +301 -0
  345. package/dist/security/supply-chain.js.map +7 -0
  346. package/dist/security/suppressions.js +106 -0
  347. package/dist/security/suppressions.js.map +7 -0
  348. package/dist/security/types.js +17 -0
  349. package/dist/security/types.js.map +7 -0
  350. package/dist/security/vuln/client.js +355 -0
  351. package/dist/security/vuln/client.js.map +7 -0
  352. package/dist/security/vuln/client.test.js +288 -0
  353. package/dist/security/vuln/client.test.js.map +7 -0
  354. package/dist/security/vuln/epss-client.js +95 -0
  355. package/dist/security/vuln/epss-client.js.map +7 -0
  356. package/dist/security/vuln/index.js +29 -0
  357. package/dist/security/vuln/index.js.map +7 -0
  358. package/dist/security/vuln/osv-adapter.js +321 -0
  359. package/dist/security/vuln/osv-adapter.js.map +7 -0
  360. package/dist/security/vuln/osv-adapter.test.js +391 -0
  361. package/dist/security/vuln/osv-adapter.test.js.map +7 -0
  362. package/dist/security/vuln/types.js +17 -0
  363. package/dist/security/vuln/types.js.map +7 -0
  364. package/dist/types.js.map +2 -2
  365. package/package.json +5 -2
@@ -0,0 +1,1349 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var security_export_exports = {};
30
+ __export(security_export_exports, {
31
+ register: () => register
32
+ });
33
+ module.exports = __toCommonJS(security_export_exports);
34
+ var fs = __toESM(require("fs"));
35
+ var path = __toESM(require("path"));
36
+ var import_child_process = require("child_process");
37
+ var import_config = require("../../config");
38
+ var import_ui = require("../ui");
39
+ var import_license = require("../../security/license");
40
+ function openBrowser(filePath) {
41
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
42
+ (0, import_child_process.spawn)(cmd, [filePath], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
43
+ }
44
+ function generateHtml(params) {
45
+ const {
46
+ projectName,
47
+ generatedAt,
48
+ vulns,
49
+ deps,
50
+ sbomJson,
51
+ vexJson,
52
+ denyList,
53
+ warnList,
54
+ attackSurface,
55
+ secretsResult,
56
+ flowsResult,
57
+ remediationStatus
58
+ } = params;
59
+ const licDeps = deps.map((d) => ({ package_name: d.package_name, ecosystem: d.ecosystem, license: d.license }));
60
+ const violations = (0, import_license.checkLicensePolicy)(licDeps, { deny: denyList, warn: warnList });
61
+ const violationMap = {};
62
+ for (const v of violations) {
63
+ violationMap[`${v.ecosystem}:${v.packageName}`] = v.severity;
64
+ }
65
+ const licenseData = deps.map((d) => ({
66
+ package_name: d.package_name,
67
+ ecosystem: d.ecosystem,
68
+ license: d.license ?? null,
69
+ status: violationMap[`${d.ecosystem}:${d.package_name}`] ?? (d.license ? "ok" : "unknown")
70
+ }));
71
+ const totalDeps = deps.length;
72
+ const totalVulns = vulns.length;
73
+ const affected = vulns.filter((v) => v.verdict === "affected").length;
74
+ const underInvestigation = vulns.filter((v) => v.verdict === "under_investigation").length;
75
+ const notAffected = vulns.filter((v) => v.verdict === "not_affected").length;
76
+ const staleDeps = deps.filter((d) => (d.staleness_score ?? 0) >= 0.3).length;
77
+ const top5 = [...vulns].filter((v) => v.epss_score != null || v.severity_score != null).sort((a, b) => {
78
+ const scoreA = (a.epss_score ?? 0) * (a.severity_score ?? 0);
79
+ const scoreB = (b.epss_score ?? 0) * (b.severity_score ?? 0);
80
+ return scoreB - scoreA;
81
+ }).slice(0, 5);
82
+ const directCount = deps.filter((d) => d.classification === "direct").length;
83
+ const transitiveCount = deps.filter((d) => d.classification !== "direct").length;
84
+ const stalenessRows = deps.filter((d) => d.staleness_score != null).sort((a, b) => (b.staleness_score ?? 0) - (a.staleness_score ?? 0));
85
+ const topRiskCve = vulns.length > 0 ? [...vulns].sort((a, b) => (b.risk_score ?? b.severity_score ?? 0) - (a.risk_score ?? a.severity_score ?? 0))[0] : null;
86
+ const owaspCounts = {};
87
+ if (flowsResult) {
88
+ for (const f of flowsResult) {
89
+ owaspCounts[f.owaspCategory] = (owaspCounts[f.owaspCategory] ?? 0) + 1;
90
+ }
91
+ }
92
+ const owaspOrder = ["A01", "A02", "A03", "A04", "A05", "A06", "A07", "A08", "A09", "A10"];
93
+ const owaspNames = {
94
+ A01: "Broken Access Control",
95
+ A02: "Cryptographic Failures",
96
+ A03: "Injection",
97
+ A04: "Insecure Design",
98
+ A05: "Security Misconfiguration",
99
+ A06: "Vulnerable Components",
100
+ A07: "Auth Failures",
101
+ A08: "Integrity Failures",
102
+ A09: "Logging Failures",
103
+ A10: "SSRF"
104
+ };
105
+ const owaspMaxCount = Math.max(1, ...Object.values(owaspCounts));
106
+ const secretsCount = secretsResult?.totalFindings ?? null;
107
+ const criticalFlowsCount = flowsResult?.filter((f) => f.severity === "critical").length ?? null;
108
+ const safeJson = (obj) => JSON.stringify(obj).replace(/<\/script>/gi, "<\\/script>");
109
+ return `<!DOCTYPE html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>Security Dashboard \u2014 ${projectName}</title>
115
+ <style>
116
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
117
+ :root {
118
+ --bg: #0d1117;
119
+ --bg-card: #161b22;
120
+ --bg-input: #21262d;
121
+ --border: #30363d;
122
+ --accent: #a78bfa;
123
+ --text: #c9d1d9;
124
+ --text-muted: #8b949e;
125
+ --text-bright: #f0f6fc;
126
+ --green: #3fb950;
127
+ --yellow: #d29922;
128
+ --red: #f85149;
129
+ --orange: #db6d28;
130
+ --radius: 8px;
131
+ }
132
+ html { scroll-behavior: smooth; }
133
+ body {
134
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
135
+ background: var(--bg);
136
+ color: var(--text);
137
+ line-height: 1.5;
138
+ font-size: 14px;
139
+ -webkit-font-smoothing: antialiased;
140
+ }
141
+ a { color: var(--accent); text-decoration: none; }
142
+ a:hover { text-decoration: underline; }
143
+
144
+ /* Header */
145
+ .header {
146
+ background: var(--bg-card);
147
+ border-bottom: 1px solid var(--border);
148
+ padding: 1rem 2rem;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ gap: 1rem;
153
+ }
154
+ .header-title { font-size: 1.1rem; font-weight: 600; color: var(--text-bright); }
155
+ .header-meta { font-size: 0.8rem; color: var(--text-muted); }
156
+ .header-badge {
157
+ display: inline-flex; align-items: center; gap: 0.35rem;
158
+ background: rgba(167,139,250,0.12); color: var(--accent);
159
+ border: 1px solid rgba(167,139,250,0.25);
160
+ padding: 0.25rem 0.6rem; border-radius: 4px;
161
+ font-size: 0.75rem; font-weight: 600;
162
+ }
163
+
164
+ /* Tabs */
165
+ .tabs {
166
+ display: flex; gap: 0;
167
+ background: var(--bg-card);
168
+ border-bottom: 1px solid var(--border);
169
+ padding: 0 2rem;
170
+ overflow-x: auto;
171
+ }
172
+ .tab {
173
+ padding: 0.75rem 1.25rem;
174
+ cursor: pointer;
175
+ border: none;
176
+ background: none;
177
+ color: var(--text-muted);
178
+ font-size: 0.875rem;
179
+ font-weight: 500;
180
+ border-bottom: 2px solid transparent;
181
+ transition: color 0.15s, border-color 0.15s;
182
+ white-space: nowrap;
183
+ }
184
+ .tab:hover { color: var(--text); }
185
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
186
+
187
+ /* Content */
188
+ .content { padding: 1.5rem 2rem; }
189
+ .tab-panel { display: none; }
190
+ .tab-panel.active { display: block; }
191
+
192
+ /* Stats cards */
193
+ .stats-grid {
194
+ display: grid;
195
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
196
+ gap: 1rem;
197
+ margin-bottom: 1.5rem;
198
+ }
199
+ .stat-card {
200
+ background: var(--bg-card);
201
+ border: 1px solid var(--border);
202
+ border-radius: var(--radius);
203
+ padding: 1rem;
204
+ }
205
+ .stat-label { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.04em; }
206
+ .stat-value { font-size: 1.75rem; font-weight: 700; color: var(--text-bright); line-height: 1; }
207
+ .stat-value.red { color: var(--red); }
208
+ .stat-value.yellow { color: var(--yellow); }
209
+ .stat-value.green { color: var(--green); }
210
+ .stat-value.accent { color: var(--accent); }
211
+
212
+ /* Section heading */
213
+ .section-title {
214
+ font-size: 0.85rem; font-weight: 600;
215
+ color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em;
216
+ margin-bottom: 1rem;
217
+ }
218
+
219
+ /* Chart bar */
220
+ .bar-chart { margin-bottom: 1.5rem; }
221
+ .bar-row {
222
+ display: flex; align-items: center; gap: 0.75rem;
223
+ margin-bottom: 0.5rem;
224
+ }
225
+ .bar-label { width: 140px; font-size: 0.8rem; color: var(--text-muted); flex-shrink: 0; }
226
+ .bar-track { flex: 1; height: 14px; background: var(--bg-input); border-radius: 3px; overflow: hidden; }
227
+ .bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
228
+ .bar-count { width: 40px; text-align: right; font-size: 0.8rem; color: var(--text); }
229
+
230
+ /* Top CVEs */
231
+ .top-cve-list { display: flex; flex-direction: column; gap: 0.5rem; }
232
+ .top-cve-item {
233
+ display: flex; align-items: center; gap: 0.75rem;
234
+ background: var(--bg-card); border: 1px solid var(--border);
235
+ border-radius: var(--radius); padding: 0.6rem 0.9rem;
236
+ }
237
+ .top-cve-rank { width: 24px; color: var(--text-muted); font-size: 0.8rem; font-weight: 600; flex-shrink: 0; }
238
+ .top-cve-id { font-family: monospace; font-size: 0.85rem; color: var(--accent); min-width: 130px; }
239
+ .top-cve-pkg { font-size: 0.8rem; color: var(--text); flex: 1; }
240
+ .top-cve-scores { display: flex; gap: 0.5rem; align-items: center; }
241
+
242
+ /* Filters */
243
+ .filters {
244
+ display: flex; gap: 0.75rem; flex-wrap: wrap;
245
+ margin-bottom: 1rem; align-items: center;
246
+ }
247
+ .filter-label { font-size: 0.8rem; color: var(--text-muted); }
248
+ select, input[type=text], input[type=range] {
249
+ background: var(--bg-input); border: 1px solid var(--border);
250
+ color: var(--text); padding: 0.35rem 0.6rem;
251
+ border-radius: var(--radius); font-size: 0.8rem;
252
+ outline: none;
253
+ }
254
+ select:focus, input[type=text]:focus { border-color: var(--accent); }
255
+ input[type=text] { min-width: 180px; }
256
+ .range-group { display: flex; align-items: center; gap: 0.5rem; }
257
+ #epssValue { min-width: 32px; font-size: 0.8rem; color: var(--accent); }
258
+
259
+ /* Tables */
260
+ .table-wrap { overflow-x: auto; }
261
+ table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
262
+ th {
263
+ text-align: left; padding: 0.5rem 0.75rem;
264
+ background: var(--bg-card); border-bottom: 1px solid var(--border);
265
+ color: var(--text-muted); font-weight: 600; font-size: 0.75rem;
266
+ text-transform: uppercase; letter-spacing: 0.04em;
267
+ white-space: nowrap;
268
+ }
269
+ th.sortable { cursor: pointer; user-select: none; }
270
+ th.sortable:hover { color: var(--text); }
271
+ th.sorted-asc::after { content: ' \u25B2'; color: var(--accent); }
272
+ th.sorted-desc::after { content: ' \u25BC'; color: var(--accent); }
273
+ td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); vertical-align: top; }
274
+ tr:last-child td { border-bottom: none; }
275
+ tr:hover td { background: rgba(255,255,255,0.02); }
276
+
277
+ /* Verdict row colors */
278
+ tr.verdict-affected td:first-child { border-left: 3px solid var(--red); }
279
+ tr.verdict-investigating td:first-child { border-left: 3px solid var(--yellow); }
280
+ tr.verdict-not-affected td:first-child { border-left: 3px solid var(--green); }
281
+
282
+ /* Badges */
283
+ .badge {
284
+ display: inline-flex; align-items: center;
285
+ padding: 0.15rem 0.45rem; border-radius: 3px;
286
+ font-size: 0.72rem; font-weight: 600; white-space: nowrap;
287
+ }
288
+ .badge-red { background: rgba(248,81,73,0.15); color: var(--red); }
289
+ .badge-yellow { background: rgba(210,153,34,0.15); color: var(--yellow); }
290
+ .badge-green { background: rgba(63,185,80,0.15); color: var(--green); }
291
+ .badge-gray { background: rgba(139,148,158,0.15);color: var(--text-muted); }
292
+ .badge-accent { background: rgba(167,139,250,0.15);color: var(--accent); }
293
+ .badge-orange { background: rgba(219,109,40,0.15); color: var(--orange); }
294
+
295
+ /* Expandable row */
296
+ .expand-row td { padding: 0; border-bottom: 1px solid var(--border); }
297
+ .expand-body {
298
+ padding: 0.75rem 1rem;
299
+ background: rgba(22,27,34,0.8);
300
+ font-size: 0.8rem; color: var(--text);
301
+ }
302
+ .expand-body .expand-section { margin-bottom: 0.6rem; }
303
+ .expand-body .expand-label { color: var(--text-muted); font-size: 0.75rem; margin-bottom: 0.25rem; }
304
+ .expand-body pre { font-family: monospace; font-size: 0.78rem; white-space: pre-wrap; color: var(--text-muted); }
305
+ .row-toggle { cursor: pointer; }
306
+
307
+ /* Download buttons */
308
+ .btn {
309
+ display: inline-flex; align-items: center; gap: 0.4rem;
310
+ padding: 0.45rem 1rem; border-radius: var(--radius);
311
+ font-size: 0.82rem; font-weight: 500;
312
+ cursor: pointer; border: none; transition: opacity 0.15s;
313
+ }
314
+ .btn:hover { opacity: 0.85; }
315
+ .btn-accent { background: var(--accent); color: #0d1117; }
316
+ .btn-outline {
317
+ background: none; border: 1px solid var(--border);
318
+ color: var(--text);
319
+ }
320
+ .btn-outline:hover { border-color: var(--accent); color: var(--accent); }
321
+
322
+ /* Staleness bar */
323
+ .stale-bar-track { width: 80px; height: 8px; background: var(--bg-input); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; }
324
+ .stale-bar-fill { height: 100%; border-radius: 2px; }
325
+
326
+ /* Policy banners */
327
+ .policy-banner {
328
+ display: flex; align-items: center; gap: 0.6rem;
329
+ padding: 0.65rem 1rem; border-radius: var(--radius);
330
+ margin-bottom: 0.75rem; font-size: 0.85rem;
331
+ }
332
+ .policy-banner.deny { background: rgba(248,81,73,0.1); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }
333
+ .policy-banner.warn { background: rgba(210,153,34,0.1); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }
334
+
335
+ /* Score cell */
336
+ .score-critical { color: var(--red); font-weight: 700; }
337
+ .score-high { color: var(--orange); font-weight: 600; }
338
+ .score-medium { color: var(--yellow); }
339
+ .score-low { color: var(--green); }
340
+
341
+ /* Empty state */
342
+ .empty-state { text-align: center; padding: 2.5rem; color: var(--text-muted); font-size: 0.85rem; }
343
+
344
+ /* SBOM summary badges */
345
+ .badge-counts { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
346
+
347
+ /* VEX state colors */
348
+ .state-affected { color: var(--red); }
349
+ .state-not_affected { color: var(--green); }
350
+ .state-investigating { color: var(--yellow); }
351
+ </style>
352
+ </head>
353
+ <body>
354
+
355
+ <div class="header">
356
+ <div>
357
+ <div class="header-title">Security Dashboard \u2014 ${projectName}</div>
358
+ <div class="header-meta">Generated ${generatedAt}</div>
359
+ </div>
360
+ <div class="header-badge">KiroGraph Security</div>
361
+ </div>
362
+
363
+ <div class="tabs">
364
+ <button class="tab active" data-tab="overview">Overview</button>
365
+ <button class="tab" data-tab="vulns">Vulnerabilities</button>
366
+ <button class="tab" data-tab="sbom">SBOM</button>
367
+ <button class="tab" data-tab="vex">VEX</button>
368
+ <button class="tab" data-tab="licenses">Licenses</button>
369
+ <button class="tab" data-tab="staleness">Staleness</button>
370
+ <button class="tab" data-tab="attack-surface">\u2694\uFE0F Attack Surface</button>
371
+ <button class="tab" data-tab="secrets">\u{1F511} Secrets</button>
372
+ <button class="tab" data-tab="sast">\u{1F50D} SAST Flows</button>
373
+ <button class="tab" data-tab="remediation">\u23F1\uFE0F Remediation</button>
374
+ </div>
375
+
376
+ <div class="content">
377
+
378
+ <!-- Tab: Overview -->
379
+ <div class="tab-panel active" id="tab-overview">
380
+ <div class="stats-grid">
381
+ <div class="stat-card">
382
+ <div class="stat-label">Total Dependencies</div>
383
+ <div class="stat-value accent" id="stat-deps">${totalDeps}</div>
384
+ </div>
385
+ <div class="stat-card">
386
+ <div class="stat-label">Vulnerabilities</div>
387
+ <div class="stat-value ${totalVulns > 0 ? "red" : "green"}" id="stat-vulns">${totalVulns}</div>
388
+ </div>
389
+ <div class="stat-card">
390
+ <div class="stat-label">Affected</div>
391
+ <div class="stat-value ${affected > 0 ? "red" : "green"}">${affected}</div>
392
+ </div>
393
+ <div class="stat-card">
394
+ <div class="stat-label">Under Investigation</div>
395
+ <div class="stat-value ${underInvestigation > 0 ? "yellow" : "green"}">${underInvestigation}</div>
396
+ </div>
397
+ <div class="stat-card">
398
+ <div class="stat-label">Not Affected</div>
399
+ <div class="stat-value green">${notAffected}</div>
400
+ </div>
401
+ <div class="stat-card">
402
+ <div class="stat-label">Stale Dependencies</div>
403
+ <div class="stat-value ${staleDeps > 0 ? "yellow" : "green"}">${staleDeps}</div>
404
+ </div>
405
+ ${secretsCount !== null ? `<div class="stat-card">
406
+ <div class="stat-label">Secrets Found</div>
407
+ <div class="stat-value ${secretsCount > 0 ? "red" : "green"}">${secretsCount}</div>
408
+ </div>` : ""}
409
+ ${criticalFlowsCount !== null ? `<div class="stat-card">
410
+ <div class="stat-label">Critical SAST Findings</div>
411
+ <div class="stat-value ${criticalFlowsCount > 0 ? "red" : "green"}">${criticalFlowsCount}</div>
412
+ </div>` : ""}
413
+ </div>
414
+
415
+ ${topRiskCve ? `
416
+ <div class="section-title">Top Risk CVE</div>
417
+ <div class="top-cve-list" style="margin-bottom:1.5rem">
418
+ <div class="top-cve-item">
419
+ <div class="top-cve-rank">#1</div>
420
+ <div class="top-cve-id">${topRiskCve.cve_id}</div>
421
+ <div class="top-cve-pkg">${topRiskCve.package_name ?? "\u2014"} ${topRiskCve.ecosystem ? `<span style="color:var(--text-muted);font-size:0.75rem">[${topRiskCve.ecosystem}]</span>` : ""}</div>
422
+ <div class="top-cve-scores">
423
+ ${topRiskCve.risk_score != null ? `<span class="badge ${topRiskCve.risk_score >= 7 ? "badge-red" : topRiskCve.risk_score >= 4 ? "badge-yellow" : "badge-gray"}">Risk ${topRiskCve.risk_score.toFixed(1)}</span>` : ""}
424
+ ${topRiskCve.severity_score != null ? `<span class="badge ${topRiskCve.severity_score >= 9 ? "badge-red" : topRiskCve.severity_score >= 7 ? "badge-orange" : topRiskCve.severity_score >= 4 ? "badge-yellow" : "badge-gray"}">CVSS ${topRiskCve.severity_score.toFixed(1)}</span>` : ""}
425
+ ${topRiskCve.epss_score != null ? `<span class="badge ${topRiskCve.epss_score >= 0.5 ? "badge-red" : topRiskCve.epss_score >= 0.1 ? "badge-yellow" : "badge-gray"}">EPSS ${(topRiskCve.epss_score * 100).toFixed(2)}%</span>` : ""}
426
+ ${topRiskCve.verdict ? `<span class="badge ${topRiskCve.verdict === "affected" ? "badge-red" : topRiskCve.verdict === "not_affected" ? "badge-green" : "badge-yellow"}">${topRiskCve.verdict}</span>` : ""}
427
+ </div>
428
+ </div>
429
+ </div>
430
+ ` : ""}
431
+
432
+ ${totalVulns > 0 ? `
433
+ <div class="section-title">Verdict Breakdown</div>
434
+ <div class="bar-chart">
435
+ ${affected > 0 ? `<div class="bar-row">
436
+ <div class="bar-label">Affected</div>
437
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(affected / totalVulns * 100)}%;background:var(--red)"></div></div>
438
+ <div class="bar-count">${affected}</div>
439
+ </div>` : ""}
440
+ ${underInvestigation > 0 ? `<div class="bar-row">
441
+ <div class="bar-label">Under Investigation</div>
442
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(underInvestigation / totalVulns * 100)}%;background:var(--yellow)"></div></div>
443
+ <div class="bar-count">${underInvestigation}</div>
444
+ </div>` : ""}
445
+ ${notAffected > 0 ? `<div class="bar-row">
446
+ <div class="bar-label">Not Affected</div>
447
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(notAffected / totalVulns * 100)}%;background:var(--green)"></div></div>
448
+ <div class="bar-count">${notAffected}</div>
449
+ </div>` : ""}
450
+ ${totalVulns - affected - underInvestigation - notAffected > 0 ? `<div class="bar-row">
451
+ <div class="bar-label">Pending Analysis</div>
452
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round((totalVulns - affected - underInvestigation - notAffected) / totalVulns * 100)}%;background:var(--text-muted)"></div></div>
453
+ <div class="bar-count">${totalVulns - affected - underInvestigation - notAffected}</div>
454
+ </div>` : ""}
455
+ </div>
456
+ ` : ""}
457
+
458
+ ${Object.keys(owaspCounts).length > 0 ? `
459
+ <div class="section-title">OWASP Category Breakdown</div>
460
+ <div class="bar-chart" style="margin-bottom:1.5rem">
461
+ ${owaspOrder.filter((cat) => owaspCounts[cat] > 0).map((cat) => `<div class="bar-row">
462
+ <div class="bar-label" style="width:200px"><span class="badge badge-accent" style="margin-right:0.4rem">${cat}</span>${owaspNames[cat] ?? cat}</div>
463
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round((owaspCounts[cat] ?? 0) / owaspMaxCount * 100)}%;background:var(--orange)"></div></div>
464
+ <div class="bar-count">${owaspCounts[cat]}</div>
465
+ </div>`).join("")}
466
+ </div>
467
+ ` : ""}
468
+
469
+ ${top5.length > 0 ? `
470
+ <div class="section-title">Top 5 Critical CVEs</div>
471
+ <div class="top-cve-list">
472
+ ${top5.map((v, i) => `
473
+ <div class="top-cve-item">
474
+ <div class="top-cve-rank">#${i + 1}</div>
475
+ <div class="top-cve-id">${v.cve_id}</div>
476
+ <div class="top-cve-pkg">${v.package_name ?? "\u2014"} ${v.ecosystem ? `<span style="color:var(--text-muted);font-size:0.75rem">[${v.ecosystem}]</span>` : ""}</div>
477
+ <div class="top-cve-scores">
478
+ ${v.severity_score != null ? `<span class="badge ${v.severity_score >= 9 ? "badge-red" : v.severity_score >= 7 ? "badge-orange" : v.severity_score >= 4 ? "badge-yellow" : "badge-gray"}">CVSS ${v.severity_score.toFixed(1)}</span>` : ""}
479
+ ${v.epss_score != null ? `<span class="badge ${v.epss_score >= 0.5 ? "badge-red" : v.epss_score >= 0.1 ? "badge-yellow" : "badge-gray"}">EPSS ${(v.epss_score * 100).toFixed(2)}%</span>` : ""}
480
+ ${v.verdict ? `<span class="badge ${v.verdict === "affected" ? "badge-red" : v.verdict === "not_affected" ? "badge-green" : "badge-yellow"}">${v.verdict}</span>` : ""}
481
+ </div>
482
+ </div>`).join("")}
483
+ </div>
484
+ ` : totalVulns === 0 ? '<div class="empty-state">No vulnerabilities found.</div>' : ""}
485
+ </div>
486
+
487
+ <!-- Tab: Vulnerabilities -->
488
+ <div class="tab-panel" id="tab-vulns">
489
+ <div class="filters">
490
+ <div class="filter-label">Severity:</div>
491
+ <select id="filterSeverity">
492
+ <option value="all">All</option>
493
+ <option value="critical">Critical (\u22659)</option>
494
+ <option value="high">High (\u22657)</option>
495
+ <option value="medium">Medium (\u22654)</option>
496
+ <option value="low">Low (&lt;4)</option>
497
+ </select>
498
+ <div class="filter-label">Verdict:</div>
499
+ <select id="filterVerdict">
500
+ <option value="all">All</option>
501
+ <option value="affected">Affected</option>
502
+ <option value="under_investigation">Under Investigation</option>
503
+ <option value="not_affected">Not Affected</option>
504
+ </select>
505
+ <div class="filter-label">Min EPSS:</div>
506
+ <div class="range-group">
507
+ <input type="range" id="filterEpss" min="0" max="1" step="0.01" value="0">
508
+ <span id="epssValue">0.00</span>
509
+ </div>
510
+ </div>
511
+ <div class="table-wrap">
512
+ <table id="vulnTable">
513
+ <thead>
514
+ <tr>
515
+ <th class="sortable" data-col="cve_id">CVE ID</th>
516
+ <th class="sortable" data-col="package_name">Package</th>
517
+ <th>Ecosystem</th>
518
+ <th class="sortable" data-col="severity_score">CVSS</th>
519
+ <th class="sortable sorted-desc" data-col="epss_score">EPSS</th>
520
+ <th class="sortable" data-col="epss_percentile">Percentile</th>
521
+ <th class="sortable" data-col="risk_score">Risk Score</th>
522
+ <th>Verdict</th>
523
+ <th>Fix</th>
524
+ </tr>
525
+ </thead>
526
+ <tbody id="vulnBody"></tbody>
527
+ </table>
528
+ </div>
529
+ <div id="vulnEmpty" class="empty-state" style="display:none">No vulnerabilities match the current filters.</div>
530
+ </div>
531
+
532
+ <!-- Tab: SBOM -->
533
+ <div class="tab-panel" id="tab-sbom">
534
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
535
+ <div class="badge-counts">
536
+ <span class="badge badge-accent">${directCount} direct</span>
537
+ <span class="badge badge-gray">${transitiveCount} transitive</span>
538
+ </div>
539
+ <button class="btn btn-outline" onclick="downloadSBOM()">\u2193 Download SBOM JSON</button>
540
+ </div>
541
+ <div class="filters">
542
+ <div class="filter-label">Ecosystem:</div>
543
+ <select id="filterEco">
544
+ <option value="all">All</option>
545
+ ${[...new Set(deps.map((d) => d.ecosystem))].sort().map((e) => `<option value="${e}">${e}</option>`).join("")}
546
+ </select>
547
+ <input type="text" id="filterPkg" placeholder="Search package\u2026">
548
+ </div>
549
+ <div class="table-wrap">
550
+ <table id="sbomTable">
551
+ <thead>
552
+ <tr>
553
+ <th>Package</th>
554
+ <th>Ecosystem</th>
555
+ <th>Version</th>
556
+ <th>Scope</th>
557
+ <th>License</th>
558
+ <th>purl</th>
559
+ </tr>
560
+ </thead>
561
+ <tbody id="sbomBody"></tbody>
562
+ </table>
563
+ </div>
564
+ <div id="sbomEmpty" class="empty-state" style="display:none">No packages match the current filters.</div>
565
+ </div>
566
+
567
+ <!-- Tab: VEX -->
568
+ <div class="tab-panel" id="tab-vex">
569
+ <div style="display:flex;justify-content:flex-end;margin-bottom:1rem">
570
+ <button class="btn btn-outline" onclick="downloadVEX()">\u2193 Download VEX JSON</button>
571
+ </div>
572
+ <div class="table-wrap">
573
+ <table id="vexTable">
574
+ <thead>
575
+ <tr>
576
+ <th>CVE ID</th>
577
+ <th>Package</th>
578
+ <th>Analysis State</th>
579
+ <th>Justification</th>
580
+ <th>Detail</th>
581
+ </tr>
582
+ </thead>
583
+ <tbody id="vexBody"></tbody>
584
+ </table>
585
+ </div>
586
+ <div id="vexEmpty" class="empty-state" style="display:none">No VEX entries found.</div>
587
+ </div>
588
+
589
+ <!-- Tab: Licenses -->
590
+ <div class="tab-panel" id="tab-licenses">
591
+ <div id="licBanners"></div>
592
+ <div class="filters">
593
+ <div class="filter-label">Policy:</div>
594
+ <select id="filterLicPolicy">
595
+ <option value="all">All</option>
596
+ <option value="deny">Deny</option>
597
+ <option value="warn">Warn</option>
598
+ <option value="ok">OK</option>
599
+ <option value="unknown">Unknown</option>
600
+ </select>
601
+ </div>
602
+ <div class="table-wrap">
603
+ <table id="licTable">
604
+ <thead>
605
+ <tr>
606
+ <th>Package</th>
607
+ <th>Ecosystem</th>
608
+ <th>License</th>
609
+ <th>Policy Status</th>
610
+ </tr>
611
+ </thead>
612
+ <tbody id="licBody"></tbody>
613
+ </table>
614
+ </div>
615
+ <div id="licEmpty" class="empty-state" style="display:none">No packages match the current filters.</div>
616
+ </div>
617
+
618
+ <!-- Tab: Staleness -->
619
+ <div class="tab-panel" id="tab-staleness">
620
+ <div class="stats-grid" style="grid-template-columns:repeat(3,1fr);max-width:600px;margin-bottom:1.5rem">
621
+ <div class="stat-card">
622
+ <div class="stat-label">Scored</div>
623
+ <div class="stat-value accent">${stalenessRows.length}</div>
624
+ </div>
625
+ <div class="stat-card">
626
+ <div class="stat-label">Low (0\u20130.3)</div>
627
+ <div class="stat-value green">${stalenessRows.filter((r) => (r.staleness_score ?? 0) < 0.3).length}</div>
628
+ </div>
629
+ <div class="stat-card">
630
+ <div class="stat-label">High (0.7\u20131.0)</div>
631
+ <div class="stat-value red">${stalenessRows.filter((r) => (r.staleness_score ?? 0) >= 0.7).length}</div>
632
+ </div>
633
+ </div>
634
+
635
+ ${stalenessRows.length > 0 ? `
636
+ <div class="section-title">Distribution</div>
637
+ <div class="bar-chart" style="margin-bottom:1.5rem">
638
+ <div class="bar-row">
639
+ <div class="bar-label">Low (0\u20130.3)</div>
640
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(stalenessRows.filter((r) => (r.staleness_score ?? 0) < 0.3).length / stalenessRows.length * 100)}%;background:var(--green)"></div></div>
641
+ <div class="bar-count">${stalenessRows.filter((r) => (r.staleness_score ?? 0) < 0.3).length}</div>
642
+ </div>
643
+ <div class="bar-row">
644
+ <div class="bar-label">Medium (0.3\u20130.7)</div>
645
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(stalenessRows.filter((r) => {
646
+ const s = r.staleness_score ?? 0;
647
+ return s >= 0.3 && s < 0.7;
648
+ }).length / stalenessRows.length * 100)}%;background:var(--yellow)"></div></div>
649
+ <div class="bar-count">${stalenessRows.filter((r) => {
650
+ const s = r.staleness_score ?? 0;
651
+ return s >= 0.3 && s < 0.7;
652
+ }).length}</div>
653
+ </div>
654
+ <div class="bar-row">
655
+ <div class="bar-label">High (0.7\u20131.0)</div>
656
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.round(stalenessRows.filter((r) => (r.staleness_score ?? 0) >= 0.7).length / stalenessRows.length * 100)}%;background:var(--red)"></div></div>
657
+ <div class="bar-count">${stalenessRows.filter((r) => (r.staleness_score ?? 0) >= 0.7).length}</div>
658
+ </div>
659
+ </div>
660
+ ` : ""}
661
+
662
+ <div class="table-wrap">
663
+ <table id="staleTable">
664
+ <thead>
665
+ <tr>
666
+ <th>Package</th>
667
+ <th>Ecosystem</th>
668
+ <th>Current</th>
669
+ <th>Latest</th>
670
+ <th>Months Behind</th>
671
+ <th class="sortable sorted-desc" data-col="staleness_score">Score</th>
672
+ <th>Bar</th>
673
+ </tr>
674
+ </thead>
675
+ <tbody id="staleBody"></tbody>
676
+ </table>
677
+ </div>
678
+ <div id="staleEmpty" class="empty-state" style="display:none">No staleness data available.</div>
679
+ </div>
680
+
681
+ <!-- Tab: Attack Surface -->
682
+ <div class="tab-panel" id="tab-attack-surface">
683
+ ${attackSurface ? `
684
+ <div class="stats-grid" style="grid-template-columns:repeat(4,1fr);max-width:700px;margin-bottom:1.5rem">
685
+ <div class="stat-card">
686
+ <div class="stat-label">Total Routes</div>
687
+ <div class="stat-value accent">${attackSurface.totalRoutes}</div>
688
+ </div>
689
+ <div class="stat-card">
690
+ <div class="stat-label">Public</div>
691
+ <div class="stat-value ${attackSurface.publicRoutes > 0 ? "yellow" : "green"}">${attackSurface.publicRoutes}</div>
692
+ </div>
693
+ <div class="stat-card">
694
+ <div class="stat-label">Authenticated</div>
695
+ <div class="stat-value accent">${attackSurface.authenticatedRoutes}</div>
696
+ </div>
697
+ <div class="stat-card">
698
+ <div class="stat-label">With Vulnerabilities</div>
699
+ <div class="stat-value ${attackSurface.routesWithVulns > 0 ? "red" : "green"}">${attackSurface.routesWithVulns}</div>
700
+ </div>
701
+ </div>
702
+ ${attackSurface.allRoutes.length > 0 ? `
703
+ <div class="table-wrap">
704
+ <table id="attackSurfaceTable">
705
+ <thead>
706
+ <tr>
707
+ <th>Route</th>
708
+ <th>Exposure</th>
709
+ <th>Vuln Count</th>
710
+ <th>Highest Risk</th>
711
+ <th>Hops</th>
712
+ </tr>
713
+ </thead>
714
+ <tbody>
715
+ ${attackSurface.allRoutes.map((entry) => {
716
+ const exposureBadge = entry.exposureLevel === "public" ? '<span class="badge badge-red">public</span>' : entry.exposureLevel === "authenticated" ? '<span class="badge badge-yellow">authenticated</span>' : entry.exposureLevel === "internal" ? '<span class="badge badge-gray">internal</span>' : '<span class="badge badge-gray">unknown</span>';
717
+ const hasVulns = entry.vulnerableDeps.length > 0;
718
+ const rowStyle = entry.exposureLevel === "public" && hasVulns ? 'style="background:rgba(248,81,73,0.06)"' : entry.exposureLevel === "authenticated" && hasVulns ? 'style="background:rgba(210,153,34,0.06)"' : "";
719
+ const highestRisk = entry.riskScore > 0 ? entry.riskScore.toFixed(1) : "\u2014";
720
+ const minHop = entry.vulnerableDeps.length > 0 ? Math.min(...entry.vulnerableDeps.map((d) => d.hopCount)) : null;
721
+ return `<tr ${rowStyle}>
722
+ <td><code style="font-size:0.8rem;color:var(--accent)">${entry.route.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code><br><span style="font-size:0.72rem;color:var(--text-muted)">${(entry.filePath || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</span></td>
723
+ <td>${exposureBadge}</td>
724
+ <td>${entry.vulnerableDeps.length > 0 ? `<span class="badge badge-red">${entry.vulnerableDeps.length}</span>` : '<span style="color:var(--text-muted)">0</span>'}</td>
725
+ <td>${entry.riskScore >= 7 ? `<span class="score-critical">${highestRisk}</span>` : entry.riskScore >= 4 ? `<span class="score-medium">${highestRisk}</span>` : `<span class="score-low">${highestRisk}</span>`}</td>
726
+ <td>${minHop != null ? minHop : "\u2014"}</td>
727
+ </tr>`;
728
+ }).join("")}
729
+ </tbody>
730
+ </table>
731
+ </div>` : '<div class="empty-state">No routes found.</div>'}
732
+ ` : '<div class="empty-state">No routes found.</div>'}
733
+ </div>
734
+
735
+ <!-- Tab: Secrets -->
736
+ <div class="tab-panel" id="tab-secrets">
737
+ ${secretsResult ? `
738
+ <div style="margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem">
739
+ ${secretsResult.totalFindings} finding${secretsResult.totalFindings !== 1 ? "s" : ""} in ${secretsResult.filesScanned} file${secretsResult.filesScanned !== 1 ? "s" : ""} scanned
740
+ ${secretsResult.criticalCount > 0 ? `\u2014 <span style="color:var(--red);font-weight:600">${secretsResult.criticalCount} critical</span>` : ""}
741
+ ${secretsResult.highCount > 0 ? `\u2014 <span style="color:var(--orange);font-weight:600">${secretsResult.highCount} high</span>` : ""}
742
+ </div>
743
+ ${secretsResult.findings.length > 0 ? `
744
+ <div class="table-wrap">
745
+ <table id="secretsTable">
746
+ <thead>
747
+ <tr>
748
+ <th>Severity</th>
749
+ <th>Type</th>
750
+ <th>File:Line</th>
751
+ <th>Function</th>
752
+ <th>Reachable From</th>
753
+ </tr>
754
+ </thead>
755
+ <tbody>
756
+ ${secretsResult.findings.map((f) => {
757
+ const sevBadge = f.severity === "critical" ? '<span class="badge badge-red">critical</span>' : f.severity === "high" ? '<span class="badge badge-orange">high</span>' : f.severity === "medium" ? '<span class="badge badge-yellow">medium</span>' : '<span class="badge badge-gray">low</span>';
758
+ const fileDisplay = (f.filePath || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
759
+ const funcDisplay = (f.nodeName || "\u2014").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
760
+ const typeDisplay = (f.type || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
761
+ return `<tr>
762
+ <td>${sevBadge}</td>
763
+ <td>${typeDisplay}</td>
764
+ <td><code style="font-size:0.75rem;color:var(--text-muted)">${fileDisplay}:${f.line}</code></td>
765
+ <td><code style="font-size:0.78rem">${funcDisplay}</code></td>
766
+ <td>${f.entryPointCount > 0 ? `<span class="badge badge-yellow">${f.entryPointCount} entry point${f.entryPointCount !== 1 ? "s" : ""}</span>` : '<span style="color:var(--text-muted)">\u2014</span>'}</td>
767
+ </tr>`;
768
+ }).join("")}
769
+ </tbody>
770
+ </table>
771
+ </div>` : '<div class="empty-state">No secrets detected.</div>'}
772
+ ` : '<div class="empty-state">No secrets detected.</div>'}
773
+ </div>
774
+
775
+ <!-- Tab: SAST Flows -->
776
+ <div class="tab-panel" id="tab-sast">
777
+ ${flowsResult ? `
778
+ <div style="margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem">
779
+ ${flowsResult.length} finding${flowsResult.length !== 1 ? "s" : ""} across ${new Set(flowsResult.map((f) => f.owaspCategory)).size} OWASP categor${new Set(flowsResult.map((f) => f.owaspCategory)).size !== 1 ? "ies" : "y"}
780
+ </div>
781
+ ${flowsResult.length > 0 ? `
782
+ <div class="table-wrap">
783
+ <table id="sastTable">
784
+ <thead>
785
+ <tr>
786
+ <th>OWASP</th>
787
+ <th>Severity</th>
788
+ <th>Type</th>
789
+ <th>File:Line</th>
790
+ <th>Symbol</th>
791
+ <th>Recommendation</th>
792
+ </tr>
793
+ </thead>
794
+ <tbody>
795
+ ${flowsResult.map((f) => {
796
+ const owaspBadge = `<span class="badge badge-accent">${f.owaspCategory}</span>`;
797
+ const sevBadge = f.severity === "critical" ? '<span class="badge badge-red">critical</span>' : f.severity === "high" ? '<span class="badge badge-orange">high</span>' : '<span class="badge badge-yellow">medium</span>';
798
+ const fileDisplay = (f.filePath || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
799
+ const typeDisplay = (f.type || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
800
+ const symbolDisplay = (f.symbol || "\u2014").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
801
+ const recDisplay = (f.recommendation || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
802
+ return `<tr>
803
+ <td>${owaspBadge}</td>
804
+ <td>${sevBadge}</td>
805
+ <td><code style="font-size:0.78rem">${typeDisplay}</code></td>
806
+ <td><code style="font-size:0.75rem;color:var(--text-muted)">${fileDisplay}:${f.line}</code></td>
807
+ <td><code style="font-size:0.78rem;color:var(--accent)">${symbolDisplay}</code></td>
808
+ <td style="max-width:300px;font-size:0.78rem;color:var(--text-muted)">${recDisplay}</td>
809
+ </tr>`;
810
+ }).join("")}
811
+ </tbody>
812
+ </table>
813
+ </div>` : '<div class="empty-state">No dangerous patterns detected.</div>'}
814
+ ` : '<div class="empty-state">No dangerous patterns detected.</div>'}
815
+ </div>
816
+
817
+ <!-- Tab: Remediation -->
818
+ <div class="tab-panel" id="tab-remediation">
819
+ ${remediationStatus ? `
820
+ <div class="stats-grid" style="grid-template-columns:repeat(3,1fr);max-width:500px;margin-bottom:1.5rem">
821
+ <div class="stat-card">
822
+ <div class="stat-label">Overdue</div>
823
+ <div class="stat-value ${remediationStatus.filter((r) => r.slaStatus === "overdue").length > 0 ? "red" : "green"}">${remediationStatus.filter((r) => r.slaStatus === "overdue").length}</div>
824
+ </div>
825
+ <div class="stat-card">
826
+ <div class="stat-label">On Track</div>
827
+ <div class="stat-value green">${remediationStatus.filter((r) => r.slaStatus === "ok" || r.slaStatus === "warning").length}</div>
828
+ </div>
829
+ <div class="stat-card">
830
+ <div class="stat-label">No Fix Available</div>
831
+ <div class="stat-value yellow">${remediationStatus.filter((r) => r.slaStatus === "no_fix").length}</div>
832
+ </div>
833
+ </div>
834
+ ${remediationStatus.length > 0 ? `
835
+ <div class="table-wrap">
836
+ <table id="remediationTable">
837
+ <thead>
838
+ <tr>
839
+ <th>CVE</th>
840
+ <th>Package</th>
841
+ <th>Days Open</th>
842
+ <th>Fix Available</th>
843
+ <th>SLA (days)</th>
844
+ <th>Status</th>
845
+ </tr>
846
+ </thead>
847
+ <tbody>
848
+ ${remediationStatus.map((r) => {
849
+ const statusChip = r.slaStatus === "ok" ? '<span class="badge badge-green">ok</span>' : r.slaStatus === "warning" ? '<span class="badge badge-yellow">warning</span>' : r.slaStatus === "overdue" ? '<span class="badge badge-red">overdue</span>' : '<span class="badge badge-gray">no fix</span>';
850
+ const cveDisplay = (r.cveId || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
851
+ const pkgDisplay = (r.packageName || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
852
+ const slaDays = r.severity !== null ? r.severity >= 9 ? 7 : r.severity >= 7 ? 30 : r.severity >= 4 ? 90 : 180 : 180;
853
+ return `<tr>
854
+ <td><code style="font-size:0.8rem;color:var(--accent)">${cveDisplay}</code></td>
855
+ <td>${pkgDisplay}</td>
856
+ <td>${r.daysOpen != null ? r.daysOpen + "d" : "\u2014"}</td>
857
+ <td>${r.daysWithFixAvailable != null ? `<span class="badge badge-green">${r.daysWithFixAvailable}d ago</span>` : '<span style="color:var(--text-muted)">\u2014</span>'}</td>
858
+ <td>${slaDays}d</td>
859
+ <td>${statusChip}</td>
860
+ </tr>`;
861
+ }).join("")}
862
+ </tbody>
863
+ </table>
864
+ </div>` : '<div class="empty-state">No open remediation items.</div>'}
865
+ ` : '<div class="empty-state">No remediation data available.</div>'}
866
+ </div>
867
+
868
+ </div>
869
+
870
+ <script>
871
+ // \u2500\u2500 Embedded data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
872
+ const VULNS = ${safeJson(vulns)};
873
+ const DEPS = ${safeJson(deps)};
874
+ const LIC_DATA = ${safeJson(licenseData)};
875
+ const STALE_DATA = ${safeJson(stalenessRows)};
876
+ const SBOM_JSON = ${safeJson(sbomJson)};
877
+ const VEX_JSON = ${safeJson(vexJson)};
878
+
879
+ // \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
880
+ document.querySelectorAll('.tab').forEach(btn => {
881
+ btn.addEventListener('click', () => {
882
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
883
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
884
+ btn.classList.add('active');
885
+ const tabId = btn.getAttribute('data-tab');
886
+ document.getElementById('tab-' + tabId).classList.add('active');
887
+ });
888
+ });
889
+
890
+ // \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
891
+ function esc(str) {
892
+ if (str == null) return '';
893
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
894
+ }
895
+ function downloadJson(data, filename) {
896
+ const blob = new Blob([data], {type: 'application/json'});
897
+ const a = document.createElement('a');
898
+ a.href = URL.createObjectURL(blob);
899
+ a.download = filename;
900
+ a.click();
901
+ }
902
+ function downloadSBOM() { downloadJson(SBOM_JSON, 'sbom.json'); }
903
+ function downloadVEX() { downloadJson(VEX_JSON, 'vex.json'); }
904
+
905
+ function epssClass(v) {
906
+ if (v == null) return 'badge-gray';
907
+ if (v >= 0.5) return 'badge-red';
908
+ if (v >= 0.1) return 'badge-yellow';
909
+ return 'badge-gray';
910
+ }
911
+ function cvssClass(v) {
912
+ if (v == null) return '';
913
+ if (v >= 9) return 'score-critical';
914
+ if (v >= 7) return 'score-high';
915
+ if (v >= 4) return 'score-medium';
916
+ return 'score-low';
917
+ }
918
+ function verdictBadge(v) {
919
+ if (!v) return '<span class="badge badge-gray">\u2014</span>';
920
+ if (v === 'affected') return '<span class="badge badge-red">affected</span>';
921
+ if (v === 'not_affected') return '<span class="badge badge-green">not affected</span>';
922
+ if (v === 'under_investigation') return '<span class="badge badge-yellow">investigating</span>';
923
+ return '<span class="badge badge-gray">' + esc(v) + '</span>';
924
+ }
925
+ function staleColor(score) {
926
+ if (score >= 0.7) return 'var(--red)';
927
+ if (score >= 0.3) return 'var(--yellow)';
928
+ return 'var(--green)';
929
+ }
930
+
931
+ // \u2500\u2500 Vulnerability table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
932
+ let vulnSortCol = 'epss_score';
933
+ let vulnSortDir = -1; // -1 = desc
934
+
935
+ function renderVulns() {
936
+ const sevFilter = document.getElementById('filterSeverity').value;
937
+ const verdFilter = document.getElementById('filterVerdict').value;
938
+ const epssMin = parseFloat(document.getElementById('filterEpss').value) || 0;
939
+
940
+ let rows = VULNS.filter(v => {
941
+ if (sevFilter !== 'all') {
942
+ const s = v.severity_score ?? 0;
943
+ if (sevFilter === 'critical' && s < 9) return false;
944
+ if (sevFilter === 'high' && (s < 7 || s >= 9)) return false;
945
+ if (sevFilter === 'medium' && (s < 4 || s >= 7)) return false;
946
+ if (sevFilter === 'low' && s >= 4) return false;
947
+ }
948
+ if (verdFilter !== 'all') {
949
+ if ((v.verdict ?? '') !== verdFilter) return false;
950
+ }
951
+ if ((v.epss_score ?? 0) < epssMin) return false;
952
+ return true;
953
+ });
954
+
955
+ rows = rows.slice().sort((a, b) => {
956
+ const av = a[vulnSortCol] ?? (typeof a[vulnSortCol] === 'number' ? -Infinity : '');
957
+ const bv = b[vulnSortCol] ?? (typeof b[vulnSortCol] === 'number' ? -Infinity : '');
958
+ if (av < bv) return vulnSortDir;
959
+ if (av > bv) return -vulnSortDir;
960
+ return 0;
961
+ });
962
+
963
+ const tbody = document.getElementById('vulnBody');
964
+ const empty = document.getElementById('vulnEmpty');
965
+
966
+ if (rows.length === 0) {
967
+ tbody.innerHTML = '';
968
+ empty.style.display = 'block';
969
+ return;
970
+ }
971
+ empty.style.display = 'none';
972
+
973
+ tbody.innerHTML = rows.map(v => {
974
+ const rowClass = v.verdict === 'affected' ? 'verdict-affected'
975
+ : v.verdict === 'under_investigation' ? 'verdict-investigating'
976
+ : v.verdict === 'not_affected' ? 'verdict-not-affected' : '';
977
+ const riskScore = v.risk_score;
978
+ const riskBadge = riskScore != null
979
+ ? \`<span class="badge \${riskScore >= 7 ? 'badge-red' : riskScore >= 4 ? 'badge-yellow' : 'badge-gray'}">[Risk: \${riskScore.toFixed(1)}]</span>\`
980
+ : '<span style="color:var(--text-muted)">\u2014</span>';
981
+ return \`<tr class="row-toggle \${rowClass}" onclick="toggleExpand(this, \${JSON.stringify(v).replace(/"/g,'&quot;')})">
982
+ <td><code style="font-size:0.8rem;color:var(--accent)">\${esc(v.cve_id)}</code></td>
983
+ <td>\${esc(v.package_name ?? '\u2014')}</td>
984
+ <td><span class="badge badge-gray">\${esc(v.ecosystem ?? '\u2014')}</span></td>
985
+ <td class="\${cvssClass(v.severity_score)}">\${v.severity_score != null ? v.severity_score.toFixed(1) : '\u2014'}</td>
986
+ <td><span class="badge \${epssClass(v.epss_score)}">\${v.epss_score != null ? (v.epss_score*100).toFixed(2)+'%' : '\u2014'}</span></td>
987
+ <td>\${v.epss_percentile != null ? (v.epss_percentile*100).toFixed(0)+'th' : '\u2014'}</td>
988
+ <td>\${riskBadge}</td>
989
+ <td>\${verdictBadge(v.verdict)}</td>
990
+ <td>\${v.fixed_version ? '<span class="badge badge-green">'+esc(v.fixed_version)+'</span>' : '<span style="color:var(--text-muted)">\u2014</span>'}</td>
991
+ </tr>
992
+ <tr class="expand-row" style="display:none">
993
+ <td colspan="9">
994
+ <div class="expand-body" id="expand-\${esc(v.cve_id)}"></div>
995
+ </td>
996
+ </tr>\`;
997
+ }).join('');
998
+ }
999
+
1000
+ function toggleExpand(row, data) {
1001
+ const expandRow = row.nextElementSibling;
1002
+ if (!expandRow) return;
1003
+ const isVisible = expandRow.style.display !== 'none';
1004
+ if (isVisible) {
1005
+ expandRow.style.display = 'none';
1006
+ return;
1007
+ }
1008
+ expandRow.style.display = '';
1009
+ const body = expandRow.querySelector('.expand-body');
1010
+ let html = '';
1011
+ if (data.summary) {
1012
+ html += '<div class="expand-section"><div class="expand-label">Summary</div><div>' + esc(data.summary) + '</div></div>';
1013
+ }
1014
+ if (data.affected_ranges) {
1015
+ try {
1016
+ const ranges = JSON.parse(data.affected_ranges);
1017
+ html += '<div class="expand-section"><div class="expand-label">Affected Ranges</div><pre>' + esc(JSON.stringify(ranges, null, 2)) + '</pre></div>';
1018
+ } catch {}
1019
+ }
1020
+ if (data.paths) {
1021
+ try {
1022
+ const paths = JSON.parse(data.paths);
1023
+ if (Array.isArray(paths) && paths.length > 0) {
1024
+ html += '<div class="expand-section"><div class="expand-label">Call Paths (' + paths.length + ')</div>';
1025
+ paths.slice(0, 3).forEach(p => {
1026
+ if (p.path && Array.isArray(p.path)) {
1027
+ html += '<div style="margin-bottom:0.3rem;font-family:monospace;font-size:0.78rem;color:var(--text-muted)">' + p.path.map(n => esc(n.id || n)).join(' \u2192 ') + '</div>';
1028
+ }
1029
+ });
1030
+ if (paths.length > 3) html += '<div style="color:var(--text-muted);font-size:0.75rem">\u2026 and ' + (paths.length-3) + ' more</div>';
1031
+ html += '</div>';
1032
+ }
1033
+ } catch {}
1034
+ }
1035
+ if (!html) html = '<div style="color:var(--text-muted)">No additional details available.</div>';
1036
+ body.innerHTML = html;
1037
+ }
1038
+
1039
+ // Sort headers
1040
+ document.querySelectorAll('#vulnTable th.sortable').forEach(th => {
1041
+ th.addEventListener('click', () => {
1042
+ const col = th.getAttribute('data-col');
1043
+ if (vulnSortCol === col) {
1044
+ vulnSortDir = -vulnSortDir;
1045
+ th.classList.toggle('sorted-asc', vulnSortDir === 1);
1046
+ th.classList.toggle('sorted-desc', vulnSortDir === -1);
1047
+ } else {
1048
+ document.querySelectorAll('#vulnTable th').forEach(t => { t.classList.remove('sorted-asc','sorted-desc'); });
1049
+ vulnSortCol = col;
1050
+ vulnSortDir = -1;
1051
+ th.classList.add('sorted-desc');
1052
+ }
1053
+ renderVulns();
1054
+ });
1055
+ });
1056
+
1057
+ document.getElementById('filterSeverity').addEventListener('change', renderVulns);
1058
+ document.getElementById('filterVerdict').addEventListener('change', renderVulns);
1059
+ document.getElementById('filterEpss').addEventListener('input', function() {
1060
+ document.getElementById('epssValue').textContent = parseFloat(this.value).toFixed(2);
1061
+ renderVulns();
1062
+ });
1063
+
1064
+ // \u2500\u2500 SBOM table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1065
+ function renderSBOM() {
1066
+ const eco = document.getElementById('filterEco').value;
1067
+ const pkg = document.getElementById('filterPkg').value.toLowerCase();
1068
+
1069
+ const rows = DEPS.filter(d => {
1070
+ if (eco !== 'all' && d.ecosystem !== eco) return false;
1071
+ if (pkg && !d.package_name.toLowerCase().includes(pkg)) return false;
1072
+ return true;
1073
+ });
1074
+
1075
+ const tbody = document.getElementById('sbomBody');
1076
+ const empty = document.getElementById('sbomEmpty');
1077
+ if (rows.length === 0) {
1078
+ tbody.innerHTML = '';
1079
+ empty.style.display = 'block';
1080
+ return;
1081
+ }
1082
+ empty.style.display = 'none';
1083
+
1084
+ tbody.innerHTML = rows.map(d => {
1085
+ const version = d.resolved_version || d.declared_constraint || '\u2014';
1086
+ const purl = d.ecosystem && d.package_name
1087
+ ? 'pkg:' + esc(d.ecosystem) + '/' + esc(d.package_name) + '@' + esc(version)
1088
+ : '\u2014';
1089
+ const scopeBadge = d.classification === 'direct'
1090
+ ? '<span class="badge badge-accent">direct</span>'
1091
+ : '<span class="badge badge-gray">transitive</span>';
1092
+ return \`<tr>
1093
+ <td><strong>\${esc(d.package_name)}</strong></td>
1094
+ <td><span class="badge badge-gray">\${esc(d.ecosystem)}</span></td>
1095
+ <td><code style="font-size:0.78rem">\${esc(version)}</code></td>
1096
+ <td>\${scopeBadge}</td>
1097
+ <td>\${d.license ? esc(d.license) : '<span style="color:var(--text-muted)">\u2014</span>'}</td>
1098
+ <td><code style="font-size:0.72rem;color:var(--text-muted)">\${esc(purl)}</code></td>
1099
+ </tr>\`;
1100
+ }).join('');
1101
+ }
1102
+
1103
+ document.getElementById('filterEco').addEventListener('change', renderSBOM);
1104
+ document.getElementById('filterPkg').addEventListener('input', renderSBOM);
1105
+
1106
+ // \u2500\u2500 VEX table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1107
+ function renderVEX() {
1108
+ let vexData;
1109
+ try { vexData = JSON.parse(VEX_JSON); } catch { vexData = null; }
1110
+ const tbody = document.getElementById('vexBody');
1111
+ const empty = document.getElementById('vexEmpty');
1112
+
1113
+ const vulns = vexData && vexData.vulnerabilities ? vexData.vulnerabilities : [];
1114
+ if (vulns.length === 0) {
1115
+ tbody.innerHTML = '';
1116
+ empty.style.display = 'block';
1117
+ return;
1118
+ }
1119
+ empty.style.display = 'none';
1120
+
1121
+ tbody.innerHTML = vulns.map(entry => {
1122
+ const state = entry.analysis && entry.analysis.state ? entry.analysis.state : '\u2014';
1123
+ const justification = entry.analysis && entry.analysis.justification ? entry.analysis.justification : '\u2014';
1124
+ const detail = entry.analysis && entry.analysis.detail ? entry.analysis.detail : '\u2014';
1125
+ const pkg = entry.affects && entry.affects[0] ? entry.affects[0].ref : '\u2014';
1126
+ const stateClass = state === 'affected' ? 'state-affected'
1127
+ : state === 'not_affected' ? 'state-not_affected'
1128
+ : state === 'under_investigation' ? 'state-investigating' : '';
1129
+ return \`<tr>
1130
+ <td><code style="font-size:0.8rem;color:var(--accent)">\${esc(entry.id)}</code></td>
1131
+ <td><code style="font-size:0.75rem;color:var(--text-muted)">\${esc(pkg)}</code></td>
1132
+ <td><span class="\${stateClass}" style="font-weight:500">\${esc(state)}</span></td>
1133
+ <td>\${esc(justification)}</td>
1134
+ <td style="max-width:300px">\${esc(detail)}</td>
1135
+ </tr>\`;
1136
+ }).join('');
1137
+ }
1138
+
1139
+ // \u2500\u2500 Licenses table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1140
+ function renderLicenses() {
1141
+ // Banners
1142
+ const bannerEl = document.getElementById('licBanners');
1143
+ const denyCount = LIC_DATA.filter(d => d.status === 'deny').length;
1144
+ const warnCount = LIC_DATA.filter(d => d.status === 'warn').length;
1145
+ let banners = '';
1146
+ if (denyCount > 0) banners += \`<div class="policy-banner deny">\u2717 \${denyCount} package\${denyCount !== 1 ? 's' : ''} with denied license\${denyCount !== 1 ? 's' : ''}</div>\`;
1147
+ if (warnCount > 0) banners += \`<div class="policy-banner warn">\u26A0 \${warnCount} package\${warnCount !== 1 ? 's' : ''} with warned license\${warnCount !== 1 ? 's' : ''}</div>\`;
1148
+ bannerEl.innerHTML = banners;
1149
+
1150
+ const pol = document.getElementById('filterLicPolicy').value;
1151
+ const rows = LIC_DATA.filter(d => pol === 'all' || d.status === pol);
1152
+
1153
+ const tbody = document.getElementById('licBody');
1154
+ const empty = document.getElementById('licEmpty');
1155
+ if (rows.length === 0) {
1156
+ tbody.innerHTML = '';
1157
+ empty.style.display = 'block';
1158
+ return;
1159
+ }
1160
+ empty.style.display = 'none';
1161
+
1162
+ tbody.innerHTML = rows.map(d => {
1163
+ let statusHtml;
1164
+ if (d.status === 'deny') statusHtml = '<span class="badge badge-red">\u2717 deny</span>';
1165
+ else if (d.status === 'warn') statusHtml = '<span class="badge badge-yellow">\u26A0 warn</span>';
1166
+ else if (d.status === 'ok') statusHtml = '<span class="badge badge-green">\u2713 ok</span>';
1167
+ else statusHtml = '<span class="badge badge-gray">\u2014 unknown</span>';
1168
+ return \`<tr>
1169
+ <td><strong>\${esc(d.package_name)}</strong></td>
1170
+ <td><span class="badge badge-gray">\${esc(d.ecosystem)}</span></td>
1171
+ <td>\${d.license ? esc(d.license) : '<span style="color:var(--text-muted)">\u2014</span>'}</td>
1172
+ <td>\${statusHtml}</td>
1173
+ </tr>\`;
1174
+ }).join('');
1175
+ }
1176
+
1177
+ document.getElementById('filterLicPolicy').addEventListener('change', renderLicenses);
1178
+
1179
+ // \u2500\u2500 Staleness table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1180
+ function renderStaleness() {
1181
+ const tbody = document.getElementById('staleBody');
1182
+ const empty = document.getElementById('staleEmpty');
1183
+
1184
+ if (STALE_DATA.length === 0) {
1185
+ tbody.innerHTML = '';
1186
+ empty.style.display = 'block';
1187
+ return;
1188
+ }
1189
+ empty.style.display = 'none';
1190
+
1191
+ tbody.innerHTML = STALE_DATA.map(d => {
1192
+ const version = d.resolved_version || d.declared_constraint || '\u2014';
1193
+ const latest = d.latest_version || '\u2014';
1194
+ const score = d.staleness_score ?? 0;
1195
+ const months = d.latest_published
1196
+ ? Math.round((Date.now() - d.latest_published) / (1000 * 60 * 60 * 24 * 30))
1197
+ : null;
1198
+ const barPct = Math.round(score * 100);
1199
+ const barColor = staleColor(score);
1200
+ const scoreClass = score >= 0.7 ? 'score-critical' : score >= 0.3 ? 'score-medium' : 'score-low';
1201
+ return \`<tr>
1202
+ <td><strong>\${esc(d.package_name)}</strong></td>
1203
+ <td><span class="badge badge-gray">\${esc(d.ecosystem)}</span></td>
1204
+ <td><code style="font-size:0.78rem">\${esc(version)}</code></td>
1205
+ <td><code style="font-size:0.78rem;color:var(--green)">\${esc(latest)}</code></td>
1206
+ <td>\${months != null ? months + 'mo' : '\u2014'}</td>
1207
+ <td class="\${scoreClass}">\${score.toFixed(2)}</td>
1208
+ <td>
1209
+ <div class="stale-bar-track">
1210
+ <div class="stale-bar-fill" style="width:\${barPct}%;background:\${barColor}"></div>
1211
+ </div>
1212
+ </td>
1213
+ </tr>\`;
1214
+ }).join('');
1215
+ }
1216
+
1217
+ // \u2500\u2500 Initial render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1218
+ renderVulns();
1219
+ renderSBOM();
1220
+ renderVEX();
1221
+ renderLicenses();
1222
+ renderStaleness();
1223
+ </script>
1224
+
1225
+ </body>
1226
+ </html>`;
1227
+ }
1228
+ function register(secCmd) {
1229
+ secCmd.command("export [projectPath]").description("Generate a self-contained HTML security dashboard").option("--output <file>", "Output HTML file path", ".kirograph/security-export.html").option("--open", "Open in browser after generating").action(async (projectPath, opts) => {
1230
+ const target = path.resolve(projectPath ?? process.cwd());
1231
+ const config = await (0, import_config.loadConfig)(target);
1232
+ if (!config.enableSecurity) {
1233
+ console.error(`
1234
+ ${"\x1B[33m"}\u26A0 Security analysis is disabled.${import_ui.reset}`);
1235
+ console.error(` ${import_ui.dim}Enable it in .kirograph/config.json:${import_ui.reset} ${import_ui.violet}${import_ui.bold}"enableSecurity": true${import_ui.reset}`);
1236
+ console.error(` ${import_ui.dim}Then re-run:${import_ui.reset} ${import_ui.violet}${import_ui.bold}kirograph index${import_ui.reset}
1237
+ `);
1238
+ process.exit(1);
1239
+ }
1240
+ const KiroGraph = (await Promise.resolve().then(() => require("../../index.js"))).default;
1241
+ if (!KiroGraph.isInitialized(target)) {
1242
+ console.error(" \u2716 KiroGraph not initialized. Run: kirograph init");
1243
+ process.exit(1);
1244
+ }
1245
+ const cg = await KiroGraph.open(target);
1246
+ const db = cg.getDatabase();
1247
+ db.applySecuritySchema();
1248
+ const rawDb = db.getRawDb();
1249
+ const rawVulns = rawDb.all(`
1250
+ SELECT v.cve_id, v.severity_score, v.risk_score, v.epss_score, v.epss_percentile,
1251
+ v.fixed_version, v.summary, v.affected_ranges,
1252
+ d.package_name, d.ecosystem, d.resolved_version, d.declared_constraint,
1253
+ d.scope, d.license,
1254
+ r.verdict, r.paths, r.unresolved_symbols, r.reaching_entry_point_count
1255
+ FROM sec_vulnerabilities v
1256
+ LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'
1257
+ LEFT JOIN sec_dependencies d ON d.node_id = e.source
1258
+ LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id
1259
+ ORDER BY v.epss_score DESC NULLS LAST, v.severity_score DESC NULLS LAST
1260
+ `);
1261
+ const _vRank = (v) => v === "affected" ? 3 : v === "under_investigation" ? 2 : v === "not_affected" ? 1 : 0;
1262
+ const _dedupMap = /* @__PURE__ */ new Map();
1263
+ for (const row of rawVulns) {
1264
+ const key = `${row.cve_id}::${row.package_name ?? ""}::${row.ecosystem ?? ""}`;
1265
+ const ex = _dedupMap.get(key);
1266
+ if (!ex || _vRank(row.verdict) > _vRank(ex.verdict)) _dedupMap.set(key, row);
1267
+ }
1268
+ const vulns = [..._dedupMap.values()];
1269
+ const directNodeIds = new Set(
1270
+ rawDb.all(`SELECT source FROM edges WHERE kind = 'declared_in'`).map((r) => r.source)
1271
+ );
1272
+ const rawDeps = rawDb.all(
1273
+ `SELECT node_id, package_name, ecosystem, resolved_version, declared_constraint,
1274
+ scope, license, latest_version, latest_published, staleness_score
1275
+ FROM sec_dependencies ORDER BY package_name`
1276
+ );
1277
+ const deps = rawDeps.map((d) => ({
1278
+ ...d,
1279
+ classification: directNodeIds.has(d.node_id) ? "direct" : "transitive"
1280
+ }));
1281
+ const { SBOMExporter } = await Promise.resolve().then(() => require("../../security/export/sbom.js"));
1282
+ const { VEXExporter } = await Promise.resolve().then(() => require("../../security/export/vex.js"));
1283
+ const sbomExporter = new SBOMExporter(db, target);
1284
+ const vexExporter = new VEXExporter(db, target);
1285
+ const sbomJson = sbomExporter.exportJSON();
1286
+ const vexJson = vexExporter.exportJSON();
1287
+ let attackSurface = null;
1288
+ try {
1289
+ const { AttackSurfaceAnalyzer } = await Promise.resolve().then(() => require("../../security/attack-surface.js"));
1290
+ const analyzer = new AttackSurfaceAnalyzer(db);
1291
+ attackSurface = await analyzer.analyze();
1292
+ } catch {
1293
+ }
1294
+ let secretsResult = null;
1295
+ try {
1296
+ const { SecretsScanner } = await Promise.resolve().then(() => require("../../security/secrets.js"));
1297
+ const scanner = new SecretsScanner(db, target);
1298
+ secretsResult = await scanner.scan();
1299
+ } catch {
1300
+ }
1301
+ let flowsResult = null;
1302
+ try {
1303
+ const { DataFlowAnalyzer } = await Promise.resolve().then(() => require("../../security/data-flows.js"));
1304
+ const analyzer = new DataFlowAnalyzer(db);
1305
+ flowsResult = await analyzer.analyze();
1306
+ } catch {
1307
+ }
1308
+ let remediationStatus = null;
1309
+ try {
1310
+ const { RemediationTracker } = await Promise.resolve().then(() => require("../../security/remediation.js"));
1311
+ const tracker = new RemediationTracker(db);
1312
+ remediationStatus = tracker.getStatus();
1313
+ } catch {
1314
+ }
1315
+ cg.close();
1316
+ const denyList = config.securityLicensePolicy?.deny ?? [];
1317
+ const warnList = config.securityLicensePolicy?.warn ?? [];
1318
+ const projectName = path.basename(target);
1319
+ const generatedAt = (/* @__PURE__ */ new Date()).toLocaleString();
1320
+ const html = generateHtml({
1321
+ projectName,
1322
+ generatedAt,
1323
+ vulns,
1324
+ deps,
1325
+ sbomJson,
1326
+ vexJson,
1327
+ denyList,
1328
+ warnList,
1329
+ attackSurface,
1330
+ secretsResult,
1331
+ flowsResult,
1332
+ remediationStatus
1333
+ });
1334
+ const outPath = path.resolve(target, opts.output);
1335
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1336
+ fs.writeFileSync(outPath, html, "utf-8");
1337
+ console.error(` ${import_ui.green}\u2713${import_ui.reset} Security dashboard written to ${import_ui.violet}${import_ui.bold}${outPath}${import_ui.reset}`);
1338
+ console.error(` ${import_ui.dim}${vulns.length} vulnerabilities \xB7 ${deps.length} dependencies${import_ui.reset}`);
1339
+ if (opts.open) {
1340
+ openBrowser(outPath);
1341
+ console.error(` ${import_ui.dim}Opening in browser\u2026${import_ui.reset}`);
1342
+ }
1343
+ });
1344
+ }
1345
+ // Annotate the CommonJS export names for ESM import in node:
1346
+ 0 && (module.exports = {
1347
+ register
1348
+ });
1349
+ //# sourceMappingURL=security-export.js.map