@preventive/triage 1.0.0-alpha.23 → 1.0.0-alpha.26
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/common/server-info.ts +1 -0
- package/out/client-managed-import.js +41 -41
- package/out/client-managed.js +79 -63
- package/out/graph.js +6 -6
- package/out/prism.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +187 -186
- package/package.json +4 -1
- package/report/index.js +7 -1
- package/report/src/finding-id.js +1 -1
- package/report/src/finding.js +6 -4
- package/report/src/labels.js +3 -1
- package/report/src/parse-codex.js +3 -1
- package/report/src/parse-deepsec.js +3 -1
- package/report/src/parse-deepview-md.js +2 -1
- package/report/src/parse-md.js +3 -2
- package/report/src/parse-piolium-rows.js +7 -12
- package/report/src/parse-piolium-tokens.js +12 -2
- package/report/src/parse-piolium.js +7 -8
- package/report/src/severity.js +18 -0
- package/server-common/vercel-blob.ts +7 -18
- package/server-e2e/config.ts +4 -0
- package/server-e2e/objstore/blob-vercel.ts +4 -4
- package/server-managed/blob-vercel.ts +10 -8
- package/server-managed/config.ts +2 -1
- package/server-managed/db-methods.ts +41 -14
- package/server-managed/db-neon.ts +11 -4
- package/server-managed/http.ts +112 -90
- package/server-managed/import-triage.ts +4 -1
- package/server-managed/index.ts +30 -8
- package/server-managed/management.ts +86 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/public-workspace.ts +1 -11
- package/server-managed/report-query.ts +6 -0
- package/server-managed/repository-browser.ts +6 -1
- package/server-managed/sql.ts +5 -0
- package/server-managed/uploads.ts +28 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.26",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -119,7 +119,9 @@
|
|
|
119
119
|
"server-managed/http.ts",
|
|
120
120
|
"server-managed/import-triage.ts",
|
|
121
121
|
"server-managed/managed-issues.ts",
|
|
122
|
+
"server-managed/management.ts",
|
|
122
123
|
"server-managed/index.ts",
|
|
124
|
+
"server-managed/package-entry-points.ts",
|
|
123
125
|
"server-managed/public-workspace.ts",
|
|
124
126
|
"server-managed/repo-path.ts",
|
|
125
127
|
"server-managed/report-migration.ts",
|
|
@@ -153,6 +155,7 @@
|
|
|
153
155
|
"report/index.js",
|
|
154
156
|
"report/src/finding-id.js",
|
|
155
157
|
"report/src/finding.js",
|
|
158
|
+
"report/src/severity.js",
|
|
156
159
|
"report/src/security.js",
|
|
157
160
|
"report/src/labels.js",
|
|
158
161
|
"report/src/md-structure.js",
|
package/report/index.js
CHANGED
|
@@ -82,6 +82,7 @@ import { parseDeepviewMarkdown } from './src/parse-deepview-md.js'
|
|
|
82
82
|
import { parseMarkdownFindings } from './src/parse-md.js'
|
|
83
83
|
import { parsePioliumFindings } from './src/parse-piolium.js'
|
|
84
84
|
import { deriveFindingId } from './src/finding-id.js'
|
|
85
|
+
import { normalizeFindingSeverity } from './src/severity.js'
|
|
85
86
|
|
|
86
87
|
// The rest of the surface, so a consumer needs one import: the codex
|
|
87
88
|
// splitter, the id helpers the analyzer shares with the viewer, and the
|
|
@@ -193,7 +194,10 @@ export function readReport(content) {
|
|
|
193
194
|
let jsonError
|
|
194
195
|
try {
|
|
195
196
|
const data = JSON.parse(content)
|
|
196
|
-
if (reportEntries(data))
|
|
197
|
+
if (reportEntries(data)) {
|
|
198
|
+
for (const finding of flattenFindings(reportEntries(data))) normalizeFindingSeverity(finding)
|
|
199
|
+
return { data, format: 'json', reason: null }
|
|
200
|
+
}
|
|
197
201
|
return { data: null, format: null, reason: 'JSON, but not a report: no findings array' }
|
|
198
202
|
} catch (err) {
|
|
199
203
|
jsonError = err
|
|
@@ -233,6 +237,8 @@ function flattenFindings(entries) {
|
|
|
233
237
|
// untouched. Batched via Promise.all — sequential awaits would
|
|
234
238
|
// serialise hundreds of crypto.subtle.digest calls for no reason.
|
|
235
239
|
export async function backfillFindingIds(findings) {
|
|
240
|
+
// Managed responses may arrive as already-parsed objects.
|
|
241
|
+
for (const finding of findings) normalizeFindingSeverity(finding)
|
|
236
242
|
const idLess = findings.filter((f) => !f.id)
|
|
237
243
|
if (idLess.length === 0) return
|
|
238
244
|
const derived = await Promise.all(idLess.map(deriveFindingId))
|
package/report/src/finding-id.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function deriveFindingId(f) {
|
|
|
71
71
|
// The choice above as the object that gets hashed. Key order is part of
|
|
72
72
|
// the id — JSON.stringify keeps insertion order — so every shape lists
|
|
73
73
|
// severity and description first.
|
|
74
|
-
function fingerprintOf(f) {
|
|
74
|
+
export function fingerprintOf(f) {
|
|
75
75
|
if (f._idBasis) return f._idBasis
|
|
76
76
|
const { severity, description } = f
|
|
77
77
|
if (f.fileHash) return { severity, description, fileHash: f.fileHash }
|
package/report/src/finding.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { fenceRanges, inFence } from './md-structure.js'
|
|
10
10
|
import { SOURCE_LABELS } from './labels.js'
|
|
11
|
+
import { canonicalSeverity } from './severity.js'
|
|
11
12
|
|
|
12
13
|
// Severity ranking — higher = more severe. Two stacks: vulnerabilities
|
|
13
14
|
// (critical → low) over bug-class findings (high_bug → bug), with
|
|
@@ -33,6 +34,7 @@ export const SEVERITIES = ['critical', 'high', 'medium', 'low', 'high_bug', 'bug
|
|
|
33
34
|
// Only a known tier counts: an unrecognised one would sort to rank 0 and
|
|
34
35
|
// render an uncolored badge, so the intrinsic severity stands instead.
|
|
35
36
|
function validCorrected(corrected) {
|
|
37
|
+
corrected = canonicalSeverity(corrected)
|
|
36
38
|
return corrected != null && corrected in SEVERITY_ORDER ? corrected : null
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -40,20 +42,20 @@ function validCorrected(corrected) {
|
|
|
40
42
|
// correction — it IS that report's finding — so no report key is needed;
|
|
41
43
|
// divergence across reports comes from correctedVariants.
|
|
42
44
|
export function effectiveSeverity(f) {
|
|
43
|
-
return validCorrected(f?.correctedSeverity) ?? f?.severity
|
|
45
|
+
return validCorrected(f?.correctedSeverity) ?? canonicalSeverity(f?.severity)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// True when the finding carries a valid correction that actually changes
|
|
47
49
|
// the tier — the trigger for the dual badge / reason affordance.
|
|
48
50
|
export function hasSeverityCorrection(f) {
|
|
49
51
|
const c = validCorrected(f?.correctedSeverity)
|
|
50
|
-
return c != null && c !== f?.severity
|
|
52
|
+
return c != null && c !== canonicalSeverity(f?.severity)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// Every display, count and sort site reads severity through this with the
|
|
54
56
|
// current lens (`state.severityMode`), not off `f.severity`.
|
|
55
57
|
export function displayedSeverity(f, mode) {
|
|
56
|
-
return mode === 'original' ? f?.severity : effectiveSeverity(f)
|
|
58
|
+
return mode === 'original' ? canonicalSeverity(f?.severity) : effectiveSeverity(f)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// The per-report map of a deduped survivor, but only where the reports
|
|
@@ -61,7 +63,7 @@ export function displayedSeverity(f, mode) {
|
|
|
61
63
|
export function correctedVariants(f) {
|
|
62
64
|
const byReport = f?._correctedByReport
|
|
63
65
|
if (!byReport) return null
|
|
64
|
-
const tiers = new Set(Object.values(byReport).map((v) => v?.severity))
|
|
66
|
+
const tiers = new Set(Object.values(byReport).map((v) => canonicalSeverity(v?.severity)))
|
|
65
67
|
return tiers.size > 1 ? byReport : null
|
|
66
68
|
}
|
|
67
69
|
|
package/report/src/labels.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// export dialog, the analyzer dropdown, the page header — so a filter
|
|
4
4
|
// the dialog lists and the header line in the file can't disagree.
|
|
5
5
|
|
|
6
|
+
import { canonicalSeverity } from './severity.js'
|
|
7
|
+
|
|
6
8
|
export const SEVERITY_LABELS = {
|
|
7
9
|
critical: 'Critical', high: 'High', medium: 'Medium', low: 'Low',
|
|
8
10
|
high_bug: 'High bug', bug: 'Bug', informational: 'Informational',
|
|
@@ -29,5 +31,5 @@ export const SOURCE_LABELS = {
|
|
|
29
31
|
// invent one, and the reader is better served by the word than by a
|
|
30
32
|
// blank.
|
|
31
33
|
export function severityLabel(severity) {
|
|
32
|
-
return SEVERITY_LABELS[severity] ?? String(severity ?? '')
|
|
34
|
+
return SEVERITY_LABELS[canonicalSeverity(severity)] ?? String(severity ?? '')
|
|
33
35
|
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// Only the first path in `relevant_paths` becomes `f.file` (some
|
|
15
15
|
// findings list several, as `path1 | path2 | …`); the rest are dropped.
|
|
16
16
|
|
|
17
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
18
|
+
|
|
17
19
|
const REQUIRED_COLUMNS = [
|
|
18
20
|
'finding_url', 'repository', 'title', 'description', 'severity',
|
|
19
21
|
'configured_scan_id', 'relevant_paths',
|
|
@@ -143,5 +145,5 @@ function rowToFinding(r) {
|
|
|
143
145
|
if (r.commit_hash) finding.commitHash = r.commit_hash
|
|
144
146
|
if (r.detected_at) finding.detectedAt = r.detected_at
|
|
145
147
|
if (r.committed_at) finding.committedAt = r.committed_at
|
|
146
|
-
return finding
|
|
148
|
+
return normalizeFindingSeverity(finding)
|
|
147
149
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// `## SEVERITY (n)` header appears and the chain moves on.
|
|
31
31
|
|
|
32
32
|
import { normalizeNewlines, splitHeadingLine } from './md-structure.js'
|
|
33
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
33
34
|
|
|
34
35
|
// The `## SEVERITY (n)` header that marks a DeepSec document. Splitting
|
|
35
36
|
// on it with the tier captured interleaves tiers and content:
|
|
@@ -50,6 +51,7 @@ function mapSeverity(s) {
|
|
|
50
51
|
case 'HIGH_BUG': return 'high_bug'
|
|
51
52
|
case 'BUG': return 'bug'
|
|
52
53
|
case 'INFO': case 'INFORMATIONAL': return 'informational'
|
|
54
|
+
case 'INFORMATIVE': return 'informative'
|
|
53
55
|
default: return 'medium'
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -193,5 +195,5 @@ function parseBlock(block, severity) {
|
|
|
193
195
|
if (fields.reasoning) finding.revalidateVerdict = fields.reasoning.replaceAll('**', '')
|
|
194
196
|
}
|
|
195
197
|
if (fields.slug) finding.slug = fields.slug
|
|
196
|
-
return finding
|
|
198
|
+
return normalizeFindingSeverity(finding, severity === 'informative' ? 'medium' : severity)
|
|
197
199
|
}
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
import { locationLabel } from './finding.js'
|
|
57
57
|
import { H2_RE, H3_RE, H4_RE, normalizeNewlines, splitByHeading, splitLeading } from './md-structure.js'
|
|
58
58
|
import { applyFact, buildDescription, narrativeSplit, readAnalyzer, readEvidence, readProse, readRepository, splitFacts, splitSections, tierOf } from './parse-deepview-fields.js'
|
|
59
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
59
60
|
|
|
60
61
|
const MARKER_RE = /^<!--\s*DeepView findings export\b[^>]*-->/u
|
|
61
62
|
const CASE_RE = /^Case \d+ of \d+(?:\s|$)/u
|
|
@@ -136,7 +137,7 @@ function readCase(body, depth, entryTitle, tier) {
|
|
|
136
137
|
for (const [field, value] of fields) f[field] = value
|
|
137
138
|
const reason = sections.find((s) => s.label.toLowerCase() === 'severity correction')
|
|
138
139
|
if (reason?.body) f.correctedSeverityReason = readProse(reason.body)
|
|
139
|
-
return { finding: f, analyzer }
|
|
140
|
+
return { finding: normalizeFindingSeverity(f), analyzer }
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
// The producer and the run for the whole report: from the Analyzer facts
|
package/report/src/parse-md.js
CHANGED
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
// carry anything mandatory.
|
|
31
31
|
|
|
32
32
|
import { frozenIdBasis } from './parse-md-id.js'
|
|
33
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
33
34
|
import { LIST_MARKER_RE, findMdLink, normalizeNewlines, splitHeadingLine, unescapeMd } from './md-structure.js'
|
|
34
35
|
|
|
35
|
-
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'high_bug', 'bug', 'informational'])
|
|
36
|
+
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'high_bug', 'bug', 'informational', 'informative'])
|
|
36
37
|
|
|
37
38
|
export function parseMarkdownFindings(content) {
|
|
38
39
|
const text = normalizeNewlines(content).trim()
|
|
@@ -115,7 +116,7 @@ function parseBlock(block) {
|
|
|
115
116
|
const idBasis = frozenIdBasis(block)
|
|
116
117
|
if (idBasis) finding._idBasis = idBasis
|
|
117
118
|
|
|
118
|
-
return finding
|
|
119
|
+
return normalizeFindingSeverity(finding)
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// The sections half (before the first `---`) and the metadata half
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { cellValue, parseCodeRef, stripBold, tableObjects } from './md-structure.js'
|
|
7
7
|
import { frozenIdBasis } from './parse-piolium-id.js'
|
|
8
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
8
9
|
import {
|
|
9
|
-
idCell, leadingId, leadingLink, mapSeverity, severityFromId, slugTitle,
|
|
10
|
+
idCell, leadingId, leadingLink, mapSeverity, resolveSeverity, severityFromId, slugTitle,
|
|
10
11
|
} from './parse-piolium-tokens.js'
|
|
11
12
|
|
|
12
13
|
// Normalize a table-row object to the shared row shape used by the
|
|
@@ -33,10 +34,7 @@ export function indexRowOf(obj) {
|
|
|
33
34
|
// namespaced to read as an opaque token rather than a URL. A Location
|
|
34
35
|
// column, where a table has one, is parsed like any code reference.
|
|
35
36
|
export function fromIndexRow(row, sevFallback = '') {
|
|
36
|
-
const severity = mapSeverity(row.severity)
|
|
37
|
-
|| sevFallback
|
|
38
|
-
|| severityFromId(row.id)
|
|
39
|
-
|| 'medium'
|
|
37
|
+
const { severity, identitySeverity } = resolveSeverity(mapSeverity(row.severity), sevFallback, severityFromId(row.id))
|
|
40
38
|
const { file, line, locationLink } = parseCodeRef(row.location || '')
|
|
41
39
|
const finding = {
|
|
42
40
|
file: file || 'unknown',
|
|
@@ -49,12 +47,12 @@ export function fromIndexRow(row, sevFallback = '') {
|
|
|
49
47
|
// The fingerprint reads the same reference its own way — see
|
|
50
48
|
// parse-piolium-id.js.
|
|
51
49
|
finding._idBasis = frozenIdBasis({
|
|
52
|
-
severity, description: finding.description, ref: row.location || '', id: row.id,
|
|
50
|
+
severity: identitySeverity, description: finding.description, ref: row.location || '', id: row.id,
|
|
53
51
|
})
|
|
54
52
|
if (row.pocStatus) finding.pocStatus = row.pocStatus
|
|
55
53
|
if (row.status) finding.status = row.status
|
|
56
54
|
if (row.parent) finding.parent = row.parent
|
|
57
|
-
return finding
|
|
55
|
+
return normalizeFindingSeverity(finding)
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
// Findings rendered as a list: the mode outline asks for "links to
|
|
@@ -92,10 +90,7 @@ export function listFindings(body, sev, index) {
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
const row = index.get(id)
|
|
95
|
-
const severity = mapSeverity(row?.severity)
|
|
96
|
-
|| sev
|
|
97
|
-
|| severityFromId(id)
|
|
98
|
-
|| 'medium'
|
|
93
|
+
const { severity, identitySeverity } = resolveSeverity(mapSeverity(row?.severity), sev, severityFromId(id))
|
|
99
94
|
const finding = { file: 'unknown', line: '?', severity, description: stripBold(title) }
|
|
100
95
|
if (id) finding.location = `piolium:${id}`
|
|
101
96
|
else if (link) finding.location = link
|
|
@@ -103,7 +98,7 @@ export function listFindings(body, sev, index) {
|
|
|
103
98
|
if (row?.pocStatus) finding.pocStatus = row.pocStatus
|
|
104
99
|
if (row?.status) finding.status = row.status
|
|
105
100
|
if (row?.parent) finding.parent = row.parent
|
|
106
|
-
out.push({ id, finding })
|
|
101
|
+
out.push({ id, finding: normalizeFindingSeverity(finding, identitySeverity) })
|
|
107
102
|
}
|
|
108
103
|
return out
|
|
109
104
|
}
|
|
@@ -23,10 +23,20 @@ export function mapSeverity(s) {
|
|
|
23
23
|
case 'MEDIUM': return 'medium'
|
|
24
24
|
case 'LOW': return 'low'
|
|
25
25
|
case 'INFO': case 'INFORMATIONAL': return 'informational'
|
|
26
|
+
case 'INFORMATIVE': return 'informative'
|
|
26
27
|
default: return ''
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Keep the pre-alias fallback for the ID fingerprint, while displaying the
|
|
32
|
+
// newly recognized tier. Inputs are ordered from strongest to weakest source.
|
|
33
|
+
export function resolveSeverity(...tiers) {
|
|
34
|
+
return {
|
|
35
|
+
severity: tiers.find(Boolean) || 'medium',
|
|
36
|
+
identitySeverity: tiers.find(tier => tier && tier !== 'informative') || 'medium',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
// Final-report ids are severity-prefixed and sequential — `C1`, `H2`,
|
|
31
41
|
// `H-001` in lite consolidation — so the prefix is a second source for
|
|
32
42
|
// the tier. Only that exact scheme counts: a bare leading letter would
|
|
@@ -42,7 +52,7 @@ export function severityFromId(id) {
|
|
|
42
52
|
// findings. Anchored to the whole heading, so "High memory usage in
|
|
43
53
|
// parser" is never mistaken for one.
|
|
44
54
|
export function severityGroupOf(heading) {
|
|
45
|
-
const m = /^(critical|high|medium|low|informational|info)(?:[ -](?:severity|risk))?(?:[ -]findings?)?(?:\s*\(\d+\))?$/iu
|
|
55
|
+
const m = /^(critical|high|medium|low|informational|informative|info)(?:[ -](?:severity|risk))?(?:[ -]findings?)?(?:\s*\(\d+\))?$/iu
|
|
46
56
|
.exec((heading || '').trim())
|
|
47
57
|
return m ? mapSeverity(m[1]) : ''
|
|
48
58
|
}
|
|
@@ -51,7 +61,7 @@ export function severityGroupOf(heading) {
|
|
|
51
61
|
// `High: remaining` — for sections recognized by their CONTENT rather
|
|
52
62
|
// than the anchored severityGroupOf shape.
|
|
53
63
|
export function headerSeverity(header) {
|
|
54
|
-
const m = /^(critical|high|medium|low|informational|info)\b/iu.exec((header || '').trim())
|
|
64
|
+
const m = /^(critical|high|medium|low|informational|informative|info)\b/iu.exec((header || '').trim())
|
|
55
65
|
return m ? mapSeverity(m[1]) : ''
|
|
56
66
|
}
|
|
57
67
|
|
|
@@ -64,11 +64,12 @@
|
|
|
64
64
|
|
|
65
65
|
import { H2_RE, H3_RE, H4_RE, normalizeNewlines, parseCodeRef, parseLabelledFields, splitByHeading, splitLeading, tableObjects } from './md-structure.js'
|
|
66
66
|
import { frozenIdBasis } from './parse-piolium-id.js'
|
|
67
|
+
import { normalizeFindingSeverity } from './severity.js'
|
|
67
68
|
import { fromIndexRow, indexRowOf, listFindings, variantFindings } from './parse-piolium-rows.js'
|
|
68
69
|
import {
|
|
69
70
|
CODE_REF_FIELDS, codeRefOf, headerSeverity, idCell, idFromToken,
|
|
70
71
|
isVariantsHeading, mapSeverity, parseHeading, preambleMeta,
|
|
71
|
-
severityFromId, severityGroupOf,
|
|
72
|
+
resolveSeverity, severityFromId, severityGroupOf,
|
|
72
73
|
} from './parse-piolium-tokens.js'
|
|
73
74
|
|
|
74
75
|
|
|
@@ -295,11 +296,9 @@ function parseBlock(heading, body, index, groupSeverity = '') {
|
|
|
295
296
|
// Precedence: the block's own bullet, the index row, the enclosing
|
|
296
297
|
// group, the id's prefix, then medium — where an unrecognized tier
|
|
297
298
|
// stays visible rather than dropping out.
|
|
298
|
-
const severity =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|| severityFromId(id)
|
|
302
|
-
|| 'medium'
|
|
299
|
+
const { severity, identitySeverity } = resolveSeverity(
|
|
300
|
+
mapSeverity(fields.severity), mapSeverity(row?.severity), groupSeverity, severityFromId(id),
|
|
301
|
+
)
|
|
303
302
|
|
|
304
303
|
const ref = parseCodeRef(codeRefOf(fields))
|
|
305
304
|
// A `**Line:**` / `**Lines:**` bullet supplies the line when the
|
|
@@ -332,7 +331,7 @@ function parseBlock(heading, body, index, groupSeverity = '') {
|
|
|
332
331
|
// not. finding-id.js prefers `_idBasis` when deriving the uuid; read
|
|
333
332
|
// that module's header before touching either side.
|
|
334
333
|
finding._idBasis = frozenIdBasis({
|
|
335
|
-
severity, description: finding.description, ref: codeRefOf(fields), lineBullet, id,
|
|
334
|
+
severity: identitySeverity, description: finding.description, ref: codeRefOf(fields), lineBullet, id,
|
|
336
335
|
})
|
|
337
336
|
// Auxiliary provenance, kept as plain strings so an export can cite
|
|
338
337
|
// the audit's own artifacts — as parse-md.js keeps branch / status.
|
|
@@ -344,7 +343,7 @@ function parseBlock(heading, body, index, groupSeverity = '') {
|
|
|
344
343
|
const parent = row?.parent || (variantOf ? idCell(variantOf[1]) : '')
|
|
345
344
|
if (parent) finding.parent = parent
|
|
346
345
|
|
|
347
|
-
return { id, finding }
|
|
346
|
+
return { id, finding: normalizeFindingSeverity(finding) }
|
|
348
347
|
}
|
|
349
348
|
|
|
350
349
|
// The `## ` sections, keyed case-folded. A repeated header CONCATENATES
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fingerprintOf } from './finding-id.js'
|
|
2
|
+
|
|
3
|
+
// Informative is an input spelling of the existing Informational tier.
|
|
4
|
+
export function canonicalSeverity(value) {
|
|
5
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'informative' ? 'informational' : value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Normalize presentation without re-keying stored triage. Markdown parsers
|
|
9
|
+
// can supply the severity their previous fallback used for the fingerprint.
|
|
10
|
+
export function normalizeFindingSeverity(finding, identitySeverity = finding.severity) {
|
|
11
|
+
const severity = canonicalSeverity(finding.severity)
|
|
12
|
+
if (severity !== finding.severity) {
|
|
13
|
+
if (!finding.id && !finding._idBasis) finding._idBasis = fingerprintOf({ ...finding, severity: identitySeverity })
|
|
14
|
+
finding.severity = severity
|
|
15
|
+
}
|
|
16
|
+
if (finding.correctedSeverity !== undefined) finding.correctedSeverity = canonicalSeverity(finding.correctedSeverity)
|
|
17
|
+
return finding
|
|
18
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { Buffer } from 'node:buffer'
|
|
|
17
17
|
|
|
18
18
|
type VercelBlobBody = Readable | Buffer | string | Blob | ArrayBuffer | ReadableStream<Uint8Array>
|
|
19
19
|
export type VercelBlobSdk = {
|
|
20
|
+
BlobNotFoundError: new () => Error
|
|
20
21
|
put: (
|
|
21
22
|
pathname: string,
|
|
22
23
|
body: VercelBlobBody,
|
|
@@ -83,24 +84,12 @@ export type VercelBlobSdk = {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// Recognise "blob is gone" errors uniformly across read/write/delete
|
|
86
|
-
// paths so callers can treat them as success (delete) or
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Class-name check ONLY — the SDK's internal mapper turns every API
|
|
93
|
-
// `not_found` into BlobNotFoundError-by-name, so a bare-404 transport
|
|
94
|
-
// leak doesn't reach here. A broader `/does not exist|\b404\b/` match
|
|
95
|
-
// is DANGEROUS: it also matches BlobStoreNotFoundError's "This store
|
|
96
|
-
// does not exist.", so a config fault (revoked token, deleted store)
|
|
97
|
-
// would silently surface as every-blob-missing across reads/unlinks,
|
|
98
|
-
// masking the fatal misconfiguration. The tight name check lets
|
|
99
|
-
// BlobStoreNotFoundError / other classes propagate as real exceptions.
|
|
100
|
-
export function isNotFound(err: unknown): boolean {
|
|
101
|
-
if (err == null || typeof err !== 'object') return false
|
|
102
|
-
const name = (err as { name?: unknown }).name
|
|
103
|
-
return typeof name === 'string' && name === 'BlobNotFoundError'
|
|
87
|
+
// paths so callers can treat them as success (delete) or not-found (read).
|
|
88
|
+
// SDK error subclasses retain `.name === 'Error'`; identify the missing-blob
|
|
89
|
+
// class from the same lazily loaded SDK instance that performs the request.
|
|
90
|
+
// Store/configuration failures must propagate, never masquerade as cache misses.
|
|
91
|
+
export function isNotFound(err: unknown, sdk: Pick<VercelBlobSdk, 'BlobNotFoundError'>): boolean {
|
|
92
|
+
return err instanceof sdk.BlobNotFoundError
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
export async function loadVercelBlobSdk(): Promise<VercelBlobSdk> {
|
package/server-e2e/config.ts
CHANGED
|
@@ -54,6 +54,10 @@ Environment:
|
|
|
54
54
|
DEEPVIEW_SCAN_SERVER optional HTTP(S) scan-service URL; advertised
|
|
55
55
|
in /api/config and allowed by the UI CSP.
|
|
56
56
|
Unset by default (no external scan service).
|
|
57
|
+
GITHUB_NEW_ISSUE_LABELS optional comma-separated labels for the client's
|
|
58
|
+
prefilled GitHub issue form, in addition to
|
|
59
|
+
deepview and security (for security findings).
|
|
60
|
+
Advertised in /api/config and sync connections.
|
|
57
61
|
DB_PATH sqlite file (default: server-e2e/data/data.db);
|
|
58
62
|
ignored when an e2e database URL is set
|
|
59
63
|
DATABASE_URL shared Neon Postgres URL for enabled modes.
|
|
@@ -195,7 +195,7 @@ function buildStatStaging(sdk: VercelBlobSdk, token: string): BlobBackend['statS
|
|
|
195
195
|
const h = await sdk.head(stagingBlobPath(tag, stagingId), { token })
|
|
196
196
|
return h.size
|
|
197
197
|
} catch (err) {
|
|
198
|
-
if (isNotFound(err)) return null
|
|
198
|
+
if (isNotFound(err, sdk)) return null
|
|
199
199
|
throw err
|
|
200
200
|
}
|
|
201
201
|
}
|
|
@@ -266,7 +266,7 @@ function buildOpenLiveReader(sdk: VercelBlobSdk, token: string): BlobBackend['op
|
|
|
266
266
|
// same condition (blob-fs.ts maps ENOENT → `unavailable`), telling
|
|
267
267
|
// the client the resource is gone for good when it should refetch.
|
|
268
268
|
// See server-e2e/README.md's GET status table.
|
|
269
|
-
if (isNotFound(err)) return { ok: false, reason: 'unavailable', detail: 'vercel-get-not-found' }
|
|
269
|
+
if (isNotFound(err, sdk)) return { ok: false, reason: 'unavailable', detail: 'vercel-get-not-found' }
|
|
270
270
|
throw err
|
|
271
271
|
}
|
|
272
272
|
// SDK returned null (no blob) — same "bytes missing for a live row"
|
|
@@ -297,7 +297,7 @@ function buildOpenLiveReader(sdk: VercelBlobSdk, token: string): BlobBackend['op
|
|
|
297
297
|
// Blob vanished between get() and the head() size fallback (a
|
|
298
298
|
// racing reaper GC) — still the "live row present, bytes gone"
|
|
299
299
|
// transient, so `unavailable` (503), matching the get() path above.
|
|
300
|
-
if (isNotFound(headErr)) return { ok: false, reason: 'unavailable', detail: 'vercel-head-not-found' }
|
|
300
|
+
if (isNotFound(headErr, sdk)) return { ok: false, reason: 'unavailable', detail: 'vercel-head-not-found' }
|
|
301
301
|
throw headErr
|
|
302
302
|
}
|
|
303
303
|
}
|
|
@@ -328,7 +328,7 @@ function buildOpenLiveReader(sdk: VercelBlobSdk, token: string): BlobBackend['op
|
|
|
328
328
|
function buildUnlink(sdk: VercelBlobSdk, token: string, op: 'staging' | 'live', toPath: (tag: string, id: string) => string): (tag: string, id: string) => Promise<void> {
|
|
329
329
|
return async (tag, id) => {
|
|
330
330
|
try { await sdk.del(toPath(tag, id), { token }) } catch (err) {
|
|
331
|
-
if (isNotFound(err)) return
|
|
331
|
+
if (isNotFound(err, sdk)) return
|
|
332
332
|
// Don't propagate — the DB-side row drop has already
|
|
333
333
|
// committed by the time the caller reaches here, and the
|
|
334
334
|
// reaper picks up the stranded blob on its next sweep. Same
|
|
@@ -64,7 +64,7 @@ export async function openManagedVercelStorage(token: string, sdk?: VercelBlobSd
|
|
|
64
64
|
// Content-Length in that case so HTTP clients do not truncate the body.
|
|
65
65
|
const size = result.blob.size === 0 ? null : result.blob.size
|
|
66
66
|
return { size, stream: Readable.fromWeb(result.stream as Parameters<typeof Readable.fromWeb>[0]) }
|
|
67
|
-
} catch (err) { if (isNotFound(err)) return null; throw err }
|
|
67
|
+
} catch (err) { if (isNotFound(err, blobs)) return null; throw err }
|
|
68
68
|
}
|
|
69
69
|
async function get(path: string) {
|
|
70
70
|
const result = await open(path)
|
|
@@ -74,7 +74,7 @@ export async function openManagedVercelStorage(token: string, sdk?: VercelBlobSd
|
|
|
74
74
|
return Buffer.concat(parts)
|
|
75
75
|
}
|
|
76
76
|
async function remove(path: string) {
|
|
77
|
-
try { await blobs.del(path, { token }) } catch (err) { if (!isNotFound(err)) throw err }
|
|
77
|
+
try { await blobs.del(path, { token }) } catch (err) { if (!isNotFound(err, blobs)) throw err }
|
|
78
78
|
}
|
|
79
79
|
function store(name: string, suffix = ''): BlobStore {
|
|
80
80
|
const path = (id: string) => `.managed/${name}/${validate(id)}${suffix}`
|
|
@@ -90,7 +90,7 @@ export async function openManagedVercelStorage(token: string, sdk?: VercelBlobSd
|
|
|
90
90
|
const cacheStorage: BundleCacheStorage = {
|
|
91
91
|
async exists(id, file) {
|
|
92
92
|
try { await blobs.head(cachePath(id, file), { token }); return true }
|
|
93
|
-
catch (err) { if (isNotFound(err)) return false; throw err }
|
|
93
|
+
catch (err) { if (isNotFound(err, blobs)) return false; throw err }
|
|
94
94
|
},
|
|
95
95
|
put: (id, file, bytes) => put(cachePath(id, file), bytes),
|
|
96
96
|
async open(id, file) {
|
|
@@ -100,17 +100,19 @@ export async function openManagedVercelStorage(token: string, sdk?: VercelBlobSd
|
|
|
100
100
|
},
|
|
101
101
|
delete: id => deletePrefix(blobs, token, cachePath(id, ''), remove),
|
|
102
102
|
}
|
|
103
|
-
// Staging uploads are never published.
|
|
104
|
-
//
|
|
105
|
-
function reapUploads(now = Date.now()) {
|
|
106
|
-
|
|
103
|
+
// Staging uploads are never published. Automatic maintenance and explicit
|
|
104
|
+
// cleanup remove abandoned parts after a full day.
|
|
105
|
+
async function reapUploads(now = Date.now()) {
|
|
106
|
+
let removed = 0
|
|
107
|
+
await deletePrefix(blobs, token, '.managed/uploads/', async path => { await remove(path); removed++ },
|
|
107
108
|
blob => new Date(blob.uploadedAt ?? now).getTime() < now - 86_400_000)
|
|
109
|
+
return removed
|
|
108
110
|
}
|
|
109
111
|
const sourcesPath = (key: string) => `.managed/cache/report-sources/${validateCacheKey(key)}`
|
|
110
112
|
const reportSourcesStorage: CacheStorage = {
|
|
111
113
|
async exists(key) {
|
|
112
114
|
try { await blobs.head(sourcesPath(key), { token }); return true }
|
|
113
|
-
catch (err) { if (isNotFound(err)) return false; throw err }
|
|
115
|
+
catch (err) { if (isNotFound(err, blobs)) return false; throw err }
|
|
114
116
|
},
|
|
115
117
|
put: (key, bytes) => put(sourcesPath(key), bytes),
|
|
116
118
|
async open(key) {
|
package/server-managed/config.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// GitHub App can provide both flows, with repository grants at installation.
|
|
4
4
|
import { env } from 'node:process'
|
|
5
5
|
import { databaseUrls } from '../server-common/database-config.ts'
|
|
6
|
+
import { MAX_UPLOAD_BYTES } from './uploads.ts'
|
|
6
7
|
|
|
7
8
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
8
9
|
|
|
@@ -124,7 +125,7 @@ export function loadManagedConfig({ combined = false } = {}): ManagedConfig {
|
|
|
124
125
|
githubAppSlug: env['GITHUB_APP_SLUG'] ?? null,
|
|
125
126
|
githubNewIssueLabels: env['GITHUB_NEW_ISSUE_LABELS'] ?? '',
|
|
126
127
|
maxReportBytes: intEnv('MAX_REPORT_BYTES', 10_485_760, 1, 104_857_600),
|
|
127
|
-
maxBundleBytes: intEnv('MAX_BUNDLE_BYTES', 209_715_200, 1,
|
|
128
|
+
maxBundleBytes: intEnv('MAX_BUNDLE_BYTES', 209_715_200, 1, MAX_UPLOAD_BYTES),
|
|
128
129
|
triageHistoryLimit: intEnv('TRIAGE_HISTORY_LIMIT', 0, 0, 1_000_000_000),
|
|
129
130
|
}
|
|
130
131
|
}
|