@preventive/triage 1.0.0-alpha.2 → 1.0.0-alpha.21
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/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-metadata.d.ts +10 -0
- package/common/bundle-metadata.js +177 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -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 +36 -0
- package/common/default-scan-models.ts +30 -0
- package/common/finding-id.js +47 -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 +121 -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 +33 -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 +81 -0
- package/out/client-managed.js +110 -0
- package/out/client-sync.js +16 -13
- package/out/graph.js +30 -4
- package/out/index.html +55 -8
- package/out/prism.js +2 -2
- package/out/stasis.svg +45 -0
- package/out/terminal.js +273 -39
- package/out/view.css +1 -1
- package/out/view.js +198 -62
- package/package.json +179 -55
- package/report/index.js +254 -0
- package/report/src/finding-id.js +80 -0
- package/report/src/finding.js +312 -0
- package/report/src/labels.js +33 -0
- package/report/src/md-structure.js +471 -0
- package/report/src/md-text.js +167 -0
- package/report/src/meta.js +76 -0
- package/report/src/parse-codex.js +147 -0
- package/report/src/parse-deepsec.js +197 -0
- package/report/src/parse-deepview-fields.js +375 -0
- package/report/src/parse-deepview-md.js +185 -0
- package/report/src/parse-md-id.js +137 -0
- package/report/src/parse-md.js +322 -0
- package/report/src/parse-piolium-id.js +79 -0
- package/report/src/parse-piolium-rows.js +131 -0
- package/report/src/parse-piolium-tokens.js +175 -0
- package/report/src/parse-piolium.js +400 -0
- package/report/src/security.js +63 -0
- package/report/src/utf8.js +21 -0
- package/report/src/write-md-finding.js +273 -0
- package/report/src/write-md.js +291 -0
- 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-log.ts +34 -0
- package/server-common/vercel-blob.ts +110 -0
- package/server-e2e/app.ts +485 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +9 -8
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +54 -39
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +7 -10
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +96 -135
- package/{server → server-e2e}/http.ts +110 -12
- package/{server → server-e2e}/hub.ts +44 -14
- 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 +6 -8
- package/{server → server-e2e}/objstore/blob-vercel.ts +49 -141
- package/{server → server-e2e}/objstore/blob.ts +24 -9
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +19 -20
- package/{server → server-e2e}/objstore/init.ts +53 -27
- 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 +110 -93
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +10 -14
- package/{server → server-e2e}/objstore/store.ts +98 -118
- package/{server → server-e2e}/objstore/tokens.ts +9 -12
- package/{server → server-e2e}/peer.ts +7 -9
- package/{server → server-e2e}/pubsub.ts +29 -36
- package/{server → server-e2e}/sign.ts +12 -14
- package/{server → server-e2e}/sse-server.ts +105 -73
- package/{server → server-e2e}/sse-session.ts +30 -16
- package/{server → server-e2e}/static.ts +36 -29
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/util.ts +9 -0
- package/{server → server-e2e}/ws-server.ts +29 -23
- package/server-managed/activity.ts +231 -0
- package/server-managed/avatar-store.ts +51 -0
- package/server-managed/blob-store.ts +66 -0
- package/server-managed/blob-vercel.ts +125 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-cache.ts +185 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-store.ts +28 -0
- package/server-managed/bundle-summary-cache.ts +97 -0
- package/server-managed/bundle.ts +39 -0
- package/server-managed/cache-storage.ts +40 -0
- package/server-managed/cli.js +13 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +144 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1336 -0
- package/server-managed/db-neon.ts +171 -0
- package/server-managed/db-schema.ts +203 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +109 -0
- package/server-managed/github-app.ts +332 -0
- package/server-managed/github-metadata.ts +65 -0
- package/server-managed/github-oauth.ts +215 -0
- package/server-managed/github-pulls.ts +115 -0
- package/server-managed/http-response.ts +18 -0
- package/server-managed/http.ts +2043 -0
- package/server-managed/import-triage.ts +48 -0
- package/server-managed/index.ts +135 -0
- package/server-managed/public-workspace.ts +150 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +4 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +154 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/session.ts +78 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/sql-postgres.ts +30 -0
- package/server-managed/sql.ts +61 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage.ts +34 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +128 -0
- package/server-managed/team-reports.ts +156 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/uploads.ts +47 -0
- package/server-managed/workspace-shares.ts +150 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -481
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -327
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// What one finding IS, read off the parsed object: the severity it
|
|
2
|
+
// displays under, the revalidation stamp it carries, the run it came
|
|
3
|
+
// from, the export marker to strip, how its text splits into a name and
|
|
4
|
+
// a body. The viewer and write-md.js beside it both read a parser's
|
|
5
|
+
// object through these; format.js re-exports every name. Pure — no DOM,
|
|
6
|
+
// no app state, nothing above `report/` — so a viewer switch arrives as
|
|
7
|
+
// an argument (`displayedSeverity`, `runMetaLine`).
|
|
8
|
+
|
|
9
|
+
import { fenceRanges, inFence } from './md-structure.js'
|
|
10
|
+
import { SOURCE_LABELS } from './labels.js'
|
|
11
|
+
|
|
12
|
+
// Severity ranking — higher = more severe. Two stacks: vulnerabilities
|
|
13
|
+
// (critical → low) over bug-class findings (high_bug → bug), with
|
|
14
|
+
// informational at the bottom; only DeepSec emits the bug tiers. A new
|
|
15
|
+
// tier goes here, and every other list keys off SEVERITIES below.
|
|
16
|
+
export const SEVERITY_ORDER = {
|
|
17
|
+
critical: 6, high: 5, medium: 4, low: 3,
|
|
18
|
+
high_bug: 2, bug: 1, informational: 0,
|
|
19
|
+
}
|
|
20
|
+
// Highest-to-lowest iteration order.
|
|
21
|
+
export const SEVERITIES = ['critical', 'high', 'medium', 'low', 'high_bug', 'bug', 'informational']
|
|
22
|
+
|
|
23
|
+
// ── Corrected severity ───────────────────────────────────────────────
|
|
24
|
+
// `correctedSeverity`, with a free-text `correctedSeverityReason`, is a
|
|
25
|
+
// report's own re-rating of the analyzer's `severity`, and PER-REPORT
|
|
26
|
+
// where `severity` is not: the id hashes `severity`, so a dedupe keeps
|
|
27
|
+
// each occurrence's corrected value on the survivor in
|
|
28
|
+
// `f._correctedByReport`.
|
|
29
|
+
//
|
|
30
|
+
// Every display, count and sort resolves severity through the helpers
|
|
31
|
+
// below; IDENTITY — the id fingerprint, dedupe keys — stays raw.
|
|
32
|
+
|
|
33
|
+
// Only a known tier counts: an unrecognised one would sort to rank 0 and
|
|
34
|
+
// render an uncolored badge, so the intrinsic severity stands instead.
|
|
35
|
+
function validCorrected(corrected) {
|
|
36
|
+
return corrected != null && corrected in SEVERITY_ORDER ? corrected : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The finding's own effective severity. It carries its own report's
|
|
40
|
+
// correction — it IS that report's finding — so no report key is needed;
|
|
41
|
+
// divergence across reports comes from correctedVariants.
|
|
42
|
+
export function effectiveSeverity(f) {
|
|
43
|
+
return validCorrected(f?.correctedSeverity) ?? f?.severity
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// True when the finding carries a valid correction that actually changes
|
|
47
|
+
// the tier — the trigger for the dual badge / reason affordance.
|
|
48
|
+
export function hasSeverityCorrection(f) {
|
|
49
|
+
const c = validCorrected(f?.correctedSeverity)
|
|
50
|
+
return c != null && c !== f?.severity
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Every display, count and sort site reads severity through this with the
|
|
54
|
+
// current lens (`state.severityMode`), not off `f.severity`.
|
|
55
|
+
export function displayedSeverity(f, mode) {
|
|
56
|
+
return mode === 'original' ? f?.severity : effectiveSeverity(f)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The per-report map of a deduped survivor, but only where the reports
|
|
60
|
+
// disagree — the "varies across reports" hint. null otherwise.
|
|
61
|
+
export function correctedVariants(f) {
|
|
62
|
+
const byReport = f?._correctedByReport
|
|
63
|
+
if (!byReport) return null
|
|
64
|
+
const tiers = new Set(Object.values(byReport).map((v) => v?.severity))
|
|
65
|
+
return tiers.size > 1 ? byReport : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Revalidation ─────────────────────────────────────────────────────
|
|
69
|
+
// A second pass over a finding. `revalidate` is what it concluded —
|
|
70
|
+
// `confirmed` (the finding stands), `partial` (part of it does),
|
|
71
|
+
// `refuted` (it doesn't), `unreachable` (nothing can reach the code),
|
|
72
|
+
// `unknown` (it couldn't tell) — with its reasoning in
|
|
73
|
+
// `revalidateVerdict` and, for a refutation, `revalidateRecommendation`.
|
|
74
|
+
// `revalidation` is the odd one out: the row that IS the pass rather
|
|
75
|
+
// than one it judged, carrying no verdict. `revalidateSource` names
|
|
76
|
+
// WHOSE pass said so, keyed like a report's `source`, and earns its keep
|
|
77
|
+
// when dedup carries a stamp onto another report's finding.
|
|
78
|
+
//
|
|
79
|
+
// One of these words or nothing — an unrecognised value is no stamp.
|
|
80
|
+
export const REVALIDATE_KINDS = ['revalidation', 'refuted', 'unreachable', 'confirmed', 'partial', 'unknown']
|
|
81
|
+
const REVALIDATE_SET = new Set(REVALIDATE_KINDS)
|
|
82
|
+
|
|
83
|
+
// The outcome as the DATA has it, '' when there is none; the viewer's
|
|
84
|
+
// `revalidateKind` is this behind the layer switch.
|
|
85
|
+
//
|
|
86
|
+
// Read as written: `Refuted ` is not this field's value. A drifted
|
|
87
|
+
// spelling is folded back where a person can edit one — a DOCUMENT
|
|
88
|
+
// (parse-deepview-fields.js readRevalidation) — and past that boundary
|
|
89
|
+
// case-folding it would say the opposite of what the analyzer wrote.
|
|
90
|
+
export function revalidateKindOf(f) {
|
|
91
|
+
return REVALIDATE_SET.has(f?.revalidate) ? f.revalidate : ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Does this finding belong to the APP layer — the code as the
|
|
95
|
+
// application runs it — rather than the source underneath? Another
|
|
96
|
+
// product's does by construction: its producer looked at the
|
|
97
|
+
// application, and a `source` in labels.js is exactly "not DeepView's own
|
|
98
|
+
// dump". DeepView's own are source-layer, except the row that IS its
|
|
99
|
+
// revalidation pass, which describes the run and not a line of code.
|
|
100
|
+
//
|
|
101
|
+
// `source` comes in separately because callers resolve it against the
|
|
102
|
+
// report's marker first. The sole definition of the split; readers take
|
|
103
|
+
// it off the finding as `isApp`, stamped once.
|
|
104
|
+
export function isAppFinding(f, source = f?.source) {
|
|
105
|
+
return Object.hasOwn(SOURCE_LABELS, source) || revalidateKindOf(f) === 'revalidation'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Security involvement is independent of the App/code layer and the displayed
|
|
109
|
+
// (possibly corrected) severity. External reports use their original severity,
|
|
110
|
+
// not the synthetic report-level `type: security` supplied by their parsers.
|
|
111
|
+
// `security: false` opts out of the analyzer default, not the other OR rules.
|
|
112
|
+
export function isSecurityFinding(f, source = f?._source ?? f?.source) {
|
|
113
|
+
const external = typeof source === 'string' && source !== '' && source !== 'deepview'
|
|
114
|
+
const analyzer = external ? source : f?.analyzer ?? f?.type ?? f?._analyzer
|
|
115
|
+
return f?.security === true
|
|
116
|
+
|| ((analyzer === 'security' || analyzer === 'dependencies') && f?.security !== false)
|
|
117
|
+
|| (external && ['critical', 'high', 'medium', 'low'].includes(f?.severity))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Run meta ─────────────────────────────────────────────────────────
|
|
121
|
+
export function prettyModel(model) {
|
|
122
|
+
if (!model) return model
|
|
123
|
+
return model.replace(/^[^/]+\//u, '').replace(/^claude-/u, '').replaceAll('-', ' ')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The run a finding came from, as one line — analyzer type, model,
|
|
127
|
+
// effort, exports mode, ` · `-joined, absent fields elided. Every
|
|
128
|
+
// surface that prints it (card, table row, bundle source rows, the
|
|
129
|
+
// markdown export) comes here, so the field list and separator can't
|
|
130
|
+
// drift apart.
|
|
131
|
+
//
|
|
132
|
+
// The revalidation row names itself right after the mode it ran in
|
|
133
|
+
// (`security · revalidate · opus 5 · …`) — that row only, not a verdict
|
|
134
|
+
// row the pass produced. `revalidation` is whether the layer is
|
|
135
|
+
// applied; off, the pass's name goes with the rest of it.
|
|
136
|
+
export function runMetaLine(f, revalidation = true) {
|
|
137
|
+
const pass = revalidation && revalidateKindOf(f) === 'revalidation' ? 'revalidate' : ''
|
|
138
|
+
return [f?.type, pass, prettyModel(f?.model), f?.effort, f?.exportsMode]
|
|
139
|
+
.filter(Boolean).join(' · ')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Export markers ───────────────────────────────────────────────────
|
|
143
|
+
// Isolate mode injects `[export: <name>]` markers and a `(<name>): `
|
|
144
|
+
// lead-in so a merged per-file response stays traceable to individual
|
|
145
|
+
// exports (src/isolate.js). Once post-process has lifted the name into
|
|
146
|
+
// `f.exportName` / `f.methodName`, the inline copy only repeats it, so a
|
|
147
|
+
// marker or prefix naming either field comes off; one naming something
|
|
148
|
+
// else is context ("this export affects <other>") and stays.
|
|
149
|
+
//
|
|
150
|
+
// In isolate mode a leading marker or `(<any>): [export: <any>] ` prefix
|
|
151
|
+
// comes off whatever it names, since that can be a sibling export. That
|
|
152
|
+
// pass runs FIRST, or the per-name strip could decapitate the prefix and
|
|
153
|
+
// leave the `(…): ` lead-in stranded.
|
|
154
|
+
export function stripExportMarker(text, f) {
|
|
155
|
+
if (!text) return text
|
|
156
|
+
let result = text
|
|
157
|
+
if (f?.exportsMode === 'isolate') {
|
|
158
|
+
// One level of nested `()` — `` (first branch of `bar()`) ``.
|
|
159
|
+
// Deeper nesting doesn't match at all, leaving the prose intact
|
|
160
|
+
// rather than over-stripping it.
|
|
161
|
+
result = result.replace(/^\((?:[^()]|\([^()]*\))*\): \[export:\s*\w+\] /u, '')
|
|
162
|
+
result = result.replace(/^\[export:\s*\w+\] /u, '')
|
|
163
|
+
}
|
|
164
|
+
const names = [f?.exportName, f?.methodName].filter(Boolean)
|
|
165
|
+
.map((name) => name.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
|
|
166
|
+
// Every marker first, then the prefixes. The two passes are ordered,
|
|
167
|
+
// not merely grouped: a `(Foo): ` prefix can sit BEHIND a marker
|
|
168
|
+
// naming the OTHER name — `[export: bar] (Foo): …` — and the prefix
|
|
169
|
+
// strip only ever looks at the front of the text, so a per-name pass
|
|
170
|
+
// that checked the prefix before the other name's marker came off
|
|
171
|
+
// would leave it there.
|
|
172
|
+
for (const name of names) result = result.replaceAll(new RegExp(`\\[export:\\s*${name}\\]\\s*`, 'gu'), '')
|
|
173
|
+
for (const name of names) result = result.replace(new RegExp(`^\\(\`?${name}\`?\\): `, 'u'), '')
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The export/method location as a label: `exportName.methodName` for a
|
|
178
|
+
// class export with a specific method, one name when they agree, '' when
|
|
179
|
+
// there is neither.
|
|
180
|
+
export function findingDisplayName(f) {
|
|
181
|
+
const e = f?.exportName
|
|
182
|
+
const m = f?.methodName
|
|
183
|
+
if (e && m && e !== m) return `${e}.${m}`
|
|
184
|
+
return e || m || ''
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Finding title ────────────────────────────────────────────────────
|
|
188
|
+
// What the finding is CALLED: the `title` field when a report has one,
|
|
189
|
+
// else the description's first line — where every markdown import puts
|
|
190
|
+
// the finding's heading, and where a JSON finding's one-paragraph
|
|
191
|
+
// description stands in for a name. Every surface that names a finding
|
|
192
|
+
// comes through here, so a report's own title reaches all of them or
|
|
193
|
+
// none. The export marker comes off first, being chrome.
|
|
194
|
+
export function firstLine(text) {
|
|
195
|
+
if (!text) return ''
|
|
196
|
+
for (const line of text.split('\n')) {
|
|
197
|
+
if (line.trim()) return line.trim()
|
|
198
|
+
}
|
|
199
|
+
return ''
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The `title` a finding carries, trimmed — '' when there is none a
|
|
203
|
+
// reader can use. The three readers below all open on it.
|
|
204
|
+
function ownTitle(f) {
|
|
205
|
+
return typeof f?.title === 'string' ? f.title.trim() : ''
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function findingTitle(f) {
|
|
209
|
+
return ownTitle(f) || firstLine(stripExportMarker(f?.description, f))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Title + body for a heading-over-body layout. With a `title` field the
|
|
213
|
+
// split is already made and the description is all body — except a first
|
|
214
|
+
// line REPEATING the title, which would stutter under it.
|
|
215
|
+
//
|
|
216
|
+
// Without one, the first line is the title, but only over a non-empty
|
|
217
|
+
// body: a single-line description stays whole rather than being bolded,
|
|
218
|
+
// and one that OPENS on a fence keeps its first line, since lifting it
|
|
219
|
+
// out would leave the block unopened and render its code as prose.
|
|
220
|
+
export function splitDescription(f) {
|
|
221
|
+
const text = stripExportMarker(f?.description, f) || ''
|
|
222
|
+
const own = ownTitle(f)
|
|
223
|
+
if (own) {
|
|
224
|
+
const body = text.trim()
|
|
225
|
+
const nl = body.indexOf('\n')
|
|
226
|
+
const first = (nl < 0 ? body : body.slice(0, nl)).trim()
|
|
227
|
+
if (first !== own) return { title: own, body }
|
|
228
|
+
return { title: own, body: nl < 0 ? '' : body.slice(nl + 1).replace(/^\s+/u, '') }
|
|
229
|
+
}
|
|
230
|
+
if (!text) return { title: '', body: '' }
|
|
231
|
+
const nl = text.indexOf('\n')
|
|
232
|
+
if (nl < 0) return { title: '', body: text }
|
|
233
|
+
// A fence opening at index 0 — the same reading codeBlockSegments
|
|
234
|
+
// gives it (format.js).
|
|
235
|
+
if (fenceRanges(text)[0]?.[0] === 0) return { title: '', body: text }
|
|
236
|
+
const body = text.slice(nl + 1).replace(/^\s+/u, '')
|
|
237
|
+
if (!body) return { title: '', body: text }
|
|
238
|
+
return { title: text.slice(0, nl).trim(), body }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The description with the name in front of it — the shape a format
|
|
242
|
+
// without a `title` field already writes, for the surfaces that show one
|
|
243
|
+
// blob per finding rather than a heading over a body.
|
|
244
|
+
export function titledDescription(f) {
|
|
245
|
+
const own = ownTitle(f)
|
|
246
|
+
if (!own) return stripExportMarker(f?.description, f) || ''
|
|
247
|
+
const { body } = splitDescription(f)
|
|
248
|
+
return body ? `${own}\n\n${body}` : own
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Description sections ─────────────────────────────────────────────
|
|
252
|
+
// A description body split into the sections the report wrote it in: a
|
|
253
|
+
// paragraph OPENING with `**Label:**` is one. Every parser emits its
|
|
254
|
+
// narrative fields that way, whatever the report called them, so keying
|
|
255
|
+
// off the markup rather than a list of label words picks all of them up.
|
|
256
|
+
//
|
|
257
|
+
// Everything else is prose, and consecutive unlabelled paragraphs stay
|
|
258
|
+
// in ONE block so their spacing survives. `[{ label, body }]` in
|
|
259
|
+
// document order, `label` null for prose.
|
|
260
|
+
const SECTION_LABEL_RE = /^\*\*([^*\n]+):\*\*[ \t]*/u
|
|
261
|
+
|
|
262
|
+
// Blank lines, but only OUTSIDE a fence. A snippet's own blank line
|
|
263
|
+
// would tear the block in two, leaving each half with one bare fence
|
|
264
|
+
// marker and neither rendering as code.
|
|
265
|
+
function paragraphs(text) {
|
|
266
|
+
const ranges = fenceRanges(text)
|
|
267
|
+
if (ranges.length === 0) return text.split(/\n{2,}/u)
|
|
268
|
+
const parts = []
|
|
269
|
+
let last = 0
|
|
270
|
+
for (const m of text.matchAll(/\n{2,}/gu)) {
|
|
271
|
+
if (inFence(ranges, m.index)) continue
|
|
272
|
+
parts.push(text.slice(last, m.index))
|
|
273
|
+
last = m.index + m[0].length
|
|
274
|
+
}
|
|
275
|
+
parts.push(text.slice(last))
|
|
276
|
+
return parts
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function descriptionSections(body) {
|
|
280
|
+
const sections = []
|
|
281
|
+
for (const para of paragraphs(body || '')) {
|
|
282
|
+
if (!para.trim()) continue
|
|
283
|
+
const m = SECTION_LABEL_RE.exec(para)
|
|
284
|
+
if (m) {
|
|
285
|
+
sections.push({ label: m[1].trim(), body: para.slice(m[0].length).trim() })
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
const open = sections.at(-1)
|
|
289
|
+
if (open && open.label === null) open.body += `\n\n${para}`
|
|
290
|
+
else sections.push({ label: null, body: para })
|
|
291
|
+
}
|
|
292
|
+
return sections
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Locations and evidence ───────────────────────────────────────────
|
|
296
|
+
// `file:line`, the line dropped when there isn't a finite one ('?' on
|
|
297
|
+
// imports that carry none). Takes a finding or an evidence row — both
|
|
298
|
+
// carry the pair and print it alike — and passes `line` through raw, so
|
|
299
|
+
// a range (`10-20`) survives whole.
|
|
300
|
+
export function locationLabel(x) {
|
|
301
|
+
return Number.isFinite(parseInt(x?.line, 10)) ? `${x.file}:${x.line}` : (x?.file ?? '')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// An evidence row's note: `text` is what parse-md.js writes from the
|
|
305
|
+
// lines under a reference, `observation` what a JSON report may call the
|
|
306
|
+
// same thing. No producer emits both, but a row carrying both reads as
|
|
307
|
+
// the observation over the text rather than one silently winning.
|
|
308
|
+
// Non-string values are ignored — this reads whatever JSON arrives.
|
|
309
|
+
export function evidenceNote(row) {
|
|
310
|
+
const str = (v) => (typeof v === 'string' ? v : '')
|
|
311
|
+
return `${str(row?.observation)}\n${str(row?.text)}`.trim()
|
|
312
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The words the markdown writer uses for the app's enumerations, one
|
|
2
|
+
// table per dimension. The viewer's prose surfaces read them too — the
|
|
3
|
+
// export dialog, the analyzer dropdown, the page header — so a filter
|
|
4
|
+
// the dialog lists and the header line in the file can't disagree.
|
|
5
|
+
|
|
6
|
+
export const SEVERITY_LABELS = {
|
|
7
|
+
critical: 'Critical', high: 'High', medium: 'Medium', low: 'Low',
|
|
8
|
+
high_bug: 'High bug', bug: 'Bug', informational: 'Informational',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TRIAGE_LABELS = {
|
|
12
|
+
inprogress: 'In progress', fixed: 'Fixed', invalid: 'Invalid',
|
|
13
|
+
deleted: 'Deleted', ignored: 'Ignored',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const COLOR_LABELS = { red: 'Red', blue: 'Blue', green: 'Green', gray: 'Gray', none: 'Unmarked' }
|
|
17
|
+
|
|
18
|
+
// The producer behind a report's `source` marker, one per format the
|
|
19
|
+
// library reads. The analyzer's own dump carries no marker and is
|
|
20
|
+
// described by its run meta instead.
|
|
21
|
+
export const SOURCE_LABELS = {
|
|
22
|
+
'claude-security': 'Claude Security',
|
|
23
|
+
'codex-security': 'Codex Security',
|
|
24
|
+
'deepsec': 'DeepSec',
|
|
25
|
+
'piolium': 'Piolium',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// An unknown tier prints as itself rather than vanishing: a report can
|
|
29
|
+
// invent one, and the reader is better served by the word than by a
|
|
30
|
+
// blank.
|
|
31
|
+
export function severityLabel(severity) {
|
|
32
|
+
return SEVERITY_LABELS[severity] ?? String(severity ?? '')
|
|
33
|
+
}
|