@prism-d1/cli 1.1.2 → 1.3.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 (255) hide show
  1. package/dist/assets/github-workflows/prism-agent-eval.yml +2 -1
  2. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +12 -2
  3. package/dist/assets/gitlab-workflows/prism-eval-gate.yml +8 -0
  4. package/dist/assets/infra/bin/app.ts +61 -37
  5. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +96 -0
  6. package/dist/assets/infra/lib/constructs/security-agent-construct.ts +35 -30
  7. package/dist/assets/infra/lib/dashboard-stack.ts +27 -0
  8. package/dist/assets/infra/lib/lambda/otel-receiver.ts +278 -8
  9. package/dist/assets/infra/lib/lambda/pricing/litellm-snapshot.json +3769 -0
  10. package/dist/assets/infra/lib/lambda/pricing/price-source.ts +56 -0
  11. package/dist/assets/infra/lib/lambda/pricing/price-table.ts +240 -0
  12. package/dist/assets/infra/lib/lambda/pricing/prices-key.ts +6 -0
  13. package/dist/assets/infra/lib/lambda/pricing/reprice.ts +108 -0
  14. package/dist/assets/infra/lib/lambda/pricing-refresh.ts +352 -0
  15. package/dist/assets/infra/lib/lambda/velocity-widget.ts +124 -1
  16. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +2 -110
  17. package/dist/assets/infra/lib/security-agent-resources.ts +136 -0
  18. package/dist/assets/infra/lib/security-agent-stack.ts +69 -0
  19. package/dist/assets/infra/package.json +4 -1
  20. package/dist/assets/metric-hooks/prepare-commit-msg +3 -1
  21. package/dist/assets/pentest-cdk/bin/app.ts +12 -0
  22. package/dist/assets/pentest-cdk/cdk.json +7 -0
  23. package/dist/assets/pentest-cdk/lib/pentest-target-stack.ts +206 -0
  24. package/dist/assets/pentest-cdk/package.json +18 -0
  25. package/dist/assets/pentest-cdk/tsconfig.json +12 -0
  26. package/dist/src/codewatch/config.d.ts +17 -0
  27. package/dist/src/codewatch/config.d.ts.map +1 -0
  28. package/dist/src/codewatch/config.js +40 -0
  29. package/dist/src/codewatch/config.js.map +1 -0
  30. package/dist/src/codewatch/gitleaks.d.ts +49 -0
  31. package/dist/src/codewatch/gitleaks.d.ts.map +1 -0
  32. package/dist/src/codewatch/gitleaks.js +160 -0
  33. package/dist/src/codewatch/gitleaks.js.map +1 -0
  34. package/dist/src/codewatch/globalHooks.d.ts +52 -0
  35. package/dist/src/codewatch/globalHooks.d.ts.map +1 -0
  36. package/dist/src/codewatch/globalHooks.js +235 -0
  37. package/dist/src/codewatch/globalHooks.js.map +1 -0
  38. package/dist/src/codewatch/hooks.d.ts +38 -0
  39. package/dist/src/codewatch/hooks.d.ts.map +1 -0
  40. package/dist/src/codewatch/hooks.js +176 -0
  41. package/dist/src/codewatch/hooks.js.map +1 -0
  42. package/dist/src/codewatch/prepush.d.ts +37 -0
  43. package/dist/src/codewatch/prepush.d.ts.map +1 -0
  44. package/dist/src/codewatch/prepush.js +231 -0
  45. package/dist/src/codewatch/prepush.js.map +1 -0
  46. package/dist/src/codewatch/results.d.ts +210 -0
  47. package/dist/src/codewatch/results.d.ts.map +1 -0
  48. package/dist/src/codewatch/results.js +127 -0
  49. package/dist/src/codewatch/results.js.map +1 -0
  50. package/dist/src/codewatch/reviewers/claude.d.ts +9 -0
  51. package/dist/src/codewatch/reviewers/claude.d.ts.map +1 -0
  52. package/dist/src/codewatch/reviewers/claude.js +48 -0
  53. package/dist/src/codewatch/reviewers/claude.js.map +1 -0
  54. package/dist/src/codewatch/reviewers/index.d.ts +39 -0
  55. package/dist/src/codewatch/reviewers/index.d.ts.map +1 -0
  56. package/dist/src/codewatch/reviewers/index.js +97 -0
  57. package/dist/src/codewatch/reviewers/index.js.map +1 -0
  58. package/dist/src/codewatch/reviewers/kiro.d.ts +14 -0
  59. package/dist/src/codewatch/reviewers/kiro.d.ts.map +1 -0
  60. package/dist/src/codewatch/reviewers/kiro.js +32 -0
  61. package/dist/src/codewatch/reviewers/kiro.js.map +1 -0
  62. package/dist/src/codewatch/reviewers/types.d.ts +56 -0
  63. package/dist/src/codewatch/reviewers/types.d.ts.map +1 -0
  64. package/dist/src/codewatch/reviewers/types.js +91 -0
  65. package/dist/src/codewatch/reviewers/types.js.map +1 -0
  66. package/dist/src/codewatch/rules.d.ts +12 -0
  67. package/dist/src/codewatch/rules.d.ts.map +1 -0
  68. package/dist/src/codewatch/rules.js +27 -0
  69. package/dist/src/codewatch/rules.js.map +1 -0
  70. package/dist/src/commands/_top/dashboard.d.ts +17 -0
  71. package/dist/src/commands/_top/dashboard.d.ts.map +1 -0
  72. package/dist/src/commands/_top/dashboard.js +76 -0
  73. package/dist/src/commands/_top/dashboard.js.map +1 -0
  74. package/dist/src/commands/_top/version.d.ts +62 -0
  75. package/dist/src/commands/_top/version.d.ts.map +1 -0
  76. package/dist/src/commands/_top/version.js +175 -0
  77. package/dist/src/commands/_top/version.js.map +1 -0
  78. package/dist/src/commands/bedrock-protection/scan-account.d.ts.map +1 -1
  79. package/dist/src/commands/bedrock-protection/scan-account.js +2 -1
  80. package/dist/src/commands/bedrock-protection/scan-account.js.map +1 -1
  81. package/dist/src/commands/bedrock-protection/scan-org.d.ts.map +1 -1
  82. package/dist/src/commands/bedrock-protection/scan-org.js +16 -4
  83. package/dist/src/commands/bedrock-protection/scan-org.js.map +1 -1
  84. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  85. package/dist/src/commands/bootstrapper/install-eval-harness.js +15 -48
  86. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  87. package/dist/src/commands/bootstrapper/install-git-hooks.d.ts +13 -0
  88. package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -1
  89. package/dist/src/commands/bootstrapper/install-git-hooks.js +28 -147
  90. package/dist/src/commands/bootstrapper/install-git-hooks.js.map +1 -1
  91. package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts +12 -6
  92. package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
  93. package/dist/src/commands/bootstrapper/setup-otel-sync.js +18 -443
  94. package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
  95. package/dist/src/commands/codewatch/pre-commit.d.ts +7 -0
  96. package/dist/src/commands/codewatch/pre-commit.d.ts.map +1 -0
  97. package/dist/src/commands/codewatch/pre-commit.js +30 -0
  98. package/dist/src/commands/codewatch/pre-commit.js.map +1 -0
  99. package/dist/src/commands/codewatch/pre-push.d.ts +16 -0
  100. package/dist/src/commands/codewatch/pre-push.d.ts.map +1 -0
  101. package/dist/src/commands/codewatch/pre-push.js +32 -0
  102. package/dist/src/commands/codewatch/pre-push.js.map +1 -0
  103. package/dist/src/commands/codewatch/report.d.ts +22 -0
  104. package/dist/src/commands/codewatch/report.d.ts.map +1 -0
  105. package/dist/src/commands/codewatch/report.js +64 -0
  106. package/dist/src/commands/codewatch/report.js.map +1 -0
  107. package/dist/src/commands/enable/codeburn.d.ts +59 -0
  108. package/dist/src/commands/enable/codeburn.d.ts.map +1 -0
  109. package/dist/src/commands/enable/codeburn.js +531 -0
  110. package/dist/src/commands/enable/codeburn.js.map +1 -0
  111. package/dist/src/commands/enable/codewatch.d.ts +15 -0
  112. package/dist/src/commands/enable/codewatch.d.ts.map +1 -0
  113. package/dist/src/commands/enable/codewatch.js +186 -0
  114. package/dist/src/commands/enable/codewatch.js.map +1 -0
  115. package/dist/src/commands/enable/security.d.ts +41 -0
  116. package/dist/src/commands/enable/security.d.ts.map +1 -0
  117. package/dist/src/commands/enable/security.js +130 -0
  118. package/dist/src/commands/enable/security.js.map +1 -0
  119. package/dist/src/commands/export/otel.d.ts +31 -0
  120. package/dist/src/commands/export/otel.d.ts.map +1 -0
  121. package/dist/src/commands/export/otel.js +52 -0
  122. package/dist/src/commands/export/otel.js.map +1 -0
  123. package/dist/src/commands/git/claude-session-context.d.ts.map +1 -1
  124. package/dist/src/commands/git/claude-session-context.js +2 -1
  125. package/dist/src/commands/git/claude-session-context.js.map +1 -1
  126. package/dist/src/commands/git/commit-trailers.js +1 -1
  127. package/dist/src/commands/git/commit-trailers.js.map +1 -1
  128. package/dist/src/commands/security/code-review.d.ts +22 -0
  129. package/dist/src/commands/security/code-review.d.ts.map +1 -0
  130. package/dist/src/commands/security/code-review.js +43 -0
  131. package/dist/src/commands/security/code-review.js.map +1 -0
  132. package/dist/src/commands/security/findings.d.ts +15 -0
  133. package/dist/src/commands/security/findings.d.ts.map +1 -0
  134. package/dist/src/commands/security/findings.js +31 -0
  135. package/dist/src/commands/security/findings.js.map +1 -0
  136. package/dist/src/commands/security/pentest.d.ts +18 -0
  137. package/dist/src/commands/security/pentest.d.ts.map +1 -0
  138. package/dist/src/commands/security/pentest.js +32 -0
  139. package/dist/src/commands/security/pentest.js.map +1 -0
  140. package/dist/src/commands/security/threat-model.d.ts +16 -0
  141. package/dist/src/commands/security/threat-model.d.ts.map +1 -0
  142. package/dist/src/commands/security/threat-model.js +24 -0
  143. package/dist/src/commands/security/threat-model.js.map +1 -0
  144. package/dist/src/commands/security/threats.d.ts +12 -0
  145. package/dist/src/commands/security/threats.d.ts.map +1 -0
  146. package/dist/src/commands/security/threats.js +23 -0
  147. package/dist/src/commands/security/threats.js.map +1 -0
  148. package/dist/src/commands/securityagent/setup.d.ts +19 -5
  149. package/dist/src/commands/securityagent/setup.d.ts.map +1 -1
  150. package/dist/src/commands/securityagent/setup.js +25 -189
  151. package/dist/src/commands/securityagent/setup.js.map +1 -1
  152. package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -1
  153. package/dist/src/commands/workshop/verify-setup.js +5 -3
  154. package/dist/src/commands/workshop/verify-setup.js.map +1 -1
  155. package/dist/src/dashboard/codewatch.d.ts +106 -0
  156. package/dist/src/dashboard/codewatch.d.ts.map +1 -0
  157. package/dist/src/dashboard/codewatch.js +120 -0
  158. package/dist/src/dashboard/codewatch.js.map +1 -0
  159. package/dist/src/dashboard/commits.d.ts +96 -0
  160. package/dist/src/dashboard/commits.d.ts.map +1 -0
  161. package/dist/src/dashboard/commits.js +207 -0
  162. package/dist/src/dashboard/commits.js.map +1 -0
  163. package/dist/src/dashboard/page.d.ts +10 -0
  164. package/dist/src/dashboard/page.d.ts.map +1 -0
  165. package/dist/src/dashboard/page.js +470 -0
  166. package/dist/src/dashboard/page.js.map +1 -0
  167. package/dist/src/dashboard/payload.d.ts +114 -0
  168. package/dist/src/dashboard/payload.d.ts.map +1 -0
  169. package/dist/src/dashboard/payload.js +96 -0
  170. package/dist/src/dashboard/payload.js.map +1 -0
  171. package/dist/src/dashboard/report.d.ts +99 -0
  172. package/dist/src/dashboard/report.d.ts.map +1 -0
  173. package/dist/src/dashboard/report.js +300 -0
  174. package/dist/src/dashboard/report.js.map +1 -0
  175. package/dist/src/dashboard/repos.d.ts +58 -0
  176. package/dist/src/dashboard/repos.d.ts.map +1 -0
  177. package/dist/src/dashboard/repos.js +111 -0
  178. package/dist/src/dashboard/repos.js.map +1 -0
  179. package/dist/src/dashboard/security.d.ts +130 -0
  180. package/dist/src/dashboard/security.d.ts.map +1 -0
  181. package/dist/src/dashboard/security.js +144 -0
  182. package/dist/src/dashboard/security.js.map +1 -0
  183. package/dist/src/dashboard/server.d.ts +52 -0
  184. package/dist/src/dashboard/server.d.ts.map +1 -0
  185. package/dist/src/dashboard/server.js +163 -0
  186. package/dist/src/dashboard/server.js.map +1 -0
  187. package/dist/src/dashboard/setup.d.ts +67 -0
  188. package/dist/src/dashboard/setup.d.ts.map +1 -0
  189. package/dist/src/dashboard/setup.js +146 -0
  190. package/dist/src/dashboard/setup.js.map +1 -0
  191. package/dist/src/dashboard/usage.d.ts +43 -0
  192. package/dist/src/dashboard/usage.d.ts.map +1 -0
  193. package/dist/src/dashboard/usage.js +88 -0
  194. package/dist/src/dashboard/usage.js.map +1 -0
  195. package/dist/src/index.d.ts +1 -0
  196. package/dist/src/index.d.ts.map +1 -1
  197. package/dist/src/index.js +48 -2
  198. package/dist/src/index.js.map +1 -1
  199. package/dist/src/security/cfn-diff.d.ts +57 -0
  200. package/dist/src/security/cfn-diff.d.ts.map +1 -0
  201. package/dist/src/security/cfn-diff.js +112 -0
  202. package/dist/src/security/cfn-diff.js.map +1 -0
  203. package/dist/src/security/errors.d.ts +45 -0
  204. package/dist/src/security/errors.d.ts.map +1 -0
  205. package/dist/src/security/errors.js +116 -0
  206. package/dist/src/security/errors.js.map +1 -0
  207. package/dist/src/security/marker.d.ts +46 -0
  208. package/dist/src/security/marker.d.ts.map +1 -0
  209. package/dist/src/security/marker.js +120 -0
  210. package/dist/src/security/marker.js.map +1 -0
  211. package/dist/src/security/pentest-dockerfile.d.ts +8 -0
  212. package/dist/src/security/pentest-dockerfile.d.ts.map +1 -0
  213. package/dist/src/security/pentest-dockerfile.js +163 -0
  214. package/dist/src/security/pentest-dockerfile.js.map +1 -0
  215. package/dist/src/security/pentest.d.ts +233 -0
  216. package/dist/src/security/pentest.d.ts.map +1 -0
  217. package/dist/src/security/pentest.js +1057 -0
  218. package/dist/src/security/pentest.js.map +1 -0
  219. package/dist/src/security/repo.d.ts +25 -0
  220. package/dist/src/security/repo.d.ts.map +1 -0
  221. package/dist/src/security/repo.js +38 -0
  222. package/dist/src/security/repo.js.map +1 -0
  223. package/dist/src/security/review.d.ts +202 -0
  224. package/dist/src/security/review.d.ts.map +1 -0
  225. package/dist/src/security/review.js +706 -0
  226. package/dist/src/security/review.js.map +1 -0
  227. package/dist/src/security/setup.d.ts +193 -0
  228. package/dist/src/security/setup.d.ts.map +1 -0
  229. package/dist/src/security/setup.js +728 -0
  230. package/dist/src/security/setup.js.map +1 -0
  231. package/dist/src/security/threat-model.d.ts +133 -0
  232. package/dist/src/security/threat-model.d.ts.map +1 -0
  233. package/dist/src/security/threat-model.js +653 -0
  234. package/dist/src/security/threat-model.js.map +1 -0
  235. package/dist/src/utils/audit-bedrock.d.ts +1 -0
  236. package/dist/src/utils/audit-bedrock.d.ts.map +1 -1
  237. package/dist/src/utils/audit-bedrock.js +133 -0
  238. package/dist/src/utils/audit-bedrock.js.map +1 -1
  239. package/dist/src/utils/audit-org.d.ts +1 -0
  240. package/dist/src/utils/audit-org.d.ts.map +1 -1
  241. package/dist/src/utils/audit-org.js +155 -0
  242. package/dist/src/utils/audit-org.js.map +1 -1
  243. package/dist/src/utils/exec.d.ts +6 -0
  244. package/dist/src/utils/exec.d.ts.map +1 -1
  245. package/dist/src/utils/exec.js +12 -1
  246. package/dist/src/utils/exec.js.map +1 -1
  247. package/dist/src/utils/otel-sync-schedule.d.ts +41 -0
  248. package/dist/src/utils/otel-sync-schedule.d.ts.map +1 -0
  249. package/dist/src/utils/otel-sync-schedule.js +90 -0
  250. package/dist/src/utils/otel-sync-schedule.js.map +1 -0
  251. package/dist/src/utils/tool-check.d.ts +14 -0
  252. package/dist/src/utils/tool-check.d.ts.map +1 -0
  253. package/dist/src/utils/tool-check.js +42 -0
  254. package/dist/src/utils/tool-check.js.map +1 -0
  255. package/package.json +3 -2
