@preventive/triage 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (168) hide show
  1. package/api/reap.ts +17 -0
  2. package/cli.js +6 -0
  3. package/client/finding-link.js +305 -0
  4. package/client/linked-findings.d.ts +1 -0
  5. package/client/linked-findings.js +111 -0
  6. package/common/bundle-metadata.d.ts +10 -0
  7. package/common/bundle-metadata.js +177 -0
  8. package/common/bundle-reasons.d.ts +2 -0
  9. package/common/bundle-reasons.js +21 -0
  10. package/common/bundle-sources.d.ts +3 -0
  11. package/common/bundle-sources.js +284 -0
  12. package/common/bundle-stats.js +41 -0
  13. package/common/bundle-tabs.js +1 -0
  14. package/common/code-language.js +36 -0
  15. package/common/default-scan-models.ts +30 -0
  16. package/common/finding-id.js +47 -0
  17. package/common/github-pr.ts +56 -0
  18. package/common/managed/comments.ts +34 -0
  19. package/common/managed/permissions.ts +35 -0
  20. package/common/managed/report-content.ts +42 -0
  21. package/common/managed/report-filter.ts +108 -0
  22. package/common/managed/roles.ts +28 -0
  23. package/common/managed/routes.d.ts +2 -0
  24. package/common/managed/routes.js +121 -0
  25. package/common/managed/scan-models.ts +6 -0
  26. package/common/managed/triage.ts +83 -0
  27. package/common/save-error-reason.ts +20 -7
  28. package/common/scan-server.ts +13 -0
  29. package/common/server-info.ts +33 -0
  30. package/common/utf8.d.ts +3 -0
  31. package/common/utf8.js +45 -0
  32. package/out/brotli-fallback.js +3 -3
  33. package/out/client-managed-import.js +81 -0
  34. package/out/client-managed.js +110 -0
  35. package/out/client-sync.js +16 -13
  36. package/out/graph.js +30 -4
  37. package/out/index.html +55 -8
  38. package/out/prism.js +2 -2
  39. package/out/stasis.svg +45 -0
  40. package/out/terminal.js +273 -39
  41. package/out/view.css +1 -1
  42. package/out/view.js +198 -62
  43. package/package.json +179 -55
  44. package/report/index.js +254 -0
  45. package/report/src/finding-id.js +80 -0
  46. package/report/src/finding.js +312 -0
  47. package/report/src/labels.js +33 -0
  48. package/report/src/md-structure.js +471 -0
  49. package/report/src/md-text.js +167 -0
  50. package/report/src/meta.js +76 -0
  51. package/report/src/parse-codex.js +147 -0
  52. package/report/src/parse-deepsec.js +197 -0
  53. package/report/src/parse-deepview-fields.js +375 -0
  54. package/report/src/parse-deepview-md.js +185 -0
  55. package/report/src/parse-md-id.js +137 -0
  56. package/report/src/parse-md.js +322 -0
  57. package/report/src/parse-piolium-id.js +79 -0
  58. package/report/src/parse-piolium-rows.js +131 -0
  59. package/report/src/parse-piolium-tokens.js +175 -0
  60. package/report/src/parse-piolium.js +400 -0
  61. package/report/src/security.js +63 -0
  62. package/report/src/utf8.js +21 -0
  63. package/report/src/write-md-finding.js +273 -0
  64. package/report/src/write-md.js +291 -0
  65. package/server-common/database-config.ts +16 -0
  66. package/server-common/initialize.ts +18 -0
  67. package/server-common/npm-advisories.ts +101 -0
  68. package/{server → server-common}/origin.ts +5 -5
  69. package/server-common/reap.ts +48 -0
  70. package/server-common/scan-config.ts +19 -0
  71. package/server-common/standalone.ts +29 -0
  72. package/server-common/storage-log.ts +34 -0
  73. package/server-common/vercel-blob.ts +110 -0
  74. package/server-e2e/app.ts +485 -0
  75. package/{server → server-e2e}/auth.ts +5 -1
  76. package/{server → server-e2e}/bus-receiver.ts +9 -8
  77. package/{server → server-e2e}/cli.js +9 -4
  78. package/{server → server-e2e}/config.ts +54 -39
  79. package/{server → server-e2e}/db-neon.ts +2 -2
  80. package/{server → server-e2e}/db-revision-sql.ts +7 -10
  81. package/{server → server-e2e}/db-stmt.ts +2 -2
  82. package/{server → server-e2e}/db.ts +96 -135
  83. package/{server → server-e2e}/http.ts +110 -12
  84. package/{server → server-e2e}/hub.ts +44 -14
  85. package/server-e2e/index.ts +17 -0
  86. package/server-e2e/lifecycle.ts +95 -0
  87. package/{server → server-e2e}/neon-driver.ts +2 -2
  88. package/{server → server-e2e}/npm-proxy.ts +11 -144
  89. package/{server → server-e2e}/objstore/blob-fs.ts +6 -8
  90. package/{server → server-e2e}/objstore/blob-vercel.ts +49 -141
  91. package/{server → server-e2e}/objstore/blob.ts +24 -9
  92. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  93. package/{server → server-e2e}/objstore/handlers.ts +19 -20
  94. package/{server → server-e2e}/objstore/init.ts +53 -27
  95. package/{server → server-e2e}/objstore/reaper.ts +31 -11
  96. package/server-e2e/objstore/rest-deny.ts +28 -0
  97. package/server-e2e/objstore/rest-mint.ts +224 -0
  98. package/{server → server-e2e}/objstore/rest.ts +110 -93
  99. package/{server → server-e2e}/objstore/sign.ts +105 -0
  100. package/{server → server-e2e}/objstore/store-neon.ts +10 -14
  101. package/{server → server-e2e}/objstore/store.ts +98 -118
  102. package/{server → server-e2e}/objstore/tokens.ts +9 -12
  103. package/{server → server-e2e}/peer.ts +7 -9
  104. package/{server → server-e2e}/pubsub.ts +29 -36
  105. package/{server → server-e2e}/sign.ts +12 -14
  106. package/{server → server-e2e}/sse-server.ts +105 -73
  107. package/{server → server-e2e}/sse-session.ts +30 -16
  108. package/{server → server-e2e}/static.ts +36 -29
  109. package/server-e2e/sync-handlers.ts +408 -0
  110. package/{server → server-e2e}/util.ts +9 -0
  111. package/{server → server-e2e}/ws-server.ts +29 -23
  112. package/server-managed/activity.ts +231 -0
  113. package/server-managed/avatar-store.ts +51 -0
  114. package/server-managed/blob-store.ts +66 -0
  115. package/server-managed/blob-vercel.ts +125 -0
  116. package/server-managed/brotli.ts +10 -0
  117. package/server-managed/bundle-cache.ts +185 -0
  118. package/server-managed/bundle-catalog.ts +29 -0
  119. package/server-managed/bundle-store.ts +28 -0
  120. package/server-managed/bundle-summary-cache.ts +97 -0
  121. package/server-managed/bundle.ts +39 -0
  122. package/server-managed/cache-storage.ts +40 -0
  123. package/server-managed/cli.js +13 -0
  124. package/server-managed/combined.ts +46 -0
  125. package/server-managed/comments.ts +151 -0
  126. package/server-managed/config.ts +144 -0
  127. package/server-managed/content-access.ts +15 -0
  128. package/server-managed/crypto.ts +25 -0
  129. package/server-managed/db-methods.ts +1330 -0
  130. package/server-managed/db-neon.ts +171 -0
  131. package/server-managed/db-schema.ts +203 -0
  132. package/server-managed/db-table-names.ts +22 -0
  133. package/server-managed/db.ts +109 -0
  134. package/server-managed/github-app.ts +332 -0
  135. package/server-managed/github-metadata.ts +65 -0
  136. package/server-managed/github-oauth.ts +215 -0
  137. package/server-managed/github-pulls.ts +115 -0
  138. package/server-managed/http-response.ts +18 -0
  139. package/server-managed/http.ts +2043 -0
  140. package/server-managed/import-triage.ts +48 -0
  141. package/server-managed/index.ts +135 -0
  142. package/server-managed/public-workspace.ts +150 -0
  143. package/server-managed/repo-path.ts +21 -0
  144. package/server-managed/report-migration.ts +35 -0
  145. package/server-managed/report-query.ts +4 -0
  146. package/server-managed/report-response.ts +16 -0
  147. package/server-managed/report-sources.ts +154 -0
  148. package/server-managed/repository-discovery.ts +82 -0
  149. package/server-managed/repository-policy.ts +25 -0
  150. package/server-managed/session.ts +78 -0
  151. package/server-managed/slugs.ts +38 -0
  152. package/server-managed/sql-postgres.ts +30 -0
  153. package/server-managed/sql.ts +61 -0
  154. package/server-managed/static.ts +28 -0
  155. package/server-managed/storage.ts +34 -0
  156. package/server-managed/team-catalog.ts +7 -0
  157. package/server-managed/team-feed.ts +128 -0
  158. package/server-managed/team-reports.ts +156 -0
  159. package/server-managed/triage-response.ts +16 -0
  160. package/server-managed/uploads.ts +47 -0
  161. package/server-managed/workspace-shares.ts +150 -0
  162. package/server.ts +50 -0
  163. package/server/index.ts +0 -481
  164. package/server/lifecycle.ts +0 -204
  165. package/server/sync-handlers.ts +0 -327
  166. /package/{server → server-e2e}/config.example.json +0 -0
  167. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  168. /package/{server → server-e2e}/validation.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/triage",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Client & relay server for triaging of automated reports",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -29,56 +29,175 @@
