@preventive/triage 1.0.0-alpha.3 → 1.0.0-alpha.31
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.
- package/LICENSE-APACHE +202 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +111 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-metadata.d.ts +10 -0
- package/common/bundle-metadata.js +182 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +5 -0
- package/common/bundle-source-links.js +82 -0
- package/common/bundle-sources.d.ts +3 -0
- package/common/bundle-sources.js +284 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +51 -0
- package/common/default-scan-models.ts +30 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/permissions.ts +35 -0
- package/common/managed/report-content.ts +42 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +124 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/triage.ts +83 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/out/brotli-fallback.js +3 -3
- package/out/client-managed-import.js +83 -0
- package/out/client-managed.js +169 -0
- package/out/client-sync.js +16 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +30 -4
- package/out/index.html +55 -8
- package/out/prism.js +2 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +273 -39
- package/out/view.css +1 -1
- package/out/view.js +259 -63
- package/package.json +205 -57
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +110 -12
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +11 -144
- package/{server → server-e2e}/objstore/blob-fs.ts +2 -2
- package/{server → server-e2e}/objstore/blob-vercel.ts +13 -117
- package/{server → server-e2e}/objstore/blob.ts +9 -2
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +31 -11
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +58 -33
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +1 -1
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/util.ts +9 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +231 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +53 -0
- package/server-managed/bundle-advisory-inventory.ts +75 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +54 -0
- package/server-managed/bundle-build.ts +168 -0
- package/server-managed/bundle-cache.ts +166 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +116 -0
- package/server-managed/bundle.ts +104 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +149 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1517 -0
- package/server-managed/db-neon.ts +277 -0
- package/server-managed/db-schema.ts +206 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +120 -0
- package/server-managed/github-app.ts +385 -0
- package/server-managed/github-errors.ts +49 -0
- package/server-managed/github-issue-oauth.ts +48 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +98 -0
- package/server-managed/github-oauth.ts +199 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +18 -0
- package/server-managed/http.ts +2350 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +175 -0
- package/server-managed/managed-issues.ts +47 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +86 -0
- package/server-managed/object-storage-disk.ts +144 -0
- package/server-managed/object-storage-vercel.ts +63 -0
- package/server-managed/object-storage.ts +71 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/public-workspace.ts +146 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +166 -0
- package/server-managed/repository-browser.ts +139 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +78 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +72 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +211 -0
- package/server-managed/storage-maintenance.ts +223 -0
- package/server-managed/storage-payload.ts +72 -0
- package/server-managed/storage-stores.ts +81 -0
- package/server-managed/storage.ts +58 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-reports.ts +219 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/uploads.ts +65 -0
- package/server-managed/workspace-shares.ts +162 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
package/LICENSE-APACHE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/api/reap.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// E2e-only Vercel deployment: no persistent relay is running in this function.
|
|
2
|
+
// The managed deployment routes /api/reap through its top-level app instead.
|
|
3
|
+
import { createReapHandler } from '../server-common/reap.ts'
|
|
4
|
+
import { databaseUrls } from '../server-common/database-config.ts'
|
|
5
|
+
import { reapOrphans } from '../server-e2e/objstore/reaper.ts'
|
|
6
|
+
import { openNeonObjstore } from '../server-e2e/objstore/store-neon.ts'
|
|
7
|
+
import { openVercelBlobBackend } from '../server-e2e/objstore/blob-vercel.ts'
|
|
8
|
+
|
|
9
|
+
const handler = createReapHandler({ e2e: async () => {
|
|
10
|
+
const url = databaseUrls().e2e
|
|
11
|
+
const token = process.env['BLOB_READ_WRITE_TOKEN']
|
|
12
|
+
if (!url || !token) throw new Error('E2e cron requires DATABASE_URL or E2E_DATABASE_URL, plus BLOB_READ_WRITE_TOKEN')
|
|
13
|
+
const blob = await openVercelBlobBackend({ token })
|
|
14
|
+
// Stateless Neon HTTP handle: nothing to close, no relay listener or timer.
|
|
15
|
+
await reapOrphans(await openNeonObjstore(url, blob))
|
|
16
|
+
} })
|
|
17
|
+
export default handler
|
package/cli.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Deep links to a single finding — pure logic side. Encodes the
|
|
2
|
+
// finding's id plus a "where does it live" hint into the URL hash
|
|
3
|
+
// (`#finding=<id>&v=<hints>`) and parses the same back on the receiving
|
|
4
|
+
// side, so "look at THIS issue" is a link you can paste into a chat
|
|
5
|
+
// instead of a screenshot plus directions.
|
|
6
|
+
//
|
|
7
|
+
// Sibling of `workspace-share-link.js`, and deliberately unlike it in
|
|
8
|
+
// one respect: the id here is NOT a secret. A share link carries the
|
|
9
|
+
// workspace's private key and is therefore encrypted; a finding id is
|
|
10
|
+
// meaningless without the report the recipient must already have, so it
|
|
11
|
+
// rides in the clear and the link stays legible in a chat client.
|
|
12
|
+
//
|
|
13
|
+
// The two LOCATION hints are a different matter. A report filename
|
|
14
|
+
// ("acme-corp-pentest-2024.json") and a workspace id are the sender's
|
|
15
|
+
// own metadata, and a URL is the most-forwarded, most-logged, most-
|
|
16
|
+
// screenshotted string there is — so they ship as 3-byte digests
|
|
17
|
+
// (4 base64url chars) rather than plaintext. That is a fingerprint, not
|
|
18
|
+
// a sealed box: 24 bits only narrows the field, and anyone holding a
|
|
19
|
+
// candidate name can confirm it by hashing. What it does buy is that
|
|
20
|
+
// the name itself never appears in the URL, and a recipient who does
|
|
21
|
+
// NOT hold the report learns nothing from the link but its length.
|
|
22
|
+
//
|
|
23
|
+
// Cheap on the receiving side too: matching a hint means hashing the
|
|
24
|
+
// names already on disk, not reading a single file (see
|
|
25
|
+
// `finding-locate.js`).
|
|
26
|
+
//
|
|
27
|
+
// The hint is a HINT, not an address. The receiver tries the named report
|
|
28
|
+
// or workspace, then a scan of everything stored locally (see
|
|
29
|
+
// `ui/view/finding-link-nav.js`) — so a link built in a workspace still
|
|
30
|
+
// lands for a recipient who only has the single report, and a stale
|
|
31
|
+
// hint costs a scan rather than the finding.
|
|
32
|
+
//
|
|
33
|
+
// Param names live in the same `&`-joined fragment namespace as
|
|
34
|
+
// `share=`, and the parser has the same shape as `extractShareEncoded`
|
|
35
|
+
// — a fragment can therefore only ever be one kind of link, and adding
|
|
36
|
+
// a fourth param later doesn't disturb either.
|
|
37
|
+
|
|
38
|
+
import { encodeUtf8 } from '../common/utf8.js'
|
|
39
|
+
import { managedRoutePath, parseManagedRoute } from '../common/managed/routes.js'
|
|
40
|
+
import { MAX_FINDING_ID_LENGTH, isLinkableFindingId } from '../common/finding-id.js'
|
|
41
|
+
export { isLinkableFindingId } from '../common/finding-id.js'
|
|
42
|
+
|
|
43
|
+
// ── location hints ───────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// 3 bytes → exactly 4 base64url characters, no padding. Sized to keep
|
|
46
|
+
// the fragment short while still being selective enough that a hint
|
|
47
|
+
// usually names one report out of a user's collection: with 16.7M
|
|
48
|
+
// values, a library of 100 reports has a ~0.03% chance of any collision
|
|
49
|
+
// at all, and a collision costs only a wrong first guess (the receiver
|
|
50
|
+
// re-checks that the finding is actually there, then scans).
|
|
51
|
+
const HINT_BYTES = 3
|
|
52
|
+
const HINT_CHARS = 4
|
|
53
|
+
const HINT_RE = /^[\w-]{4}$/u
|
|
54
|
+
|
|
55
|
+
// Both hints travel as ONE `v=` parameter: 6 bytes, 8 base64url
|
|
56
|
+
// characters, report half first. Workspace-only links use `w` followed
|
|
57
|
+
// by their 3-byte / 4-character hint. Purely a length decision — two named
|
|
58
|
+
// params spend 20 characters (`&report=aB3-&ws=x_9Z`) to carry 8
|
|
59
|
+
// characters of payload, and a link is something people paste into
|
|
60
|
+
// chat, tickets and commit messages.
|
|
61
|
+
//
|
|
62
|
+
// Concatenating the two 4-character tokens IS the base64 of the 6-byte
|
|
63
|
+
// buffer: 3 bytes is exactly one base64 group, so nothing carries
|
|
64
|
+
// across the halves and either framing produces identical text. That's
|
|
65
|
+
// what lets the packing be a string join and the unpacking a slice,
|
|
66
|
+
// with no encode / decode round trip.
|
|
67
|
+
//
|
|
68
|
+
// `AAAA` (3 zero bytes) marks a half that isn't known — a single-file
|
|
69
|
+
// view has no workspace, and a report whose hint hasn't been hashed yet
|
|
70
|
+
// omits its own. A genuine digest of `AAAA` therefore reads as absent,
|
|
71
|
+
// once per 16.7M values per half; the cost is that the receiver skips
|
|
72
|
+
// straight to the scan, which is where an unmatched hint lands anyway.
|
|
73
|
+
const COMBINED_RE = /^[\w-]{8}$/u
|
|
74
|
+
const NO_HINT = 'AAAA'
|
|
75
|
+
|
|
76
|
+
// Domain-separated per kind so the same string can't produce a hint
|
|
77
|
+
// that matches in the other namespace — a workspace whose id happened
|
|
78
|
+
// to equal a report's filename would otherwise cross-resolve. Same
|
|
79
|
+
// convention as `workspace-id.js`'s derivation domain.
|
|
80
|
+
const HINT_DOMAINS = {
|
|
81
|
+
report: 'deepview/finding-link/report/v1\n',
|
|
82
|
+
workspace: 'deepview/finding-link/workspace/v1\n',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `${kind}\0${value}` → token. Memoised because the LINK BUILDER must
|
|
86
|
+
// be synchronous: the Link button writes to the clipboard inside the
|
|
87
|
+
// click handler, and Safari drops the clipboard grant if the write
|
|
88
|
+
// happens after an await. Ingest primes this cache for every report it
|
|
89
|
+
// loads (and `switchToWorkspace` for the workspace), so by the time a
|
|
90
|
+
// card is on screen its hint is already here. A cold entry is not a
|
|
91
|
+
// failure — the link is simply built without that hint, and the
|
|
92
|
+
// receiver falls back to the scan.
|
|
93
|
+
const hintCache = new Map()
|
|
94
|
+
|
|
95
|
+
function hintKey(kind, value) { return `${kind}\0${value}` }
|
|
96
|
+
|
|
97
|
+
// Whether a value is a well-formed hint token, i.e. something
|
|
98
|
+
// `computeLinkHint` could have produced.
|
|
99
|
+
export function isLinkHint(value) {
|
|
100
|
+
return typeof value === 'string' && HINT_RE.test(value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Derive (and memoise) the hint token for a report filename or a
|
|
104
|
+
// workspace id. Resolves to null — never throws — when the kind is
|
|
105
|
+
// unknown, the value is empty, or `crypto.subtle` is unavailable (some
|
|
106
|
+
// `file://` setups): a missing hint degrades the link, it doesn't break
|
|
107
|
+
// it, so the caller shouldn't have to guard.
|
|
108
|
+
export async function computeLinkHint(kind, value) {
|
|
109
|
+
const domain = HINT_DOMAINS[kind]
|
|
110
|
+
if (!domain || typeof value !== 'string' || !value) return null
|
|
111
|
+
const key = hintKey(kind, value)
|
|
112
|
+
const cached = hintCache.get(key)
|
|
113
|
+
if (cached !== undefined) return cached
|
|
114
|
+
let token
|
|
115
|
+
try {
|
|
116
|
+
const digest = await crypto.subtle.digest('SHA-256', encodeUtf8(domain + value))
|
|
117
|
+
token = new Uint8Array(digest).slice(0, HINT_BYTES)
|
|
118
|
+
.toBase64({ alphabet: 'base64url', omitPadding: true })
|
|
119
|
+
} catch {
|
|
120
|
+
// Not cached: an environment without crypto.subtle stays broken,
|
|
121
|
+
// but a one-off failure (a lone surrogate in a filename tripping
|
|
122
|
+
// encodeUtf8) shouldn't poison the entry forever.
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
hintCache.set(key, token)
|
|
126
|
+
return token
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Synchronous companion — the memoised token, or null if it hasn't been
|
|
130
|
+
// computed yet. The link builder uses this; see `hintCache` above for
|
|
131
|
+
// why it can't just await.
|
|
132
|
+
export function knownLinkHint(kind, value) {
|
|
133
|
+
if (typeof value !== 'string' || !value) return null
|
|
134
|
+
return hintCache.get(hintKey(kind, value)) ?? null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Pack both hints into the `v=` value, or null when neither is known
|
|
138
|
+
// (in which case the param is omitted entirely rather than shipping
|
|
139
|
+
// eight characters of "nothing"). Throws on a non-empty value that
|
|
140
|
+
// isn't a token, so a caller passing a raw filename finds out
|
|
141
|
+
// immediately instead of shipping a hint-less link.
|
|
142
|
+
function packLinkHints(report, workspace) {
|
|
143
|
+
const halves = []
|
|
144
|
+
for (const [what, hint] of [['report', report], ['workspace', workspace]]) {
|
|
145
|
+
if (hint === null || hint === undefined || hint === '') { halves.push(NO_HINT); continue }
|
|
146
|
+
if (!isLinkHint(hint)) {
|
|
147
|
+
throw new TypeError(`encodeFindingRef: ${what} must be a link hint token, not a name`)
|
|
148
|
+
}
|
|
149
|
+
halves.push(hint)
|
|
150
|
+
}
|
|
151
|
+
if (halves.every((h) => h === NO_HINT)) return null
|
|
152
|
+
// Workspace → finding needs only the workspace half. Report links
|
|
153
|
+
// retain their original six-byte / eight-character encoding.
|
|
154
|
+
if (halves[0] === NO_HINT) return `w${halves[1]}`
|
|
155
|
+
return halves.join('')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Split a validated `v=` value back into its two halves, mapping the
|
|
159
|
+
// `AAAA` filler to null.
|
|
160
|
+
function unpackLinkHints(value) {
|
|
161
|
+
const halves = [value.slice(0, HINT_CHARS), value.slice(HINT_CHARS)]
|
|
162
|
+
.map((h) => (h === NO_HINT ? null : h))
|
|
163
|
+
return { report: halves[0], workspace: halves[1] }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── fragment codec ───────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
// Build the fragment body (no leading '#') for a finding reference.
|
|
169
|
+
// `report` / `workspace` are hint TOKENS (from `computeLinkHint` /
|
|
170
|
+
// `knownLinkHint`), not names; they leave as the single packed `v=`
|
|
171
|
+
// param: `w` + four characters for a workspace, eight for a report with its
|
|
172
|
+
// optional workspace. Omitted when neither is known.
|
|
173
|
+
//
|
|
174
|
+
// The id is percent-encoded: usually a uuid with nothing to escape, but
|
|
175
|
+
// the codex importer's finding-URL ids carry `/`, `:`, `?` and —
|
|
176
|
+
// decisively — `&` and `=`, which would otherwise split into phantom
|
|
177
|
+
// params on the way back. The packed hints need no encoding (base64url
|
|
178
|
+
// is already fragment-safe) and are emitted verbatim.
|
|
179
|
+
export function encodeFindingRef({ id, report, workspace } = {}) {
|
|
180
|
+
if (!isLinkableFindingId(id)) {
|
|
181
|
+
throw new TypeError('encodeFindingRef: a persistent finding id is required')
|
|
182
|
+
}
|
|
183
|
+
const parts = [`finding=${encodeURIComponent(id)}`]
|
|
184
|
+
const packed = packLinkHints(report, workspace)
|
|
185
|
+
if (packed) parts.push(`v=${packed}`)
|
|
186
|
+
return parts.join('&')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Full shareable URL for the current page origin + pathname, mirroring
|
|
190
|
+
// `buildShareUrl`. `location.search` is dropped for the same reason it
|
|
191
|
+
// is there: the target page takes no query params, so dragging the
|
|
192
|
+
// sender's current `?foo=bar` into the recipient's URL would be a
|
|
193
|
+
// surprising leak.
|
|
194
|
+
export function buildFindingUrl(ref, pathname) {
|
|
195
|
+
const encoded = encodeFindingRef(ref)
|
|
196
|
+
if (typeof location === 'undefined') return `${pathname ?? ''}#${encoded}`
|
|
197
|
+
return `${location.origin}${pathname ?? location.pathname}#${encoded}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Extract `{ id, report, workspace }` from a hash string (or
|
|
201
|
+
// `location.hash` when called with no argument), unpacking `v=` back
|
|
202
|
+
// into the two hint tokens. Returns null when the fragment carries no
|
|
203
|
+
// usable `finding=` param — a share link, an in-page anchor, an empty
|
|
204
|
+
// hash, or a payload that fails validation.
|
|
205
|
+
//
|
|
206
|
+
// HASH ONLY, matching `extractShareEncoded`. The id isn't secret, so the
|
|
207
|
+
// Referer-leak argument that motivates it there doesn't apply; the
|
|
208
|
+
// reason is uniformity — one place in the app reads deep links, and a
|
|
209
|
+
// caller that reached for `?finding=` would quietly bypass it.
|
|
210
|
+
//
|
|
211
|
+
// A malformed `v=` drops both hints rather than the whole ref: the id is
|
|
212
|
+
// what identifies the finding, and the receiver's scan can still land
|
|
213
|
+
// it. That also covers a link from an older build, whose separate
|
|
214
|
+
// `report=` / `ws=` params simply go unread. An un-decodable ID, on the
|
|
215
|
+
// other hand, fails the whole parse — there is nothing left to look up.
|
|
216
|
+
export function extractFindingRef(hash) {
|
|
217
|
+
const raw = typeof hash === 'string' ? hash : (typeof location === 'undefined' ? '' : location.hash)
|
|
218
|
+
if (!raw) return null
|
|
219
|
+
const stripped = raw.replace(/^#/u, '')
|
|
220
|
+
if (!stripped) return null
|
|
221
|
+
const found = { id: null, report: null, workspace: null }
|
|
222
|
+
for (const part of stripped.split('&')) {
|
|
223
|
+
const eq = part.indexOf('=')
|
|
224
|
+
if (eq < 0) continue
|
|
225
|
+
const key = part.slice(0, eq)
|
|
226
|
+
const rawValue = part.slice(eq + 1)
|
|
227
|
+
if (!rawValue) continue
|
|
228
|
+
// The packed hints are one workspace token or a token pair — validated
|
|
229
|
+
// verbatim rather than decoded, since base64url has nothing
|
|
230
|
+
// `encodeURIComponent` would have touched. Anything else is a link
|
|
231
|
+
// from another era or a mangled paste; drop it and let the scan
|
|
232
|
+
// take over.
|
|
233
|
+
if (key === 'v') {
|
|
234
|
+
if (rawValue.length === 5 && rawValue[0] === 'w' && isLinkHint(rawValue.slice(1))) {
|
|
235
|
+
const workspace = rawValue.slice(1)
|
|
236
|
+
Object.assign(found, { report: null, workspace: workspace === NO_HINT ? null : workspace })
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (!COMBINED_RE.test(rawValue)) continue
|
|
240
|
+
Object.assign(found, unpackLinkHints(rawValue))
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
if (key !== 'finding') continue
|
|
244
|
+
// Cheap length bound BEFORE decoding: percent-encoding expands at
|
|
245
|
+
// most 3:1, so nothing under this cap can decode to something over
|
|
246
|
+
// MAX_FINDING_ID_LENGTH, and a megabyte fragment never reaches
|
|
247
|
+
// decodeURIComponent. `isLinkableFindingId` re-checks the decoded
|
|
248
|
+
// length, so this is a guard, not the rule.
|
|
249
|
+
if (rawValue.length > MAX_FINDING_ID_LENGTH * 3) continue
|
|
250
|
+
let value
|
|
251
|
+
// decodeURIComponent throws URIError on a truncated / invalid escape
|
|
252
|
+
// (`%`, `%zz`) — common when a chat client mangles a pasted link.
|
|
253
|
+
try { value = decodeURIComponent(rawValue) } catch { continue }
|
|
254
|
+
if (isLinkableFindingId(value)) found.id = value
|
|
255
|
+
}
|
|
256
|
+
if (!found.id) return null
|
|
257
|
+
return found
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Recognise a finding deep link pasted into free text — the reverse of
|
|
261
|
+
// `buildFindingUrl`, used to linkify comments (see `parseCommentRefs` in
|
|
262
|
+
// `ui/view/format.js`). Returns `{ id, fragment, path }` with the E2E fragment
|
|
263
|
+
// re-emitted canonically and a managed destination URL, or null
|
|
264
|
+
// for anything that isn't one of OUR links.
|
|
265
|
+
//
|
|
266
|
+
// "Ours" means the CURRENT host, scheme included. A finding id resolves
|
|
267
|
+
// only against the reader's available reports, so a link to some other
|
|
268
|
+
// deployment couldn't be followed usefully even if it were offered — and
|
|
269
|
+
// a same-name look-alike on another host is exactly what a linkifier
|
|
270
|
+
// must not present as the real thing. Credentials are refused for the
|
|
271
|
+
// same reason (`https://user@triage.space/…` reads as ours but isn't
|
|
272
|
+
// something the app ever emits).
|
|
273
|
+
//
|
|
274
|
+
// Managed team/report paths are preserved to identify the intended copy.
|
|
275
|
+
// E2E entry links resolve from `/` in managed mode, without inheriting
|
|
276
|
+
// the current report. E2E consumers use just the fragment to stay on their
|
|
277
|
+
// current deployment, including `/index.html` and subpaths.
|
|
278
|
+
//
|
|
279
|
+
// The anti-mutation guard is the one from `githubRefToken`: `new URL`
|
|
280
|
+
// silently rewrites its input (resolving `..`, lower-casing, punycoding
|
|
281
|
+
// IDN homographs, dropping a default port), any of which can let a
|
|
282
|
+
// look-alike round up into a passing link. A candidate that isn't
|
|
283
|
+
// already canonical is refused rather than linkified. Every link this
|
|
284
|
+
// app emits round-trips unchanged, since `buildFindingUrl` builds from
|
|
285
|
+
// `location` itself.
|
|
286
|
+
export function parseFindingUrl(candidate, { managed = false } = {}) {
|
|
287
|
+
if (typeof location === 'undefined') return null
|
|
288
|
+
let u
|
|
289
|
+
try { u = new URL(candidate) } catch { return null }
|
|
290
|
+
if (u.href !== candidate) return null
|
|
291
|
+
if (u.protocol !== location.protocol) return null
|
|
292
|
+
if (u.host !== location.host) return null
|
|
293
|
+
if (u.username || u.password) return null
|
|
294
|
+
const route = parseManagedRoute(u)
|
|
295
|
+
if (route?.finding) {
|
|
296
|
+
return { id: route.finding.id, fragment: encodeFindingRef(route.finding), path: managedRoutePath(route) }
|
|
297
|
+
}
|
|
298
|
+
// Only E2E entry pages carry finding hashes. Unknown managed page URLs
|
|
299
|
+
// must not turn into a search for another accessible copy of the finding.
|
|
300
|
+
if (managed && route?.view !== 'home' && !/\/(?:index|view)\.html$/u.test(u.pathname)) return null
|
|
301
|
+
const ref = extractFindingRef(u.hash)
|
|
302
|
+
if (!ref) return null
|
|
303
|
+
const fragment = encodeFindingRef(ref)
|
|
304
|
+
return { id: ref.id, fragment, path: `/#${fragment}` }
|
|
305
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function parseLinkedFindings(content: string): { groups: string[][]; skipped: number } | null
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The "links" file — a small JSON document that says which findings
|
|
2
|
+
// are the SAME finding, reported twice:
|
|
3
|
+
//
|
|
4
|
+
// [[{"id":"a"},{"id":"b"}], [{"id":"c"},{"id":"d"},{"id":"e"}]]
|
|
5
|
+
//
|
|
6
|
+
// One inner array per link. Every finding named in it is a duplicate
|
|
7
|
+
// of every other one in that array, and the file says nothing else:
|
|
8
|
+
// no severity, no title, no file, no prose. That is the whole format.
|
|
9
|
+
//
|
|
10
|
+
// It is NOT a report, and this module deliberately sits outside
|
|
11
|
+
// `@preventive/report` because of it. A report carries findings; a links file
|
|
12
|
+
// carries only ids of findings that must already live in reports the
|
|
13
|
+
// reader holds. So it never reaches `ingestReport` / `state.reports`,
|
|
14
|
+
// it has its own view (`ui/view/render-links.js`) that points at the
|
|
15
|
+
// findings. It also supplies the "Duplicates:" row on finding cards
|
|
16
|
+
// and groups explicitly linked rows in workspace App view.
|
|
17
|
+
//
|
|
18
|
+
// Pure — no storage, no DOM, no app state. `linked-findings-index.js`
|
|
19
|
+
// beside this file is the OPFS-wide store built on top; everything
|
|
20
|
+
// here is text in, data out, so the recognition rules are testable on
|
|
21
|
+
// their own.
|
|
22
|
+
|
|
23
|
+
import { isLinkableFindingId } from './finding-link.js'
|
|
24
|
+
|
|
25
|
+
// The `source` marker a links file is filed under — the same slot a
|
|
26
|
+
// report's producer ('deepsec', 'piolium', …) occupies in the counts
|
|
27
|
+
// cache, so `getKind(name)` answers "what kind of file is this" for
|
|
28
|
+
// links and reports alike and the sidebar can bucket both from one
|
|
29
|
+
// lookup. No report format can collide with it: this value comes from
|
|
30
|
+
// here, never from a document's own content.
|
|
31
|
+
export const LINKS_KIND = 'links'
|
|
32
|
+
|
|
33
|
+
// Is this one entry of an inner array — `{"id": "…"}` — as the format
|
|
34
|
+
// spells it? Extra fields are allowed and ignored: an exporter that
|
|
35
|
+
// writes the title or the report alongside the id is writing a
|
|
36
|
+
// superset of this format, not a different one.
|
|
37
|
+
function entryId(entry) {
|
|
38
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null
|
|
39
|
+
const { id } = entry
|
|
40
|
+
return typeof id === 'string' && id.length > 0 ? id : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Recognise `content` as a links file and normalise it.
|
|
44
|
+
//
|
|
45
|
+
// Returns `{ groups, skipped }` — `groups` is one `string[]` of finding
|
|
46
|
+
// ids per link, and `skipped` counts ids the app could never follow (a
|
|
47
|
+
// session-local numeric id, a control character, something absurdly
|
|
48
|
+
// long — `isLinkableFindingId` draws the same line the per-finding Link
|
|
49
|
+
// button draws). Returns null when the text isn't this format at all.
|
|
50
|
+
//
|
|
51
|
+
// Recognition is on SHAPE, and it is strict, because this parser runs
|
|
52
|
+
// against every dropped file before the report readers get their turn:
|
|
53
|
+
// a top-level array, holding only arrays, holding only objects with a
|
|
54
|
+
// string `id`. Nothing else this app reads is a bare JSON array, so
|
|
55
|
+
// nothing else can be mistaken for one — and an empty top-level array
|
|
56
|
+
// is refused rather than claimed, since `[]` is every empty JSON list
|
|
57
|
+
// in the world, not distinctively a links file.
|
|
58
|
+
//
|
|
59
|
+
// Normalisation drops what carries no information: ids repeated inside
|
|
60
|
+
// one link, ids that can't be linked, and then any link left naming
|
|
61
|
+
// fewer than two findings — a "link" to a single finding links nothing.
|
|
62
|
+
// A file whose every link normalises away is still a links file; it
|
|
63
|
+
// just has no links, which its view says plainly.
|
|
64
|
+
export function parseLinkedFindings(content) {
|
|
65
|
+
let data
|
|
66
|
+
try { data = JSON.parse(content) } catch { return null }
|
|
67
|
+
if (!Array.isArray(data) || data.length === 0) return null
|
|
68
|
+
const groups = []
|
|
69
|
+
let skipped = 0
|
|
70
|
+
for (const raw of data) {
|
|
71
|
+
if (!Array.isArray(raw)) return null
|
|
72
|
+
const ids = new Set()
|
|
73
|
+
for (const entry of raw) {
|
|
74
|
+
const id = entryId(entry)
|
|
75
|
+
if (id === null) return null
|
|
76
|
+
if (isLinkableFindingId(id)) ids.add(id)
|
|
77
|
+
else skipped++
|
|
78
|
+
}
|
|
79
|
+
if (ids.size > 1) groups.push([...ids])
|
|
80
|
+
}
|
|
81
|
+
return { groups, skipped }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// How many findings the links in `groups` name, counted once each — a
|
|
85
|
+
// finding named by two links is one linked finding. What the sidebar
|
|
86
|
+
// badge and the view's header report alongside the link count.
|
|
87
|
+
export function countLinkedIds(groups) {
|
|
88
|
+
const ids = new Set()
|
|
89
|
+
for (const group of groups) for (const id of group) ids.add(id)
|
|
90
|
+
return ids.size
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fold links into `index` (a `Map<id, Set<id>>`): every id in a link
|
|
94
|
+
// gains every OTHER id in it as a duplicate. Called once per links
|
|
95
|
+
// file, so an id linked by two files ends up with the union of what
|
|
96
|
+
// both said about it.
|
|
97
|
+
//
|
|
98
|
+
// Union, NOT transitive closure. If one file links a↔b and another
|
|
99
|
+
// links b↔c, then b has two duplicates and a has one — a and c were
|
|
100
|
+
// never said to be the same finding, and inferring it would put a
|
|
101
|
+
// claim in the reader's card that no file they dropped ever made.
|
|
102
|
+
export function collectDuplicates(groups, index) {
|
|
103
|
+
for (const group of groups) {
|
|
104
|
+
for (const id of group) {
|
|
105
|
+
let set = index.get(id)
|
|
106
|
+
if (!set) index.set(id, set = new Set())
|
|
107
|
+
for (const other of group) if (other !== id) set.add(other)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return index
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function bundleCommitHash(value: unknown): string | null
|