@@ -0,0 +1,470 @@
1
+ /**
2
+ * The dashboard's single-page app: one self-contained HTML document with
3
+ * inline CSS and JS (nonce-gated by the CSP), no CDN and no network fetch
4
+ * other than its own GET /api/data, so it works offline.
5
+ *
6
+ * Every value from the payload is placed with textContent or a DOM attribute;
7
+ * innerHTML is never used for data.
8
+ */
9
+ const STYLE = `
10
+ :root { color-scheme: light dark; --bg:#f7f7f8; --card:#fff; --text:#1d1d1f; --muted:#6b6b73; --line:#e3e3e8;
11
+ --ok:#1a7f37; --warn:#9a6700; --bad:#cf222e; --high:#cf222e; --medium:#bf8700; --low:#57606a; --accent:#0969da; }
12
+ @media (prefers-color-scheme: dark) { :root { --bg:#0d1117; --card:#161b22; --text:#e6edf3; --muted:#8b949e; --line:#30363d;
13
+ --ok:#3fb950; --warn:#d29922; --bad:#f85149; --high:#f85149; --medium:#d29922; --low:#8b949e; --accent:#58a6ff; } }
14
+ * { box-sizing: border-box; }
15
+ body { margin:0; font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--text); }
16
+ header { display:flex; align-items:center; gap:12px; padding:14px 20px; border-bottom:1px solid var(--line); background:var(--card); }
17
+ header h1 { font-size:17px; margin:0; } header .meta { color:var(--muted); font-size:12px; margin-left:auto; }
18
+ button { font:inherit; padding:4px 10px; border:1px solid var(--line); border-radius:6px; background:var(--bg); color:var(--text); cursor:pointer; }
19
+ main { padding:16px 20px; display:grid; gap:16px; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
20
+ section.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:14px 16px; min-width:0; }
21
+ section.wide { grid-column: 1 / -1; }
22
+ .sec-repo { border-top:1px solid var(--line); padding:10px 0; } .sec-repo:first-of-type { border-top:none; padding-top:0; } .sec-head { display:flex; gap:6px; align-items:baseline; flex-wrap:wrap; overflow-wrap:anywhere; }
23
+ .sec-pills { display:flex; gap:6px; flex-wrap:wrap; margin:6px 0; } ul.sec-findings { list-style:none; margin:4px 0 0; padding:0; } ul.sec-findings li { padding:6px 0; border-top:1px dashed var(--line); font-size:13px; overflow-wrap:anywhere; } .sec-title { font-weight:600; } ul.sec-findings code { white-space:normal; overflow-wrap:anywhere; }
24
+ h2 { font-size:15px; margin:0 0 10px; } h3 { font-size:13px; margin:14px 0 6px; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
25
+ .empty { color:var(--muted); padding:8px 0; } code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; background:var(--bg); padding:1px 4px; border-radius:4px; }
26
+ .tablewrap { overflow-x:auto; }
27
+ table { width:100%; border-collapse:collapse; font-size:13px; } th, td { text-align:left; padding:5px 6px; border-bottom:1px solid var(--line); vertical-align:top; overflow-wrap:break-word; }
28
+ th { white-space:nowrap; } td.msg { min-width:280px; } td.count { font-variant-numeric:tabular-nums; font-weight:600; } td code { white-space:normal; overflow-wrap:anywhere; }
29
+ th { color:var(--muted); font-weight:600; } td.num { text-align:right; font-variant-numeric: tabular-nums; }
30
+ .pill { white-space:nowrap; display:inline-block; padding:0 7px; border-radius:10px; font-size:12px; font-weight:600; border:1px solid currentColor; }
31
+ .s-ok { color:var(--ok); } .s-warn, .s-findings, .s-skipped { color:var(--warn); } .s-missing, .s-error { color:var(--bad); }
32
+ .sev-high { color:var(--high); } .sev-medium { color:var(--medium); } .sev-low { color:var(--low); }
33
+ .stats { display:flex; gap:18px; flex-wrap:wrap; margin:6px 0; } .stat b { display:block; font-size:20px; } .stat span { color:var(--muted); font-size:12px; }
34
+ .fix { color:var(--muted); font-size:12px; overflow-wrap:anywhere; }
35
+ .tag { margin-left:6px; font-size:11px; padding:0 6px; border-radius:8px; background:var(--bg); border:1px solid var(--line); color:var(--muted); white-space:nowrap; }
36
+ details.missing-group > summary { cursor:pointer; padding:8px 0; color:var(--muted); font-weight:600; }
37
+ details.repo { border-top:1px solid var(--line); padding:8px 0; } details.repo > summary { cursor:pointer; padding:4px 0; } details.repo .detail { padding-left:14px; }
38
+ .callout { border:1px solid var(--accent); border-left-width:4px; border-radius:6px; padding:10px 12px; margin:6px 0 10px; background:color-mix(in srgb, var(--accent) 8%, var(--card)); }
39
+ .callout.warn { border-color:var(--warn); background:color-mix(in srgb, var(--warn) 10%, var(--card)); } .callout code { white-space:normal; overflow-wrap:anywhere; }
40
+ .crow { display:grid; grid-template-columns: 6.5em minmax(10em,2.2fr) minmax(5em,0.8fr) 10.5em 7.5em minmax(11em,1.6fr); gap:8px; padding:6px 6px; border-bottom:1px solid var(--line); align-items:start; font-size:13px; overflow-wrap:anywhere; }
41
+ .chead { color:var(--muted); font-weight:600; } summary.crow { cursor:pointer; list-style:none; } summary.crow::-webkit-details-marker { display:none; } details.cdetails[open] > summary.crow { background:var(--bg); } details.cdetails > .tablewrap { padding:4px 0 8px 18px; }
42
+ @media (max-width: 760px) { .crow { grid-template-columns: 1fr 1fr; } .chead { display:none; } }
43
+ a.rlink { color:var(--accent); } a.rlink:hover { text-decoration:underline; }
44
+ .banner { border:1px solid var(--warn); border-left-width:4px; border-radius:6px; padding:10px 12px; margin:4px 0 12px; background:color-mix(in srgb, var(--warn) 10%, var(--card)); }
45
+ .banner strong { color:var(--text); display:block; margin-bottom:4px; } .banner .fix code { white-space:normal; overflow-wrap:anywhere; } .note { color:var(--muted); font-size:12px; margin-top:10px; }
46
+ svg.trend rect.high { fill:var(--high); } svg.trend rect.medium { fill:var(--medium); } svg.trend rect.low { fill:var(--low); } svg.trend rect.zero { fill:var(--ok); }
47
+ svg.trend line { stroke:var(--line); }
48
+ .repo-title { font-size:12px; color:var(--muted); margin-bottom:6px; word-break:break-all; }
49
+ `;
50
+ const SCRIPT = `
51
+ 'use strict';
52
+ const SVG = 'http://www.w3.org/2000/svg';
53
+ function el(tag, attrs, ...kids) {
54
+ const n = document.createElement(tag);
55
+ for (const [k, v] of Object.entries(attrs || {})) { if (v !== null && v !== undefined) n.setAttribute(k, String(v)); }
56
+ for (const c of kids.flat()) { if (c === null || c === undefined || c === false) continue; n.append(c instanceof Node ? c : document.createTextNode(String(c))); }
57
+ return n;
58
+ }
59
+ const txt = (v) => (v === null || v === undefined ? '' : String(v));
60
+ const pill = (status, label) => el('span', { class: 'pill s-' + txt(status).replace(/[^a-z-]/g, '') }, label || status);
61
+ const sev = (s) => el('span', { class: 'sev-' + txt(s).replace(/[^a-z]/g, '') }, s);
62
+ const when = (iso) => { const d = new Date(iso); return isNaN(d) ? txt(iso) : d.toLocaleString(); };
63
+ const short = (sha) => (sha ? txt(sha).slice(0, 8) : '');
64
+ function money(n, cur) { try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: cur || 'USD' }).format(n || 0); } catch { return '$' + (n || 0).toFixed(2); } }
65
+ function table(head, rows) {
66
+ return el('div', { class: 'tablewrap' }, el('table', {}, el('thead', {}, el('tr', {}, head.map((h) => el('th', {}, h)))),
67
+ el('tbody', {}, rows.map((r) => el('tr', {}, r.map((c) => (c instanceof Node && c.nodeName === 'TD') ? c : el('td', {}, c)))))));
68
+ }
69
+ const num = (v) => el('td', { class: 'num' }, txt(v));
70
+
71
+ function trendSvg(trend) {
72
+ const w = Math.max(trend.length * 14, 140), h = 48, svg = document.createElementNS(SVG, 'svg');
73
+ svg.setAttribute('class', 'trend'); svg.setAttribute('width', String(w)); svg.setAttribute('height', String(h + 2));
74
+ svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', 'findings per run, oldest first');
75
+ const max = Math.max(1, ...trend.map((r) => r.high + r.medium + r.low));
76
+ const base = document.createElementNS(SVG, 'line');
77
+ base.setAttribute('x1', '0'); base.setAttribute('x2', String(w)); base.setAttribute('y1', String(h)); base.setAttribute('y2', String(h)); svg.append(base);
78
+ trend.forEach((r, i) => {
79
+ let y = h; const parts = [['high', r.high], ['medium', r.medium], ['low', r.low]];
80
+ const total = r.high + r.medium + r.low;
81
+ if (total === 0) parts.push(['zero', 0]);
82
+ for (const [cls, n] of parts) {
83
+ const bh = cls === 'zero' ? 3 : Math.round((n / max) * (h - 4)); if (!bh) continue;
84
+ y -= bh; const rect = document.createElementNS(SVG, 'rect');
85
+ rect.setAttribute('x', String(i * 14 + 2)); rect.setAttribute('y', String(y)); rect.setAttribute('width', '10'); rect.setAttribute('height', String(bh)); rect.setAttribute('class', cls);
86
+ const t = document.createElementNS(SVG, 'title'); t.textContent = when(r.started_at) + ' ' + r.kind + ' ' + r.status + ' (' + r.tool + '): ' + r.high + ' high, ' + r.medium + ' medium, ' + r.low + ' low';
87
+ rect.append(t); svg.append(rect);
88
+ }
89
+ });
90
+ return svg;
91
+ }
92
+
93
+ function scanBanner(ss) {
94
+ if (!ss || !ss.banner) return null;
95
+ return el('div', { class: 'banner', role: 'alert' },
96
+ el('strong', {}, ss.banner),
97
+ el('div', { class: 'fix' }, 'Install: ', el('code', {}, ss.installHint)));
98
+ }
99
+
100
+ function repoName(r) {
101
+ return el('span', { title: r.path }, r.name, r.current ? el('span', { class: 'tag' }, 'this repo') : null);
102
+ }
103
+
104
+ const SCAN_LABEL = { ok: ['ok', 'ok'], 'not-scanned': ['warn', 'not scanned'], 'no-verify': ['warn', 'no-verify'], flagged: ['missing', 'secrets found'], none: ['skipped', '-'] };
105
+
106
+ /** Link to a scan report. Ids come from the server's own listing; encodeURIComponent keeps them literal. */
107
+ const reportLink = (id, label) => el('a', { href: '/report/' + encodeURIComponent(id), class: 'rlink' }, label);
108
+
109
+ function reviewCell(rv) {
110
+ if (rv.state === 'not-reviewed') return el('span', { class: 'fix' }, 'not reviewed');
111
+ const link = rv.reportId ? [' ', reportLink(rv.reportId, 'report')] : [];
112
+ if (rv.state === 'failed') return el('span', { title: txt(rv.reason) }, pill('error', 'review failed'), link);
113
+ const who = el('span', { class: 'fix' }, ' ' + txt(rv.tool || '') + (rv.model ? ' / ' + rv.model : '') + (rv.reviewCount > 1 ? ' · reviewed ' + rv.reviewCount + 'x' : ''));
114
+ if (rv.state === 'clean') return el('span', {}, pill('ok', 'reviewed, clean'), who, link);
115
+ const counts = el('span', {},
116
+ rv.high ? el('b', { class: 'sev-high' }, rv.high + ' high ') : null,
117
+ rv.medium ? el('b', { class: 'sev-medium' }, rv.medium + ' medium ') : null,
118
+ rv.low ? el('b', { class: 'sev-low' }, rv.low + ' low') : null);
119
+ return el('span', {}, counts, who, link);
120
+ }
121
+
122
+ /** The newest reviews, each linking to its report. */
123
+ function scansList(scans) {
124
+ if (!scans || !scans.length) return el('p', { class: 'empty' }, 'No pre-push reviews yet.');
125
+ return table(['When', 'Branch', 'Reviewer', 'Result', 'Findings', ''], scans.map((s) => [
126
+ when(s.started_at), txt(s.branch || '-'), txt(s.tool) + (s.model ? ' / ' + s.model : ''),
127
+ pill(s.status === 'ok' ? 'ok' : s.status === 'findings' ? 'findings' : s.status === 'error' ? 'error' : 'skipped', s.status === 'ok' ? 'clean' : s.status),
128
+ el('span', {}, s.high ? el('b', { class: 'sev-high' }, s.high + ' high ') : null, s.medium ? el('b', { class: 'sev-medium' }, s.medium + ' medium ') : null, s.low ? el('b', { class: 'sev-low' }, s.low + ' low') : null, (s.high + s.medium + s.low) ? null : '-'),
129
+ reportLink(s.id, 'Report'),
130
+ ]));
131
+ }
132
+
133
+ /** Commits as rows; a row with findings expands to list them. */
134
+ function commitsList(c) {
135
+ const wrap = el('div', { class: 'commits' });
136
+ if (!c || !c.rows.length) { wrap.append(el('p', { class: 'empty' }, 'No commits recorded yet.')); return wrap; }
137
+ wrap.append(el('div', { class: 'crow chead' }, el('span', {}, 'Commit'), el('span', {}, 'Subject'), el('span', {}, 'Branch'), el('span', {}, 'Date'), el('span', {}, 'Secret scan'), el('span', {}, 'Review')));
138
+ for (const r of c.rows) {
139
+ const scan = SCAN_LABEL[r.secretScan.state] || ['skipped', r.secretScan.state];
140
+ const cells = [
141
+ el('code', {}, r.short),
142
+ el('span', { class: r.inHistory ? '' : 'fix' }, r.inHistory ? txt(r.subject) : '(no longer in history)'),
143
+ el('span', {}, txt(r.branch || '-')),
144
+ el('span', {}, r.authorDate ? when(r.authorDate) : '-'),
145
+ r.secretScan.state === 'none' ? el('span', { class: 'fix' }, '-') : el('span', { title: txt(r.secretScan.reason) }, pill(scan[0], scan[1])),
146
+ reviewCell(r.review),
147
+ ];
148
+ if (!r.findings.length) { wrap.append(el('div', { class: 'crow' }, cells)); continue; }
149
+ const d = el('details', { class: 'cdetails' }, el('summary', { class: 'crow' }, cells));
150
+ d.append(table(['Severity', 'Category', 'Location', 'Rule', 'Message'], r.findings.map((f) => [
151
+ sev(f.severity), f.category, el('code', {}, f.file + (f.line ? ':' + f.line : '')), f.rule,
152
+ el('td', { class: 'msg' }, f.message, f.inPushOf ? el('div', { class: 'fix' }, 'in push of ' + txt(f.inPushOf).slice(0, 8) + ' (blame could not place this line)') : null)])));
153
+ wrap.append(d);
154
+ }
155
+ if (c.truncated) wrap.append(el('p', { class: 'note' }, 'showing ' + c.rows.length + ' of ' + c.total + ' commits'));
156
+ return wrap;
157
+ }
158
+
159
+ /** One repo's detail, shown inside its <details>. */
160
+ function codewatchDetail(repo) {
161
+ const cw = repo.codewatch, s = el('div', { class: 'detail' });
162
+ if (!cw.hasResults) {
163
+ const fix = repo.setup && repo.setup.fix ? repo.setup.fix : 'prism-cli enable codewatch';
164
+ s.append(el('p', { class: 'empty' }, 'No codewatch results yet. Run ', el('code', {}, fix), ', then commit and push.'));
165
+ return s;
166
+ }
167
+ s.append(el('div', { class: 'stats' },
168
+ el('div', { class: 'stat' }, el('b', {}, cw.byCategory.security), el('span', {}, 'security')),
169
+ el('div', { class: 'stat' }, el('b', {}, cw.byCategory.quality), el('span', {}, 'quality')),
170
+ el('div', { class: 'stat' }, el('b', {}, cw.gitleaksBlocks.length), el('span', {}, 'gitleaks blocks')),
171
+ el('div', { class: 'stat' }, el('b', {}, cw.skipped.length), el('span', {}, 'pushed without a scan'))));
172
+
173
+ s.append(el('h3', {}, 'Commits'));
174
+ s.append(commitsList(cw.commits));
175
+
176
+ s.append(el('h3', {}, 'Scans (last ' + cw.scans.length + ')'));
177
+ s.append(scansList(cw.scans));
178
+
179
+ s.append(el('h3', {}, 'Trend (last ' + cw.trend.length + ' runs)'));
180
+ s.append(trendSvg(cw.trend));
181
+
182
+ s.append(el('h3', {}, 'Gitleaks blocks'));
183
+ if (!cw.gitleaksBlocks.length) s.append(el('p', { class: 'empty' }, 'No commits blocked.'));
184
+ else s.append(table(['When', 'Branch', 'Result', 'Secrets'], cw.gitleaksBlocks.slice(0, 20).map((r) => [when(r.started_at), txt(r.branch), pill(r.status), r.status === 'findings' ? r.high : txt(r.reason)])));
185
+
186
+ s.append(el('h3', {}, 'Commits pushed without a pre-commit scan'));
187
+ if (!cw.skipped.length) s.append(el('p', { class: 'empty' }, 'None.'));
188
+ else {
189
+ s.append(table(['Commit', 'Branch', 'Recorded'], cw.skipped.slice(0, 20).map((r) => [el('code', {}, short(r.commit)), txt(r.branch), when(r.started_at)])));
190
+ s.append(el('p', { class: 'note' }, 'Usually git commit --no-verify; rebases and cherry-picks also show up here.'));
191
+ }
192
+ return s;
193
+ }
194
+
195
+ function codewatchPanel(data) {
196
+ const s = el('section', { class: 'card wide' }, el('h2', {}, 'Codewatch'));
197
+ const banner = scanBanner(data.machine.secretScanning);
198
+ if (banner) s.append(banner);
199
+ const rows = data.overview.codewatch;
200
+ if (!rows.length) {
201
+ const sg = (data.suggestions || []).find((x) => x.id === 'codewatch-global');
202
+ if (sg) s.append(el('p', { class: 'empty' }, "Codewatch isn't enabled in any repo yet. Cover every repo at once: ", el('code', {}, sg.command), ', or run ', el('code', {}, 'prism-cli enable codewatch'), ' inside one repo.'));
203
+ else s.append(el('p', { class: 'empty' }, "Codewatch isn't enabled in any repo yet. Run ", el('code', {}, 'prism-cli enable codewatch'), ' inside a repo.'));
204
+ return s;
205
+ }
206
+ const count = (n, cls) => el('td', { class: 'count' + (n ? ' ' + cls : '') }, n);
207
+ s.append(table(['Repo', 'High', 'Medium', 'Low', 'Last run', 'Reviewer', 'Unscanned', 'Gitleaks'], rows.map((r) => [
208
+ repoName(r), count(r.high, 'sev-high'), count(r.medium, 'sev-medium'), count(r.low, 'sev-low'),
209
+ r.lastRunAt ? (r.lastReviewId ? reportLink(r.lastReviewId, when(r.lastRunAt)) : when(r.lastRunAt)) : 'never', txt(r.reviewer || '-'), count(r.unscannedCommits, 'sev-medium'),
210
+ r.gitleaksWarning ? pill('warn', 'not scanned') : '-',
211
+ ])));
212
+ const byPath = Object.fromEntries(data.repos.map((r) => [r.path, r]));
213
+ s.append(el('h3', {}, 'Per repo'));
214
+ for (const row of rows) {
215
+ const repo = byPath[row.path]; if (!repo) continue;
216
+ const total = row.high + row.medium + row.low;
217
+ const d = el('details', repo.current ? { class: 'repo', open: '' } : { class: 'repo' },
218
+ el('summary', {}, el('b', {}, repo.name), ' ', el('span', { class: 'fix' }, repo.codewatch.hasResults ? total + ' finding' + (total === 1 ? '' : 's') + ' in the latest reviews' : 'no results yet'),
219
+ repo.current ? el('span', { class: 'tag' }, 'this repo') : null),
220
+ el('div', { class: 'repo-title' }, repo.path));
221
+ d.append(codewatchDetail(repo));
222
+ s.append(d);
223
+ }
224
+ return s;
225
+ }
226
+
227
+ function usagePanel(u) {
228
+ const s = el('section', { class: 'card' }, el('h2', {}, 'Usage (codeburn)'));
229
+ if (u.status !== 'ok') {
230
+ s.append(el('p', { class: 'empty' }, pill(u.status === 'no-data' ? 'warn' : 'missing', u.status), ' ', txt(u.message)));
231
+ } else {
232
+ s.append(el('div', { class: 'stats' },
233
+ el('div', { class: 'stat' }, el('b', {}, money(u.today.cost, u.currency)), el('span', {}, 'today, ' + u.today.calls + ' calls')),
234
+ el('div', { class: 'stat' }, el('b', {}, money(u.month.cost, u.currency)), el('span', {}, 'this month, ' + u.month.calls + ' calls, ' + u.month.sessions + ' sessions'))));
235
+ s.append(el('h3', {}, 'Top models this month'));
236
+ s.append(table(['Model', 'Cost', 'Calls'], u.topModels.map((m) => [m.name, num(money(m.cost, u.currency)), num(m.calls)])));
237
+ s.append(el('h3', {}, 'Top projects this month'));
238
+ s.append(table(['Project', 'Cost', 'Sessions'], u.topProjects.map((p) => [el('span', { title: p.path }, p.name), num(money(p.cost, u.currency)), num(p.sessions)])));
239
+ }
240
+ s.append(el('p', { class: 'note' }, 'Full charts: run ', el('code', {}, txt(u.fullView)), ' in a terminal.'));
241
+ return s;
242
+ }
243
+
244
+ /** Info (or warn) callout for the global install. Text only: the page never runs anything. */
245
+ function suggestionCallout(sg) {
246
+ if (sg.style === 'warn') {
247
+ return el('div', { class: 'callout warn', role: 'note' },
248
+ el('strong', {}, sg.missing + " repos don't have codewatch."),
249
+ el('div', {}, 'Your global core.hooksPath is set to ', el('code', {}, txt(sg.currentHooksPath)), ', so the plain global install would refuse. Use ',
250
+ el('code', {}, sg.command), ': it saves that value and still runs its hooks after codewatch.'));
251
+ }
252
+ return el('div', { class: 'callout info', role: 'note' },
253
+ el('strong', {}, sg.missing + " repos don't have codewatch. Cover all of them at once: "), el('code', {}, sg.command),
254
+ el('div', { class: 'fix' }, 'Applies to every repo on this machine, including new clones. Repos can opt out with ', el('code', {}, 'git config prism.codewatch false'), '.'));
255
+ }
256
+
257
+ function setupPanel(data) {
258
+ const s = el('section', { class: 'card' }, el('h2', {}, 'Setup checklist'));
259
+ const rows = (items) => items.map((i) => [pill(i.status), i.label, el('div', {}, i.detail, i.fix ? el('div', { class: 'fix' }, 'Fix: ' + i.fix) : null)]);
260
+ s.append(el('h3', {}, 'This machine'), table(['', 'Check', 'Detail'], rows(data.machine.setup)));
261
+ const repos = data.overview.setup, c = data.overview.counts;
262
+ s.append(el('h3', {}, 'Per repo: codewatch (' + c.codewatch + ' of ' + c.repos + ' installed)'));
263
+ const label = { ok: 'ok', warn: 'warn', missing: 'missing', 'opted-out': 'opted out' };
264
+ const pillClass = { ok: 'ok', warn: 'warn', missing: 'missing', 'opted-out': 'skipped' };
265
+ const rowOf = (r) => [
266
+ repoName(r),
267
+ el('div', {}, pill(pillClass[r.status] || 'missing', (label[r.status] || r.status) + (r.status === 'ok' && r.global ? ' (global)' : '')), ' ',
268
+ r.status === 'ok' ? 'reviewer ' + txt(r.reviewer || 'none') : txt(r.detail),
269
+ r.fix ? el('div', { class: 'fix' }, 'Fix: ', el('code', {}, r.fix)) : null),
270
+ securityCell(r.security),
271
+ ];
272
+ const sg = (data.suggestions || []).find((x) => x.id === 'codewatch-global');
273
+ if (sg) s.append(suggestionCallout(sg));
274
+ // Always shown: the current repo (installed or not) and every installed repo.
275
+ const shown = repos.filter((r) => !r.collapsed);
276
+ const hidden = repos.filter((r) => r.collapsed);
277
+ if (shown.length) s.append(table(['Repo', 'Codewatch', 'Security Agent'], shown.map(rowOf)));
278
+ else if (!hidden.length) s.append(el('p', { class: 'empty' }, 'No git repositories found under ' + txt(data.scope.root_display) + '.'));
279
+ if (hidden.length) {
280
+ s.append(el('details', { class: 'missing-group' },
281
+ el('summary', {}, hidden.length + ' repo' + (hidden.length === 1 ? '' : 's') + ' without codewatch'),
282
+ table(['Repo', 'Codewatch', 'Security Agent'], hidden.map(rowOf))));
283
+ }
284
+ return s;
285
+ }
286
+
287
+ /** Security Agent per repo, from the enable security marker (local evidence; the page makes no AWS calls). */
288
+ function securityCell(sec) {
289
+ if (!sec) return el('span', { class: 'fix' }, '-');
290
+ return el('div', {}, pill(sec.status === 'ok' ? 'ok' : 'missing', sec.status), ' ', sec.status === 'ok' ? txt(sec.detail) : '',
291
+ sec.fix ? el('div', { class: 'fix' }, 'Fix: ', el('code', {}, sec.fix)) : null);
292
+ }
293
+
294
+ /** Cached Security Agent code review results; the page only reads them. */
295
+ const RISKS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFORMATIONAL', 'UNKNOWN'];
296
+ const RISK_CLASS = { CRITICAL: 'sev-high', HIGH: 'sev-high', MEDIUM: 'sev-medium', LOW: 'sev-low' };
297
+ /** A risk-level pill; colour follows the codewatch severity colours. */
298
+ const riskPill = (level, label) => el('span', { class: 'pill ' + (RISK_CLASS[level] || 'fix') }, label);
299
+
300
+ /** One column wide (fits beside Usage and Setup): a compact block per repo, findings as wrapping rows. */
301
+ function securityPanel(sec) {
302
+ const s = el('section', { class: 'card' }, el('h2', {}, 'Security findings'));
303
+ const rows = (sec && sec.repos) || [];
304
+ const tms = (sec && sec.threat_models) || [];
305
+ const pts = (sec && sec.pentests) || [];
306
+ if (!rows.length && !tms.length && !pts.length) {
307
+ s.append(el('p', { class: 'empty' }, 'No Security Agent code reviews, threat models or penetration tests yet. Run ', el('code', {}, (sec && sec.hint) || 'prism-cli security code-review'), ', ', el('code', {}, 'prism-cli security threat-model'), ' or ', el('code', {}, 'prism-cli security pentest'), ' in a repo set up with ', el('code', {}, 'prism-cli enable security'), '.'));
308
+ return s;
309
+ }
310
+ for (const r of rows) {
311
+ const counts = r.counts || {};
312
+ const b = el('div', { class: 'sec-repo' });
313
+ b.append(el('div', { class: 'sec-head' }, el('b', {}, txt(r.name)), r.current ? el('span', { class: 'tag' }, 'this repo') : null));
314
+ b.append(el('div', { class: 'fix' },
315
+ (r.pending ? 'started ' + when(r.started_at) : when(r.finished_at)) + ' · ' + (r.scope === 'diff' ? 'branch diff' : r.scope === 'full' ? 'whole repo' : 'fetched') + ' · job ' + txt(r.job_id).slice(0, 12)));
316
+ const pills = el('div', { class: 'sec-pills' });
317
+ if (r.pending) {
318
+ // Results are local: the page only knows the job was started, not whether it has finished since.
319
+ pills.append(pill('warn', 'review running'));
320
+ if (r.stale) pills.append(pill('skipped', 'stale?'));
321
+ b.append(pills, el('p', { class: 'note' }, r.stale ? 'Started over 2 h ago and not fetched yet; it has most likely finished. ' : '', 'Fetch the results with ', el('code', {}, (sec && sec.pending_hint) || 'prism-cli security findings'), '.'));
322
+ s.append(b);
323
+ continue;
324
+ }
325
+ if (r.status === 'error') pills.append(pill('error', txt(r.job_status).toLowerCase()));
326
+ else {
327
+ const shown = RISKS.filter((k) => (counts[k] || 0) > 0);
328
+ if (!shown.length) pills.append(pill('ok', 'no findings'));
329
+ for (const k of shown) pills.append(riskPill(k, counts[k] + ' ' + k.toLowerCase()));
330
+ }
331
+ b.append(pills);
332
+ if (r.reason) b.append(el('p', { class: 'note' }, txt(r.reason)));
333
+ if (r.status === 'error' && !r.findings.length) b.append(el('p', { class: 'empty' }, 'The job did not complete.'));
334
+ if (r.findings.length) {
335
+ const list = el('ul', { class: 'sec-findings' });
336
+ for (const f of r.findings) {
337
+ list.append(el('li', {},
338
+ riskPill(f.risk_level, txt(f.risk_level).toLowerCase()), ' ',
339
+ el('span', { class: 'sec-title' }, txt(f.title)),
340
+ el('div', { class: 'fix' }, el('code', {}, f.file ? f.file + (f.line ? ':' + f.line : '') : 'no location'), f.cwe ? ' · ' + txt(f.cwe) : '')));
341
+ }
342
+ b.append(list);
343
+ }
344
+ if (r.truncated) b.append(el('p', { class: 'note' }, 'showing the first ' + r.findings.length + '; run prism-cli security findings for all'));
345
+ s.append(b);
346
+ }
347
+ securityPentestRows(s, sec);
348
+ if (tms.length) {
349
+ s.append(el('h3', {}, 'Threat models'));
350
+ for (const t of tms) s.append(threatModelBlock(t, (sec && sec.threats_hint) || 'prism-cli security threats'));
351
+ }
352
+ return s;
353
+ }
354
+
355
+ const THREAT_LEVELS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
356
+ /** "INFORMATION_DISCLOSURE" -> "information disclosure". */
357
+ const strideText = (list) => (list || []).map((x) => txt(x).toLowerCase().replace(/_/g, ' ')).join(', ');
358
+
359
+ /** One cached threat model per repo, in the code review blocks' compact style. */
360
+ function threatModelBlock(t, hint) {
361
+ const b = el('div', { class: 'sec-repo' });
362
+ b.append(el('div', { class: 'sec-head' }, el('b', {}, txt(t.name)), t.current ? el('span', { class: 'tag' }, 'this repo') : null));
363
+ const docs = (t.scope_docs || []).length;
364
+ b.append(el('div', { class: 'fix' },
365
+ (t.pending ? 'started ' + when(t.started_at) : when(t.finished_at)) + ' · commit ' + txt(t.commit).slice(0, 12)
366
+ + ' · ' + (docs ? docs + ' scope doc' + (docs === 1 ? '' : 's') : 'source only') + ' · job ' + txt(t.job_id).slice(0, 12)));
367
+ const pills = el('div', { class: 'sec-pills' });
368
+ if (t.pending) {
369
+ // Results are local: the page only knows the job was started, not whether it has finished since.
370
+ pills.append(pill('warn', 'threat model running'));
371
+ if (t.stale) pills.append(pill('skipped', 'stale?'));
372
+ b.append(pills, el('p', { class: 'note' }, t.stale ? 'Started over 2 h ago and not fetched yet; it has most likely finished. ' : '', 'Fetch the threats with ', el('code', {}, hint), '.'));
373
+ return b;
374
+ }
375
+ if (t.status === 'error') pills.append(pill('error', txt(t.job_status).toLowerCase()));
376
+ else {
377
+ const counts = t.counts || {};
378
+ const shown = THREAT_LEVELS.filter((k) => (counts[k] || 0) > 0);
379
+ if (!shown.length) pills.append(pill('ok', 'no threats'));
380
+ for (const k of shown) pills.append(riskPill(k, counts[k] + ' ' + k.toLowerCase()));
381
+ }
382
+ b.append(pills);
383
+ if (t.reason) b.append(el('p', { class: 'note' }, txt(t.reason)));
384
+ if (t.overview) b.append(el('p', { class: 'note' }, txt(t.overview)));
385
+ if ((t.threats || []).length) {
386
+ const list = el('ul', { class: 'sec-findings' });
387
+ for (const x of t.threats) {
388
+ list.append(el('li', {},
389
+ riskPill(x.severity, txt(x.severity).toLowerCase()), ' ',
390
+ el('span', { class: 'sec-title' }, txt(x.title)),
391
+ (x.stride || []).length ? el('div', { class: 'fix' }, strideText(x.stride)) : null));
392
+ }
393
+ b.append(list);
394
+ }
395
+ if (t.more) b.append(el('p', { class: 'note' }, t.more + ' more; run ' + hint + ' for all'));
396
+ return b;
397
+ }
398
+
399
+ /** Penetration-test rows, in the same Security card below the code-review rows. */
400
+ function securityPentestRows(s, sec) {
401
+ const rows = (sec && sec.pentests) || [];
402
+ if (!rows.length) return;
403
+ s.append(el('h3', {}, 'Penetration tests'));
404
+ for (const r of rows) {
405
+ const counts = r.counts || {};
406
+ const b = el('div', { class: 'sec-repo' });
407
+ b.append(el('div', { class: 'sec-head' }, el('b', {}, txt(r.name)), r.current ? el('span', { class: 'tag' }, 'this repo') : null));
408
+ b.append(el('div', { class: 'fix' },
409
+ (r.pending ? 'started ' + when(r.started_at) : when(r.finished_at)) + ' · ' + txt(r.target_host) + ' · job ' + txt(r.job_id).slice(0, 12)));
410
+ const pills = el('div', { class: 'sec-pills' });
411
+ if (r.pending) {
412
+ pills.append(pill('warn', 'pentest running'));
413
+ if (r.stale) pills.append(pill('skipped', 'stale?'));
414
+ b.append(pills, el('p', { class: 'note' }, r.stale ? 'Started over 6 h ago and not refreshed. ' : '', 'Refresh with ', el('code', {}, (sec && sec.pentest_hint) || 'prism-cli security pentest --status'), '.'));
415
+ s.append(b);
416
+ continue;
417
+ }
418
+ if (r.status === 'error') pills.append(pill('error', txt(r.job_status).toLowerCase()));
419
+ else {
420
+ const shown = RISKS.filter((k) => (counts[k] || 0) > 0);
421
+ if (!shown.length) pills.append(pill('ok', 'no findings'));
422
+ for (const k of shown) pills.append(riskPill(k, counts[k] + ' ' + k.toLowerCase()));
423
+ }
424
+ if (r.teardown_failed) pills.append(pill('error', 'teardown failed'));
425
+ b.append(pills);
426
+ if (r.reason) b.append(el('p', { class: 'note' }, txt(r.reason)));
427
+ if (r.teardown_failed) b.append(el('p', { class: 'note' }, 'The environment is still up (' + txt(r.teardown_failed.reason) + '). Retry with ', el('code', {}, 'prism-cli security pentest --cleanup'), '.'));
428
+ if (r.findings.length) {
429
+ const list = el('ul', { class: 'sec-findings' });
430
+ for (const f of r.findings) {
431
+ list.append(el('li', {},
432
+ riskPill(f.risk_level, txt(f.risk_level).toLowerCase()), ' ',
433
+ el('span', { class: 'sec-title' }, txt(f.title)),
434
+ f.confidence ? el('div', { class: 'fix' }, txt(f.confidence).toLowerCase() + ' confidence') : null));
435
+ }
436
+ b.append(list);
437
+ }
438
+ if (r.truncated) b.append(el('p', { class: 'note' }, 'showing the first ' + r.findings.length + '; run prism-cli security findings --pentest for all'));
439
+ s.append(b);
440
+ }
441
+ }
442
+
443
+ async function load() {
444
+ const main = document.getElementById('main'), meta = document.getElementById('meta');
445
+ meta.textContent = 'loading...';
446
+ let data;
447
+ try {
448
+ const res = await fetch('/api/data', { credentials: 'same-origin', cache: 'no-store' });
449
+ if (!res.ok) throw new Error('HTTP ' + res.status);
450
+ data = await res.json();
451
+ } catch (e) { meta.textContent = 'failed to load: ' + e.message; return; }
452
+ main.replaceChildren();
453
+ if (data.scope.error) main.append(el('section', { class: 'card wide' }, pill('error'), ' ', data.scope.error));
454
+ main.append(codewatchPanel(data), usagePanel(data.usage), setupPanel(data), securityPanel(data.security));
455
+ const c = data.overview.counts;
456
+ meta.textContent = c.repos + ' repos under ' + txt(data.scope.root_display) + ' · ' + c.codewatch + ' with codewatch' + (data.scope.truncated ? ' (scan capped)' : '') + ' · updated ' + when(data.generated_at);
457
+ }
458
+ document.getElementById('refresh').addEventListener('click', load);
459
+ load();
460
+ `;
461
+ export function renderPage(nonce) {
462
+ // The nonce is base64 generated server-side; nothing user-derived is interpolated here.
463
+ return `<!DOCTYPE html>
464
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
465
+ <title>PRISM dashboard</title><style nonce="${nonce}">${STYLE}</style></head>
466
+ <body><header><h1>PRISM dashboard</h1><button id="refresh" type="button">Refresh</button><span class="meta" id="meta"></span></header>
467
+ <main id="main"></main>
468
+ <script nonce="${nonce}">${SCRIPT}</script></body></html>`;
469
+ }
470
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCb,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Zd,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,wFAAwF;IACxF,OAAO;;8CAEqC,KAAK,KAAK,KAAK;;;iBAG5C,KAAK,KAAK,MAAM,yBAAyB,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The one data document behind both `prism-cli dashboard --json` and the
3
+ * web UI's /api/data, so the two can never disagree. The overview rows are
4
+ * sorted here, not in the page, so `--json` carries the same order.
5
+ */
6
+ import { type CodewatchPanel, type SecretScanning } from './codewatch.js';
7
+ import { type DiscoverOptions } from './repos.js';
8
+ import { type CheckItem, type CodewatchSetup, type MachineDeps, type SecuritySetup } from './setup.js';
9
+ import { type GlobalStatus } from '../codewatch/globalHooks.js';
10
+ export declare function tildeHome(p: string, home: string): string;
11
+ import { type CodeburnExport, type UsagePanel } from './usage.js';
12
+ import { type SecurityPanel } from './security.js';
13
+ export declare const PAYLOAD_SCHEMA = "prism.dashboard.v2";
14
+ export interface RepoPayload {
15
+ name: string;
16
+ path: string;
17
+ current: boolean;
18
+ hasPrism: boolean;
19
+ codewatch: CodewatchPanel;
20
+ setup: CodewatchSetup;
21
+ /** Security Agent, from the enable security marker. */
22
+ security: SecuritySetup;
23
+ }
24
+ /** One row of the setup table. */
25
+ export interface SetupRow {
26
+ name: string;
27
+ path: string;
28
+ current: boolean;
29
+ status: CodewatchSetup['status'];
30
+ reviewer: string | null;
31
+ detail: string;
32
+ fix: string;
33
+ /** Global hooks cover this repo. */
34
+ global: boolean;
35
+ /** Security Agent row for this repo. */
36
+ security: SecuritySetup;
37
+ /** Shown inside the collapsed "repos without codewatch" group: missing or opted out, and not the current repo. */
38
+ collapsed: boolean;
39
+ }
40
+ /** One row of the codewatch summary table. */
41
+ export interface CodewatchRow {
42
+ name: string;
43
+ path: string;
44
+ current: boolean;
45
+ high: number;
46
+ medium: number;
47
+ low: number;
48
+ lastRunAt: string | null;
49
+ /** Report id of the newest review, linked from the Last run cell. */
50
+ lastReviewId: string | null;
51
+ /** Reviewer of the last review, else the one that would run now. */
52
+ reviewer: string | null;
53
+ unscannedCommits: number;
54
+ /** Commits here were made while gitleaks was missing. */
55
+ gitleaksWarning: boolean;
56
+ }
57
+ /** A next step the page suggests. Read-only: the page shows the command, it never runs it. */
58
+ export interface Suggestion {
59
+ id: 'codewatch-global';
60
+ /** info: a suggestion. warn: a foreign global core.hooksPath is in the way. */
61
+ style: 'info' | 'warn';
62
+ command: string;
63
+ reason: string;
64
+ /** Repos without codewatch (status missing; opted-out repos are not counted). */
65
+ missing: number;
66
+ /** The foreign global core.hooksPath, for the warn variant. */
67
+ currentHooksPath: string | null;
68
+ }
69
+ /** Minimum missing repos before the global install is suggested; one repo is served by its own fix line. */
70
+ export declare const SUGGEST_GLOBAL_MIN_MISSING = 2;
71
+ export declare function globalSuggestion(g: GlobalStatus, missing: number): Suggestion | null;
72
+ export interface DashboardPayload {
73
+ /** repos: every repo found. codewatch: repos with both codewatch hooks installed. */
74
+ schema: typeof PAYLOAD_SCHEMA;
75
+ generated_at: string;
76
+ scope: {
77
+ root: string; /** root with the home directory shown as ~ */
78
+ root_display: string;
79
+ current_repo: string | null;
80
+ scan_ms: number;
81
+ truncated: boolean;
82
+ error: string | null;
83
+ };
84
+ repos: RepoPayload[];
85
+ overview: {
86
+ counts: {
87
+ repos: number;
88
+ codewatch: number;
89
+ };
90
+ setup: SetupRow[];
91
+ codewatch: CodewatchRow[];
92
+ };
93
+ machine: {
94
+ setup: CheckItem[];
95
+ secretScanning: SecretScanning;
96
+ globalHooks: GlobalStatus;
97
+ };
98
+ suggestions: Suggestion[];
99
+ usage: UsagePanel;
100
+ /** Cached Security Agent code review results (security-review), per repo. */
101
+ security: SecurityPanel;
102
+ }
103
+ export interface PayloadOptions extends DiscoverOptions {
104
+ trendRuns?: number;
105
+ exporter?: CodeburnExport;
106
+ machineDeps?: MachineDeps;
107
+ pathEnv?: string;
108
+ }
109
+ /** The current repo first, then installed, bypassed (warn), missing and opted-out repos, each by name. */
110
+ export declare function sortSetupRows(rows: SetupRow[]): SetupRow[];
111
+ /** Most high findings first, then medium, then low, then by name. */
112
+ export declare function sortCodewatchRows(rows: CodewatchRow[]): CodewatchRow[];
113
+ export declare function buildPayload(opts: PayloadOptions): Promise<DashboardPayload>;
114
+ //# sourceMappingURL=payload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../../src/dashboard/payload.ts"],"names":[],"mappings":"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"}