29
29
  "#client/sync-host.js": "./client/sync-host.js"
30
30
  },
31
31
  "exports": {
32
- "./server": "./server/index.ts",
32
+ "./reap": "./api/reap.ts",
33
+ "./report": "./report/index.js",
34
+ "./server": "./server.ts",
35
+ "./server-e2e": "./server-e2e/index.ts",
33
36
  "./strip-types-loader": "./strip-types-loader.js",
34
37
  "./package.json": "./package.json"
35
38
  },
36
39
  "bin": {
37
- "triage-server": "server/cli.js"
40
+ "triage-server": "cli.js",
41
+ "triage-e2e-server": "server-e2e/cli.js",
42
+ "triage-managed-server": "server-managed/cli.js"
38
43
  },
39
44
  "files": [
40
- "server/auth.ts",
41
- "server/bus-receiver.ts",
42
- "server/cli.js",
43
- "server/config.ts",
44
- "server/db-neon.ts",
45
- "server/db-revision-sql.ts",
46
- "server/db-stmt.ts",
47
- "server/db.ts",
48
- "server/http.ts",
49
- "server/hub.ts",
50
- "server/index.ts",
51
- "server/lifecycle.ts",
52
- "server/neon-driver.ts",
53
- "server/npm-proxy.ts",
54
- "server/objstore/blob-fs.ts",
55
- "server/objstore/blob-vercel.ts",
56
- "server/objstore/blob.ts",
57
- "server/objstore/fs.ts",
58
- "server/objstore/handlers.ts",
59
- "server/objstore/init.ts",
60
- "server/objstore/reaper.ts",
61
- "server/objstore/rest.ts",
62
- "server/objstore/sign.ts",
63
- "server/objstore/store-neon.ts",
64
- "server/objstore/store.ts",
65
- "server/objstore/tokens.ts",
66
- "server/origin.ts",
67
- "server/peer.ts",
68
- "server/pubsub.ts",
69
- "server/sign.ts",
70
- "server/sse-server.ts",
71
- "server/sse-session.ts",
72
- "server/static.ts",
73
- "server/sync-handlers.ts",
74
- "server/util.ts",
75
- "server/validation.ts",
76
- "server/ws-server.ts",
77
- "server/config.example.json",
45
+ "api/reap.ts",
46
+ "cli.js",
47
+ "server.ts",
48
+ "server-e2e/app.ts",
49
+ "server-e2e/auth.ts",
50
+ "server-e2e/bus-receiver.ts",
51
+ "server-e2e/cli.js",
52
+ "server-e2e/config.ts",
53
+ "server-e2e/db-neon.ts",
54
+ "server-e2e/db-revision-sql.ts",
55
+ "server-e2e/db-stmt.ts",
56
+ "server-e2e/db.ts",
57
+ "server-e2e/http.ts",
58
+ "server-e2e/hub.ts",
59
+ "server-e2e/index.ts",
60
+ "server-e2e/lifecycle.ts",
61
+ "server-e2e/neon-driver.ts",
62
+ "server-e2e/npm-proxy.ts",
63
+ "server-e2e/objstore/blob-fs.ts",
64
+ "server-e2e/objstore/blob-vercel.ts",
65
+ "server-e2e/objstore/blob.ts",
66
+ "server-e2e/objstore/fetch-mint-guard.ts",
67
+ "server-e2e/objstore/fs.ts",
68
+ "server-e2e/objstore/handlers.ts",
69
+ "server-e2e/objstore/init.ts",
70
+ "server-e2e/objstore/reaper.ts",
71
+ "server-e2e/objstore/rest-deny.ts",
72
+ "server-e2e/objstore/rest-mint.ts",
73
+ "server-e2e/objstore/rest.ts",
74
+ "server-e2e/objstore/sign.ts",
75
+ "server-e2e/objstore/store-neon.ts",
76
+ "server-e2e/objstore/store.ts",
77
+ "server-e2e/objstore/tokens.ts",
78
+ "server-e2e/peer.ts",
79
+ "server-e2e/pubsub.ts",
80
+ "server-e2e/sign.ts",
81
+ "server-e2e/sse-server.ts",
82
+ "server-e2e/sse-session.ts",
83
+ "server-e2e/static.ts",
84
+ "server-e2e/sync-handlers.ts",
85
+ "server-e2e/util.ts",
86
+ "server-e2e/validation.ts",
87
+ "server-e2e/ws-server.ts",
88
+ "server-e2e/config.example.json",
89
+ "server-managed/activity.ts",
90
+ "server-managed/avatar-store.ts",
91
+ "server-managed/blob-store.ts",
92
+ "server-managed/blob-vercel.ts",
93
+ "server-managed/brotli.ts",
94
+ "server-managed/bundle-cache.ts",
95
+ "server-managed/bundle-catalog.ts",
96
+ "server-managed/bundle-store.ts",
97
+ "server-managed/bundle-summary-cache.ts",
98
+ "server-managed/bundle.ts",
99
+ "server-managed/cache-storage.ts",
100
+ "server-managed/cli.js",
101
+ "server-managed/combined.ts",
102
+ "server-managed/comments.ts",
103
+ "server-managed/config.ts",
104
+ "server-managed/content-access.ts",
105
+ "server-managed/crypto.ts",
106
+ "server-managed/db-methods.ts",
107
+ "server-managed/db-neon.ts",
108
+ "server-managed/db-schema.ts",
109
+ "server-managed/db-table-names.ts",
110
+ "server-managed/db.ts",
111
+ "server-managed/github-app.ts",
112
+ "server-managed/github-metadata.ts",
113
+ "server-managed/github-oauth.ts",
114
+ "server-managed/github-pulls.ts",
115
+ "server-managed/http-response.ts",
116
+ "server-managed/http.ts",
117
+ "server-managed/import-triage.ts",
118
+ "server-managed/index.ts",
119
+ "server-managed/public-workspace.ts",
120
+ "server-managed/repo-path.ts",
121
+ "server-managed/report-migration.ts",
122
+ "server-managed/report-query.ts",
123
+ "server-managed/report-response.ts",
124
+ "server-managed/report-sources.ts",
125
+ "server-managed/repository-discovery.ts",
126
+ "server-managed/repository-policy.ts",
127
+ "server-managed/session.ts",
128
+ "server-managed/slugs.ts",
129
+ "server-managed/sql-postgres.ts",
130
+ "server-managed/sql.ts",
131
+ "server-managed/static.ts",
132
+ "server-managed/storage.ts",
133
+ "server-managed/team-catalog.ts",
134
+ "server-managed/team-feed.ts",
135
+ "server-managed/team-reports.ts",
136
+ "server-managed/triage-response.ts",
137
+ "server-managed/uploads.ts",
138
+ "server-managed/workspace-shares.ts",
139
+ "server-common/database-config.ts",
140
+ "server-common/initialize.ts",
141
+ "server-common/reap.ts",
142
+ "server-common/npm-advisories.ts",
143
+ "server-common/origin.ts",
144
+ "server-common/standalone.ts",
145
+ "server-common/storage-log.ts",
146
+ "server-common/vercel-blob.ts",
147
+ "server-common/scan-config.ts",
148
+ "report/index.js",
149
+ "report/src/finding-id.js",
150
+ "report/src/finding.js",
151
+ "report/src/security.js",
152
+ "report/src/labels.js",
153
+ "report/src/md-structure.js",
154
+ "report/src/md-text.js",
155
+ "report/src/meta.js",
156
+ "report/src/parse-codex.js",
157
+ "report/src/parse-deepsec.js",
158
+ "report/src/parse-deepview-fields.js",
159
+ "report/src/parse-deepview-md.js",
160
+ "report/src/parse-md-id.js",
161
+ "report/src/parse-md.js",
162
+ "report/src/parse-piolium-id.js",
163
+ "report/src/parse-piolium-rows.js",
164
+ "report/src/parse-piolium-tokens.js",
165
+ "report/src/parse-piolium.js",
166
+ "report/src/utf8.js",
167
+ "report/src/write-md-finding.js",
168
+ "report/src/write-md.js",
169
+ "common/bundle-metadata.d.ts",
170
+ "common/bundle-metadata.js",
171
+ "common/bundle-reasons.d.ts",
172
+ "common/bundle-reasons.js",
173
+ "common/bundle-sources.d.ts",
174
+ "common/bundle-sources.js",
175
+ "common/bundle-stats.js",
176
+ "common/bundle-tabs.js",
177
+ "common/code-language.js",
178
+ "common/default-scan-models.ts",
179
+ "common/finding-id.js",
180
+ "common/github-pr.ts",
181
+ "common/managed/comments.ts",
182
+ "common/managed/permissions.ts",
183
+ "common/managed/report-content.ts",
184
+ "common/managed/report-filter.ts",
185
+ "common/managed/roles.ts",
186
+ "common/managed/routes.d.ts",
187
+ "common/managed/routes.js",
188
+ "common/managed/scan-models.ts",
189
+ "common/managed/triage.ts",
78
190
  "common/save-error-reason.ts",
191
+ "common/server-info.ts",
192
+ "common/scan-server.ts",
79
193
  "common/utf8.d.ts",
80
194
  "common/utf8.js",
195
+ "client/finding-link.js",
196
+ "client/linked-findings.d.ts",
197
+ "client/linked-findings.js",
81
198
  "out/brotli-fallback.js",
199
+ "out/client-managed.js",
200
+ "out/client-managed-import.js",
82
201
  "out/client-sync.js",
83
202
  "out/graph.js",
84
203
  "out/icon-maskable.svg",
@@ -86,6 +205,7 @@
86
205
  "out/index.html",
87
206
  "out/manifest.webmanifest",
88
207
  "out/prism.js",
208
+ "out/stasis.svg",
89
209
  "out/terminal.js",
90
210
  "out/view.css",
91
211
  "out/view.html",
@@ -94,35 +214,39 @@
94
214
  ],
95
215
  "scripts": {
96
216
  "lint": "node --run lint:js && node --run lint:css && node --run lint:ts",
97
- "lint:js": "oxlint -c .oxlintrc.json --ignore-pattern=ui/logs.js ./tests/ ./common/ ./server/ ./client/ ./ui/",
217
+ "lint:js": "oxlint -c .oxlintrc.json --ignore-pattern=ui/logs.js ./cli.js ./server.ts ./tests/ ./common/ ./report/ ./server-common/ ./server-e2e/ ./server-managed/ ./client/ ./ui/ ./api/",
98
218
  "lint:css": "stylelint 'ui/**/*.css'",
99
219
  "lint:ts": "tsc --noEmit",
100
220
  "prepack": "node --run build",
101
221
  "build": "node build.js build",
102
222
  "serve": "node build.js serve",
103
- "server": "node server/index.ts",
104
- "test": "node --js-base-64 --experimental-test-module-mocks --test ./tests/*.test.js"
223
+ "server": "node --run server-e2e --",
224
+ "server-e2e": "node server-e2e/index.ts",
225
+ "server-managed": "node server-managed/index.ts",
226
+ "server-managed-test": "node server-managed/test-server.ts",
227
+ "test": "node --js-base-64 --import=./tests/_resource-loader.js --experimental-test-module-mocks --test-timeout=120000 --test ./tests/*.test.js ./report/tests/*.test.js"
105
228
  },
106
229
  "devDependencies": {
107
- "@electric-sql/pglite": "^0.4.5",
108
- "@exodus/stasis": "^1.0.0-alpha.1",
109
- "@noble/ciphers": "^2.2.0",
110
- "@preventive/terminal": "^1.2.2",
230
+ "@electric-sql/pglite": "^0.5.5",
231
+ "@noble/ciphers": "^2.3.0",
232
+ "@preventive/diff": "^1.0.0",
233
+ "@preventive/terminal": "2.0.0-beta.1",
111
234
  "@rray/frontend": "^1.0.0",
112
- "@stylistic/stylelint-plugin": "^5.1.0",
113
- "@types/node": "^25.6.2",
235
+ "@stylistic/stylelint-plugin": "^5.3.0",
236
+ "@types/node": "^26.2.0",
114
237
  "@types/ws": "^8.18.1",
115
238
  "brotli": "^1.3.3",
116
- "esbuild": "^0.27.4",
117
- "lit": "^3.3.2",
239
+ "esbuild": "^0.28.2",
240
+ "lit": "^3.3.3",
118
241
  "minify-html-literals": "^1.3.5",
119
- "oxlint": "^1.63.0",
242
+ "oxlint": "^1.79.0",
120
243
  "prismjs": "^1.30.0",
121
- "stylelint": "^17.11.0",
244
+ "stylelint": "^17.14.1",
122
245
  "stylelint-config-recommended": "^18.0.0",
123
- "typescript": "^6.0.3"
246
+ "typescript": "^7.0.2"
124
247
  },
125
248
  "dependencies": {
249
+ "@exodus/stasis-core": "^1.0.0-beta.4",
126
250
  "ws": "^8.20.1"
127
251
  },
128
252
  "peerDependencies": {
@@ -0,0 +1,254 @@
1
+ // The report library — one door to every report format this project
2
+ // reads, and to the one it writes.
3
+ //
4
+ // A "report" is whatever an analyzer wrote: the JSON dump this
5
+ // project's own analyzer emits, or one of the shapes other tools
6
+ // produce — DeepSec and Piolium markdown, Claude Security markdown,
7
+ // Codex CSV — or the markdown document this library itself writes
8
+ // (write-md.js), read back by parse-deepview-md.js. The parsers beside
9
+ // this file each recognise exactly one of those and know nothing about
10
+ // each other; this module is the dispatch over them, so "which formats
11
+ // do we read, and in what order" is answered in one place instead of
12
+ // once per call site.
13
+ //
14
+ // import { loadFindings, writeMarkdown } from '../report/index.js'
15
+ // const report = await loadFindings(text)
16
+ // // → { format, data, findings: [ … with ids ] } | null
17
+ // const md = writeMarkdown({ title, groups: report.findings.map((f) => [f]) })
18
+ //
19
+ // Three entry points for reading, in rising order of how much they do:
20
+ //
21
+ // detectFormat — name the format, parse nothing further
22
+ // readReport — the parsed report, or the reason it isn't one
23
+ // loadFindings — parsed, flattened, and every finding carrying an id
24
+ //
25
+ // `analyzeReport` is `readReport` for a file list — entry count and
26
+ // producer — `reportEntries` is a report's entry list whichever of the
27
+ // two names it goes under (`findings` or `groups`), for a caller that
28
+ // has to keep the grouping rather than flatten it, and
29
+ // `backfillFindingIds` is the id step on its own, for a caller that
30
+ // has to interleave something with it.
31
+ //
32
+ // And one for writing: `writeMarkdown` takes findings — the parsers'
33
+ // own objects, grouped as the viewer groups them — with whatever the
34
+ // caller knows about the selection and the reader's annotations, and
35
+ // writes the markdown document the Download button saves (write-md.js
36
+ // for the document, write-md-finding.js for one finding, labels.js for
37
+ // the words). Both directions read a finding through finding.js, so
38
+ // what a parser produced and what the writer prints agree on what a
39
+ // finding IS — and the document is itself a report the readers above
40
+ // take (parse-deepview-md.js), so what was written can be loaded
41
+ // again, with the ids and fields it left with.
42
+ //
43
+ // Codex is the one format the content doesn't name: its export is a
44
+ // CSV, and a CSV is a container — one row per finding across several
45
+ // scans — rather than a report. `detectFormat` recognises it by the
46
+ // FILENAME (`.csv`) when given one; the readers are single-report and
47
+ // don't take it. A codex export goes through `parseCodexCsvToScans`,
48
+ // which splits it into one JSON-shaped report per scan, and each of
49
+ // those reads through the readers like any other JSON report.
50
+ //
51
+ // THIS FILE IS THE WHOLE SURFACE. The modules live in `src/` and the
52
+ // package exports one path — `@preventive/report`, this file — so
53
+ // everything a caller may hold is named here, in one list, and
54
+ // everything else is free to move, split or be renamed without
55
+ // breaking anyone. A consumer that wants `fenceRanges` or
56
+ // `findingTitle` imports it from here beside `loadFindings`; there is
57
+ // no second, deeper way in, inside this repo or out of it.
58
+ //
59
+ // That is a deliberate trade against the old shape, where every module
60
+ // was its own entry point. What it costs is the ability to reach past
61
+ // this list; what it buys is that the list IS the contract. Nothing is
62
+ // pulled in that a caller doesn't use: every module here is
63
+ // side-effect-free (`sideEffects: false` in package.json — the whole
64
+ // file is declarations), so a bundler drops what a caller never names,
65
+ // and `ui/view/format.js` still rides its lazily-loaded chunk.
66
+ //
67
+ // This directory is its own package (see package.json beside this
68
+ // file) and imports nothing outside itself: no DOM, no app state, no
69
+ // storage, nothing from the rest of the repo. Text in, data out — and
70
+ // data in, text out. That is what makes it reusable outside the
71
+ // viewer — the analyzer stamps its ids with the same `findingId` the
72
+ // viewer derives them with, so both sides agree on what a finding IS
73
+ // — and `node --test` in this directory runs its suite with nothing
74
+ // else installed — and nothing outside this directory reaches into it,
75
+ // tests included: every test of this library lives in `report/tests/`.
76
+ // Those come through the door like any other caller, except where they
77
+ // exercise an internal this file doesn't export; those name `../src/`,
78
+ // which is what they are testing.
79
+
80
+ import { parseDeepsecFindings } from './src/parse-deepsec.js'
81
+ import { parseDeepviewMarkdown } from './src/parse-deepview-md.js'
82
+ import { parseMarkdownFindings } from './src/parse-md.js'
83
+ import { parsePioliumFindings } from './src/parse-piolium.js'
84
+ import { deriveFindingId } from './src/finding-id.js'
85
+
86
+ // The rest of the surface, so a consumer needs one import: the codex
87
+ // splitter, the id helpers the analyzer shares with the viewer, and the
88
+ // run-meta projection a caller applies to the findings it loads.
89
+ export { parseCodexCsvToScans } from './src/parse-codex.js'
90
+ export { computeFileHash, deriveFindingId, findingId } from './src/finding-id.js'
91
+ export { META_FIELDS, inheritReportMeta, repoDirectory, reportRepoGithub } from './src/meta.js'
92
+ export { stampSecurityGroups } from './src/security.js'
93
+ // The writing side: the document writer, and the label tables it
94
+ // spells the app's enumerations with, for the viewer's surfaces that
95
+ // describe the same things in prose.
96
+ export { writeMarkdown } from './src/write-md.js'
97
+ export { COLOR_LABELS, SEVERITY_LABELS, SOURCE_LABELS, TRIAGE_LABELS, severityLabel } from './src/labels.js'
98
+
99
+ // Reading a finding: what a finding IS, asked of one. The card, the
100
+ // row, the filters and the writer all ask the same questions of the
101
+ // same object — which tier does this display under, what is its name,
102
+ // where does it sit, what did the pass say about it — and they ask
103
+ // them here, so a parser's output and every surface that renders it
104
+ // can't drift on the answers.
105
+ export {
106
+ REVALIDATE_KINDS, SEVERITIES, SEVERITY_ORDER, correctedVariants, descriptionSections,
107
+ displayedSeverity, effectiveSeverity, evidenceNote, findingDisplayName, findingTitle,
108
+ firstLine, hasSeverityCorrection, isAppFinding, isSecurityFinding, locationLabel, prettyModel, revalidateKindOf,
109
+ runMetaLine, splitDescription, stripExportMarker, titledDescription,
110
+ } from './src/finding.js'
111
+
112
+ // The structural-markdown helpers, for a caller rendering the prose a
113
+ // parser handed back: where the fences are (so a `## ` inside a
114
+ // snippet stays in the snippet), and the escapes markdown puts on a
115
+ // name. The viewer's own markdown rendering (ui/view/format.js,
116
+ // export-view-chunks.js) reads the document's shape with these rather
117
+ // than keeping a second, subtly different set.
118
+ export { fenceRanges, inFence, unescapeMd } from './src/md-structure.js'
119
+ export { isHttpUrl } from './src/md-text.js'
120
+
121
+ // The markdown chain, in dispatch order: tightest guard first. This
122
+ // library's own document opens on a marker line no other format has;
123
+ // DeepSec keys off `## SEVERITY (n)` and Piolium off its `# Security
124
+ // Audit Report` / `## Technical Findings Detail` headings, while
125
+ // parse-md accepts any `# Title` document — so it has to stay last or
126
+ // it would swallow the others. Each returns the standard `{ type,
127
+ // findings, … }` shape, or null when the text isn't its format.
128
+ //
129
+ // `format` is this library's name for the document's producer. For the
130
+ // three foreign markdown formats it matches the `source` marker the
131
+ // parser stamps on what it returns, which is what the viewer reads for
132
+ // its header label. 'deepview-md' is the exception that proves the
133
+ // rule: the document is this library's, but its findings came from
134
+ // whichever producer the document names, and THAT is the `source` it
135
+ // carries back (none for the analyzer's own runs). 'json' has no
136
+ // marker (the analyzer's own dump carries `type` instead).
137
+ const MARKDOWN_FORMATS = [
138
+ ['deepview-md', parseDeepviewMarkdown],
139
+ ['deepsec', parseDeepsecFindings],
140
+ ['piolium', parsePioliumFindings],
141
+ ['claude-security', parseMarkdownFindings],
142
+ ]
143
+
144
+ // A report's entries: `findings`, or `groups` for a pre-deduplicated
145
+ // dump. Each entry is one finding or a Finding[] group. Null when the
146
+ // document carries neither as an array — which is how a JSON file that
147
+ // isn't a report at all (or a report with a malformed list) is told
148
+ // apart from an empty one.
149
+ //
150
+ // Exported because a report is two shapes and only one of them is
151
+ // called `findings`: a caller reading `data.findings` alone sees an
152
+ // empty report wherever the entries are groups — which is every
153
+ // deduplicated dump, and every export of a view that merged a finding
154
+ // reported twice (parse-deepview-md.js writes `groups` for exactly
155
+ // those). `loadFindings` is the answer for a caller that wants the
156
+ // member findings; this is the one for a caller that has to keep the
157
+ // grouping, as the viewer's ingest does.
158
+ export function reportEntries(data) {
159
+ if (Array.isArray(data?.findings)) return data.findings
160
+ if (Array.isArray(data?.groups)) return data.groups
161
+ return null
162
+ }
163
+
164
+ // Which producer wrote `content` — 'json' / 'deepview-md' / 'deepsec' /
165
+ // 'piolium' / 'claude-security' / 'codex', or null when nothing
166
+ // recognises it.
167
+ //
168
+ // `filename` is optional and decides only codex: a `.csv` is a codex
169
+ // export, and the content is not consulted for it (nothing in a CSV's
170
+ // text says whose it is, and no other format arrives as one). Every
171
+ // other format is named from the content alone, so a `.md` holding a
172
+ // JSON dump is 'json'. Case-insensitive on the extension; strip any
173
+ // download-duplicate suffix (`report (1).csv`) before calling if the
174
+ // name can carry one after the extension.
175
+ export function detectFormat(content, filename) {
176
+ if (typeof filename === 'string' && /\.csv$/iu.test(filename)) return 'codex'
177
+ return readReport(content).format
178
+ }
179
+
180
+ // Parse `content` in whichever format it turns out to be. JSON first —
181
+ // the analyzer's native dump is the common case and the only format
182
+ // with a cheap, total test — then the markdown chain when `JSON.parse`
183
+ // throws. A JSON document counts as a report only when it carries a
184
+ // `findings` (or `groups`) array: anything else that parses is some
185
+ // other JSON file, not an empty report.
186
+ //
187
+ // Returns `{ data, format, reason }`: the parsed report and its
188
+ // format, or `data: null` with `reason` saying why in one sentence —
189
+ // which a caller reporting "this file isn't a report" can show as is.
190
+ // The usual cause is a truncated or malformed JSON dump rather than an
191
+ // unknown format, so the JSON error rides along in that sentence.
192
+ export function readReport(content) {
193
+ let jsonError
194
+ try {
195
+ const data = JSON.parse(content)
196
+ if (reportEntries(data)) return { data, format: 'json', reason: null }
197
+ return { data: null, format: null, reason: 'JSON, but not a report: no findings array' }
198
+ } catch (err) {
199
+ jsonError = err
200
+ }
201
+ for (const [format, parse] of MARKDOWN_FORMATS) {
202
+ const data = parse(content)
203
+ if (data) return { data, format, reason: null }
204
+ }
205
+ return {
206
+ data: null,
207
+ format: null,
208
+ reason: `Not JSON, and not a recognized markdown format. (JSON error: ${jsonError.message})`,
209
+ }
210
+ }
211
+
212
+ // How many entries `content` holds and who produced it, without
213
+ // flattening anything or deriving a single id — what a file list wants
214
+ // for a badge next to a name. `count` is ENTRIES, not findings: an
215
+ // entry is either one finding or a pre-deduplicated group of them, and
216
+ // the entry count is what a user sees as rows.
217
+ export function analyzeReport(content) {
218
+ const { data } = readReport(content)
219
+ if (!data) return { count: 0, recognized: false }
220
+ return { count: reportEntries(data).length, source: data.source, recognized: true }
221
+ }
222
+
223
+ // Entries → member findings. A group contributes its members; falsy
224
+ // and non-object entries (a malformed list's stray strings and nulls)
225
+ // are dropped rather than handed on as findings.
226
+ function flattenFindings(entries) {
227
+ return entries.flat().filter((f) => f && typeof f === 'object')
228
+ }
229
+
230
+ // Fill in `f.id` for any finding that lacks one, deriving it from the
231
+ // same fingerprint the analyzer stamps. Mutates in place. Findings
232
+ // whose id can't be derived (a host without crypto.subtle) are left
233
+ // untouched. Batched via Promise.all — sequential awaits would
234
+ // serialise hundreds of crypto.subtle.digest calls for no reason.
235
+ export async function backfillFindingIds(findings) {
236
+ const idLess = findings.filter((f) => !f.id)
237
+ if (idLess.length === 0) return
238
+ const derived = await Promise.all(idLess.map(deriveFindingId))
239
+ idLess.forEach((f, i) => { if (derived[i]) f.id = derived[i] })
240
+ }
241
+
242
+ // Recognise, flatten, and give every finding an id — the whole read
243
+ // path in one call. Returns `{ format, data, findings }`, or null when
244
+ // nothing recognises the text. The findings are the parser's own
245
+ // objects (not copies), so a caller that means to keep them can, and
246
+ // one projecting run meta onto them has `inheritReportMeta` and `data`
247
+ // to hand.
248
+ export async function loadFindings(content) {
249
+ const { data, format } = readReport(content)
250
+ if (!data) return null
251
+ const findings = flattenFindings(reportEntries(data))
252
+ await backfillFindingIds(findings)
253
+ return { format, data, findings }
254
+ }
@@ -0,0 +1,80 @@
1
+ // Finding ids, stamped by the analyzer onto its JSON output and filled
2
+ // in by the viewer for findings that arrive without one. Web Crypto is
3
+ // the common surface — `crypto.subtle` exists in modern Node and in
4
+ // secure browser contexts — so one implementation runs in both.
5
+ //
6
+ // Two reports from the same source give a finding the same id; an edit
7
+ // to its description or its source invalidates it.
8
+
9
+ import { encodeUtf8 } from './utf8.js'
10
+
11
+ function toHex(bytes) {
12
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
13
+ }
14
+
15
+ // The file-content hash the JSON output format uses. sha512 because the
16
+ // id below hashes a string that already includes it, so a collision here
17
+ // would propagate into an id collision. Padded base64 with the SRI-style
18
+ // tag. `btoa` rather than `Uint8Array#toBase64`, still flagged in Node.
19
+ export async function computeFileHash(source) {
20
+ const bytes = typeof source === 'string' ? encodeUtf8(source) : source
21
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-512', bytes))
22
+ return `sha512-${btoa(String.fromCodePoint(...digest))}`
23
+ }
24
+
25
+ // A fingerprint hashed into a v4-shaped UUID: derived, not random, but
26
+ // the shape lets a downstream tool treat it as an opaque id.
27
+ async function fingerprintToId(fingerprint) {
28
+ const bytes = encodeUtf8(JSON.stringify(fingerprint))
29
+ const digest = await crypto.subtle.digest('SHA-256', bytes)
30
+ const u = new Uint8Array(digest, 0, 16)
31
+ // version 4: 0100xxxx
32
+ u[6] = (u[6] & 0x0f) | 0x40
33
+ // variant 1: 10xxxxxx
34
+ u[8] = (u[8] & 0x3f) | 0x80
35
+ const hex = toHex(u)
36
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
37
+ }
38
+
39
+ // Stable per-finding id from the (severity, description, fileHash) triple
40
+ // the analyzer emits. fileHash being undefined is fine — JSON.stringify
41
+ // drops undefined keys, so a finding with no hash keys off the pair and
42
+ // re-runs over the same source yield the same ids.
43
+ export function findingId(severity, description, fileHash) {
44
+ return fingerprintToId({ severity, description, fileHash })
45
+ }
46
+
47
+ // An id derived from a finding, on the first discriminator it carries.
48
+ // null when `crypto.subtle` is unavailable (some `file://` setups), so
49
+ // the caller can fall back to a session-local id — the UI still works,
50
+ // without persistent triage on those findings.
51
+ //
52
+ // In order:
53
+ // - _idBasis — a FROZEN fingerprint a parser stamped, used verbatim;
54
+ // it exists so a change to the rendered description
55
+ // can't re-key stored triage (parse-md-id.js).
56
+ // - fileHash — as `findingId` above.
57
+ // - location — a markdown import's url, also stable.
58
+ // - file/line — last resort for a JSON finding with neither: not what
59
+ // the spec prescribes, but better than collapsing two
60
+ // unrelated findings onto one id.
61
+ export async function deriveFindingId(f) {
62
+ if (typeof crypto?.subtle?.digest !== 'function') return null
63
+ const fingerprint = fingerprintOf(f)
64
+ try {
65
+ return await fingerprintToId(fingerprint)
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ // The choice above as the object that gets hashed. Key order is part of
72
+ // the id — JSON.stringify keeps insertion order — so every shape lists
73
+ // severity and description first.
74
+ function fingerprintOf(f) {
75
+ if (f._idBasis) return f._idBasis
76
+ const { severity, description } = f
77
+ if (f.fileHash) return { severity, description, fileHash: f.fileHash }
78
+ if (f.location) return { severity, description, location: f.location }
79
+ return { severity, description, file: f.file, line: f.line }
80
+ }