@preventive/triage 1.0.0-alpha.27 → 1.0.0-alpha.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/client/linked-findings.js +1 -1
  3. package/common/bundle-metadata.js +12 -10
  4. package/common/code-language.js +17 -2
  5. package/common/default-scan-models.ts +3 -3
  6. package/common/finding-id.js +1 -1
  7. package/common/managed/report-content.ts +1 -1
  8. package/common/managed/report-filter.ts +1 -1
  9. package/out/client-managed-import.js +16 -16
  10. package/out/client-managed.js +93 -68
  11. package/out/graph.js +11 -11
  12. package/out/index.html +1 -1
  13. package/out/prism.js +2 -2
  14. package/out/stasis.svg +0 -25
  15. package/out/terminal.js +163 -163
  16. package/out/view.css +1 -1
  17. package/out/view.js +223 -190
  18. package/package.json +18 -28
  19. package/server-e2e/static.ts +3 -4
  20. package/server-managed/bundle-advisories.ts +53 -0
  21. package/server-managed/bundle-advisory-inventory.ts +75 -0
  22. package/server-managed/bundle-build-leases.ts +41 -0
  23. package/server-managed/bundle-build-worker.js +44 -0
  24. package/server-managed/bundle-build.ts +141 -0
  25. package/server-managed/bundle-cache.ts +22 -13
  26. package/server-managed/bundle-summary-cache.ts +27 -9
  27. package/server-managed/bundle.ts +66 -1
  28. package/server-managed/cache-storage.ts +4 -0
  29. package/server-managed/content-access.ts +1 -1
  30. package/server-managed/db-methods.ts +122 -20
  31. package/server-managed/db-neon.ts +121 -32
  32. package/server-managed/db-schema.ts +2 -0
  33. package/server-managed/db.ts +5 -1
  34. package/server-managed/github-app.ts +10 -15
  35. package/server-managed/github-errors.ts +21 -0
  36. package/server-managed/github-issues.ts +1 -1
  37. package/server-managed/github-metadata.ts +34 -1
  38. package/server-managed/github-pulls.ts +142 -30
  39. package/server-managed/http.ts +244 -131
  40. package/server-managed/index.ts +25 -9
  41. package/server-managed/management-catalog.ts +45 -0
  42. package/server-managed/object-storage-vercel.ts +1 -1
  43. package/server-managed/public-workspace.ts +20 -14
  44. package/server-managed/report-migration.ts +1 -1
  45. package/server-managed/report-sources.ts +14 -4
  46. package/server-managed/repository-browser.ts +3 -0
  47. package/server-managed/repository-discovery.ts +1 -1
  48. package/server-managed/revisions.ts +35 -0
  49. package/server-managed/sql-postgres.ts +1 -0
  50. package/server-managed/sql.ts +14 -3
  51. package/server-managed/storage-db.ts +12 -5
  52. package/server-managed/storage-maintenance.ts +37 -22
  53. package/server-managed/storage-payload.ts +16 -4
  54. package/server-managed/storage-stores.ts +2 -2
  55. package/server-managed/storage.ts +6 -4
  56. package/server-managed/team-feed.ts +50 -5
  57. package/server-managed/team-reports.ts +85 -22
  58. package/server-managed/workspace-shares.ts +15 -3
  59. package/report/index.js +0 -260
  60. package/report/src/finding-id.js +0 -80
  61. package/report/src/finding.js +0 -314
  62. package/report/src/labels.js +0 -35
  63. package/report/src/md-structure.js +0 -471
  64. package/report/src/md-text.js +0 -167
  65. package/report/src/meta.js +0 -76
  66. package/report/src/parse-codex.js +0 -149
  67. package/report/src/parse-deepsec.js +0 -199
  68. package/report/src/parse-deepview-fields.js +0 -375
  69. package/report/src/parse-deepview-md.js +0 -186
  70. package/report/src/parse-md-id.js +0 -137
  71. package/report/src/parse-md.js +0 -323
  72. package/report/src/parse-piolium-id.js +0 -79
  73. package/report/src/parse-piolium-rows.js +0 -126
  74. package/report/src/parse-piolium-tokens.js +0 -185
  75. package/report/src/parse-piolium.js +0 -399
  76. package/report/src/security.js +0 -63
  77. package/report/src/severity.js +0 -18
  78. package/report/src/utf8.js +0 -21
  79. package/report/src/write-md-finding.js +0 -273
  80. package/report/src/write-md.js +0 -291
  81. /package/{LICENSE → LICENSE-MIT} +0 -0
@@ -1,76 +0,0 @@
1
- // Run-level meta — the fields at the top of a report describing the run
2
- // that produced it. Both the report view (ui/view/ingest.js) and the
3
- // OPFS-wide index (client/bundle-finding-index.js) project the header
4
- // onto the findings, so every consumer reads run meta off a finding
5
- // without asking whether the file was one run or a deduplicated dump.
6
- export const META_FIELDS = ['type', 'model', 'think', 'effort', 'exportsMode']
7
-
8
- // Each run-meta field the finding doesn't specify, filled in place from
9
- // the report header. Per-field, not all-or-nothing: `deduplicate` stamps
10
- // `model` per finding while the rest stay run-level, so a finding
11
- // carrying only its own `model` still needs the header's `type`. Null
12
- // counts as unspecified — a report is JSON, where `"type": null` can't be
13
- // told from an omitted key. Source-marked reports opt out wholesale:
14
- // each is one analyzer, and its report-level `type` is the product's
15
- // category rather than a run descriptor.
16
- export function inheritReportMeta(finding, data) {
17
- if (data.source) return
18
- for (const key of META_FIELDS) {
19
- if (finding[key] == null && data[key] != null) finding[key] = data[key]
20
- }
21
- }
22
-
23
- // `"repo": { "github": "owner/name" }` at the top of a native dump, the
24
- // repository the run covered. NOT inherited onto findings the way
25
- // META_FIELDS are: a finding's own `repo.github` names the upstream of
26
- // the file IT sits in — a dependency's repo under `node_modules/` — so
27
- // stamping the report's over it would mislabel every dependency finding.
28
- //
29
- // Takes the slug or a github.com URL, with or without scheme, `.git` or
30
- // a trailing `/tree/main`, and normalises both to the slug. Anything
31
- // else is null: a value the link builders would splice into a broken URL
32
- // is worse than none.
33
- const GITHUB_URL_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/?#]+)\/([^/?#]+?)(?:\.git)?(?:[/?#].*)?$/iu
34
- const SLUG_RE = /^[\w.-]+\/[\w.-]+$/u
35
-
36
- // Is this value that slug already? Asked wherever a report hands over
37
- // something that may be one: `repo.github` below, a Piolium preamble's
38
- // `**Target:**`, a finding's repo on its way to a link.
39
- export function isRepoSlug(s) {
40
- return SLUG_RE.test(s)
41
- }
42
-
43
- export function reportRepoGithub(data) {
44
- const raw = data?.repo?.github
45
- if (typeof raw !== 'string') return null
46
- const trimmed = raw.trim().replace(/\/+$/u, '')
47
- if (!trimmed) return null
48
- const url = GITHUB_URL_RE.exec(trimmed)
49
- const slug = (url ? `${url[1]}/${url[2]}` : trimmed).replace(/\.git$/u, '')
50
- return isRepoSlug(slug) ? slug : null
51
- }
52
-
53
- // `"directory": "packages/babel-core"` beside that `github` — where
54
- // inside the repository the tree the report describes sits, the same
55
- // field npm's `repository` object carries for a package in a monorepo
56
- // (`{ "github": "babel/babel", "directory": "packages/babel-core" }`).
57
- // The paths a report writes are relative to that tree, so a link is
58
- // the repo, then this, then the path: `babel/babel` +
59
- // `packages/babel-core` + `src/index.js`. Declared in both the places
60
- // `repo` is — the report header and a finding's own — and read off
61
- // whichever of the two answered for the repo, never mixed.
62
- //
63
- // Normalised to a bare relative path: no leading `./` or `/`, no
64
- // trailing one, no repeated separators, and `''` — same as declaring
65
- // none — for the repo root. `''` too for anything a link builder
66
- // would splice into a broken URL, the rule `reportRepoGithub` follows
67
- // above: a non-string, a `?` or `#` (either cuts the path short of the
68
- // file, and swallows the `#L42` anchor a line link puts after it), or
69
- // a `..` segment climbing out of the repository.
70
- export function repoDirectory(repo) {
71
- const raw = typeof repo?.directory === 'string' ? repo.directory.trim() : ''
72
- const path = raw.replace(/^(?:\.\/)+/u, '').replaceAll(/^\/+|\/+$/gu, '').replaceAll(/\/{2,}/gu, '/')
73
- if (!path || path === '.') return ''
74
- if (/[?#]/u.test(path) || path.split('/').includes('..')) return ''
75
- return path
76
- }
@@ -1,149 +0,0 @@
1
- // Codex Security CSV parser. Input is a multi-scan CSV export with
2
- // rows like:
3
- // finding_url,repository,repository_url,title,description,severity,
4
- // status,detected_at,committed_at,author_email,assignee_name,
5
- // assignee_email,has_patch,configured_scan_id,commit_hash,
6
- // relevant_paths,resolution_reason
7
- // One CSV typically merges several scans (each `configured_scan_id`
8
- // is one scan). We split on that field and emit one report per scan.
9
- //
10
- // Display name per scan: `${repository}:${configured_scan_id stripped
11
- // of its `<prefix>:` head}`. Each scan must contain exactly one
12
- // repository — asserted, not silently merged.
13
- //
14
- // Only the first path in `relevant_paths` becomes `f.file` (some
15
- // findings list several, as `path1 | path2 | …`); the rest are dropped.
16
-
17
- import { normalizeFindingSeverity } from './severity.js'
18
-
19
- const REQUIRED_COLUMNS = [
20
- 'finding_url', 'repository', 'title', 'description', 'severity',
21
- 'configured_scan_id', 'relevant_paths',
22
- ]
23
-
24
- // RFC 4180-ish CSV parser: handles quoted fields containing commas,
25
- // embedded newlines, and `""` escaped quotes. Returns rows as arrays
26
- // of strings (no header / object conversion — caller picks columns by
27
- // index from the header row).
28
- function parseCsvRows(text) {
29
- const rows = []
30
- let row = []
31
- let field = ''
32
- let inQuote = false
33
- const n = text.length
34
- for (let i = 0; i < n; i++) {
35
- const c = text[i]
36
- if (inQuote) {
37
- if (c === '"') {
38
- if (text[i + 1] === '"') { field += '"'; i++ }
39
- else { inQuote = false }
40
- } else {
41
- field += c
42
- }
43
- } else if (c === '"') {
44
- inQuote = true
45
- } else if (c === ',') {
46
- row.push(field); field = ''
47
- } else if (c === '\n' || c === '\r') {
48
- row.push(field); rows.push(row); row = []; field = ''
49
- if (c === '\r' && text[i + 1] === '\n') i++
50
- } else {
51
- field += c
52
- }
53
- }
54
- // Trailing field / row (no final newline).
55
- if (field !== '' || row.length > 0) { row.push(field); rows.push(row) }
56
- // Strip purely-empty trailing rows that come from a trailing newline.
57
- while (rows.length > 0 && rows.at(-1).length === 1 && rows.at(-1)[0] === '') rows.pop()
58
- return rows
59
- }
60
-
61
- // Rows as `{ <column>: value }` records keyed by the header row, so the
62
- // mapping below reads columns by name; a column the file lacks reads as
63
- // undefined, a cell a short row lacks as ''. Null-prototype so a column
64
- // named `constructor` can't alias an inherited key.
65
- function csvRecords(header, rows) {
66
- return rows.map((cells) => {
67
- const record = Object.create(null)
68
- header.forEach((name, i) => { record[name] = cells[i] ?? '' })
69
- return record
70
- })
71
- }
72
-
73
- // Parse + split + convert. Returns one entry per scan:
74
- // { displayName, data: { type, source, findings: [...] } }
75
- // where `data` is the same shape ingest.js consumes from JSON.
76
- export function parseCodexCsvToScans(text) {
77
- const rows = parseCsvRows(text)
78
- if (rows.length < 2) throw new Error('Codex CSV: empty or missing header row')
79
- const [header, ...body] = rows
80
- for (const required of REQUIRED_COLUMNS) {
81
- if (!header.includes(required)) throw new Error(`Codex CSV: missing required column "${required}"`)
82
- }
83
-
84
- // Group rows by configured_scan_id; a row without one (a blank line
85
- // included) is skipped.
86
- const byScan = new Map()
87
- for (const record of csvRecords(header, body)) {
88
- const scanId = record.configured_scan_id
89
- if (!scanId) continue
90
- if (!byScan.has(scanId)) byScan.set(scanId, [])
91
- byScan.get(scanId).push(record)
92
- }
93
- if (byScan.size === 0) throw new Error('Codex CSV: no rows with a configured_scan_id')
94
-
95
- const scans = []
96
- for (const [scanId, records] of byScan) {
97
- // Each scan must belong to a single repository — surface a real
98
- // error if upstream ever merges scans across repos rather than
99
- // silently lumping them under one display name.
100
- const repos = new Set(records.map((r) => r.repository).filter(Boolean))
101
- if (repos.size > 1) {
102
- throw new Error(`Codex CSV: scan ${scanId} contains multiple repositories: ${[...repos].join(', ')}`)
103
- }
104
- const repo = [...repos][0] || 'unknown-repo'
105
- // `${repo}:${suffix}`, the suffix being whatever follows the first
106
- // `:` in configured_scan_id (`uuid:<github-id>`) — the
107
- // human-meaningful half.
108
- const displayName = `${repo}:${scanId.replace(/^[^:]+:/u, '')}`
109
- scans.push({
110
- displayName,
111
- data: { type: 'security', source: 'codex-security', findings: records.map(rowToFinding) },
112
- })
113
- }
114
- return scans
115
- }
116
-
117
- function rowToFinding(r) {
118
- // First non-empty path only; the siblings of a `path1 | path2 | …`
119
- // list are dropped.
120
- const file = r.relevant_paths.split(' | ').map((s) => s.trim()).find(Boolean) || 'unknown'
121
-
122
- // Title + description joined with a blank line so the table view's
123
- // first-line title shows the headline and the expanded view shows
124
- // the full body.
125
- const description = [r.title, r.description].filter(Boolean).join('\n\n')
126
-
127
- const finding = {
128
- // finding_url is unique per upstream finding, so triage keys off it
129
- // and survives a reload. The saver takes any non-numeric id, URLs
130
- // included (triage.js).
131
- id: r.finding_url,
132
- file,
133
- // Codex CSVs lack line numbers — '?' is the same placeholder
134
- // markdown findings use when the source has no `#L<n>` anchor.
135
- line: '?',
136
- severity: (r.severity || 'medium').toLowerCase(),
137
- description,
138
- repo: { github: r.repository },
139
- // No per-finding `type`: the CSV carries no category column, and a
140
- // synthetic 'security' would print the same word on every run-meta
141
- // line with nothing to tell the findings apart. An empty run-meta
142
- // renders as no line at all. The report-level `type` still defaults
143
- // to 'security' for document.title.
144
- }
145
- if (r.commit_hash) finding.commitHash = r.commit_hash
146
- if (r.detected_at) finding.detectedAt = r.detected_at
147
- if (r.committed_at) finding.committedAt = r.committed_at
148
- return normalizeFindingSeverity(finding)
149
- }
@@ -1,199 +0,0 @@
1
- // Vercel DeepSec markdown findings parser. Per-finding shape differs
2
- // from parse-md.js (Claude Security):
3
- //
4
- // # Vulnerability Scan Report
5
- // …project metadata table… / ## Summary …summary table…
6
- //
7
- // ## HIGH (2)
8
- //
9
- // ### Finding title 1
10
- //
11
- // - **File:** `path/file.js`
12
- // - **Recent committers:** … (ignored)
13
- // - **Lines:** 26, 28
14
- // - **Slug:** rule-slug
15
- // - **Confidence:** high
16
- // - **Revalidation:** confirmed (only where the pass ran)
17
- // - **Reasoning:** what it concluded (only where the pass ran)
18
- //
19
- // prose body…
20
- //
21
- // **Recommendation:** recommendation text
22
- //
23
- // ---
24
- // ### Finding title 2 … ## MEDIUM (5) …
25
- //
26
- // The writer is `packages/deepsec/src/commands/report.ts` in
27
- // vercel-labs/deepsec; the shape above is settled there.
28
- //
29
- // Returns `{ type, source: 'deepsec', findings }`, or null when no
30
- // `## SEVERITY (n)` header appears and the chain moves on.
31
-
32
- import { normalizeNewlines, splitHeadingLine } from './md-structure.js'
33
- import { normalizeFindingSeverity } from './severity.js'
34
-
35
- // The `## SEVERITY (n)` header that marks a DeepSec document. Splitting
36
- // on it with the tier captured interleaves tiers and content:
37
- // [preamble, sevA, contentA, sevB, …].
38
- const SECTION_RE = /^## ([A-Z][A-Z_]*)\s*\(\d+\)\s*\n/mu
39
-
40
- // DeepSec's tiers onto the internal ladder. It separates vulnerabilities
41
- // (CRITICAL … LOW) from non-vuln defects (HIGH_BUG, BUG), and
42
- // `high_bug` / `bug` keep that apart so the chips count them
43
- // separately. Anything else falls back to medium, where a renamed or
44
- // new tier stays visible instead of vanishing.
45
- function mapSeverity(s) {
46
- switch (s.toUpperCase()) {
47
- case 'CRITICAL': return 'critical'
48
- case 'HIGH': return 'high'
49
- case 'MEDIUM': return 'medium'
50
- case 'LOW': return 'low'
51
- case 'HIGH_BUG': return 'high_bug'
52
- case 'BUG': return 'bug'
53
- case 'INFO': case 'INFORMATIONAL': return 'informational'
54
- case 'INFORMATIVE': return 'informative'
55
- default: return 'medium'
56
- }
57
- }
58
-
59
- // A field's value as the word it names, whatever punctuation it arrived
60
- // in — the writer's `~~false positive~~`, or a hand-edited document's
61
- // backticks and emphasis.
62
- const word = (s) => String(s ?? '').toLowerCase().replaceAll(/[^a-z]+/gu, '')
63
-
64
- // DeepSec's `high` / `medium` / `low` is a closed enum its schema
65
- // validates, and what the words MEAN is nowhere: the investigate prompt
66
- // asks for one of the three without saying what separates them, the docs
67
- // call it "the agent's self-rated confidence", and nothing in DeepSec
68
- // reads it back. So there is no probability to convert, only three rungs
69
- // to place on the app's 0—10 scale — where 0 is a claim the revalidation
70
- // pass withdrew, 10 the no-doubt an unscored import rides at, and a
71
- // fresh load opens on a floor of 6, 7 or 8 by volume, then walks down
72
- // through any gap that reveals nothing new (ui filters.js).
73
- //
74
- // So `high` clears every floor the tune can pick without claiming the
75
- // app's no-doubt 10; `medium` is the lowest of those floors, surviving a
76
- // small report's opening view and dropping out of a big one's; `low`
77
- // sits under every floor but clear of the 0 that means refuted, since
78
- // the agent still chose to report it. The even spacing carries as much —
79
- // the walk settles in the GAPS, one step under the lowest rung it keeps,
80
- // so a rung packed tighter leaves it nowhere to stop and a rung moved
81
- // without its gap puts that tier off screen at open.
82
- const CONFIDENCE = new Map([['high', 8], ['medium', 6], ['low', 4]])
83
-
84
- // An unknown word reads as the middle rung, for the reason an
85
- // unrecognized severity falls back to medium: a level DeepSec adds later
86
- // should neither vanish under the floor nor — as scoring it nothing
87
- // would — ride the unscored stand-in at 10, above every `high`. A block
88
- // with no `Confidence:` line rated nothing, and there that stand-in is
89
- // the honest answer.
90
- function mapConfidence(s) {
91
- if (s === undefined) return undefined
92
- return CONFIDENCE.get(word(s)) ?? CONFIDENCE.get('medium')
93
- }
94
-
95
- // The verdict of DeepSec's revalidation pass as its writer spells it —
96
- // `confirmed`, `~~false positive~~` struck through, `uncertain` for
97
- // everything else it can answer — onto the app's own outcomes
98
- // (finding.js REVALIDATE_KINDS).
99
- //
100
- // It belongs with the confidence question rather than beside it:
101
- // `Confidence:` is the INVESTIGATE pass's self-rating, written before
102
- // the adversarial pass looked at the finding, and `refuted` is the
103
- // outcome that acts on the number — the range reads a ruled-out row as
104
- // 0 whatever it claims. A report saying `high` on one line and
105
- // `~~false positive~~` on the next is not a finding to show at 8/10.
106
- const REVALIDATION = new Map([
107
- ['confirmed', 'confirmed'],
108
- ['falsepositive', 'refuted'],
109
- ['uncertain', 'unknown'],
110
- ])
111
-
112
- export function parseDeepsecFindings(content) {
113
- const text = normalizeNewlines(content).trim()
114
- // Format guard — without a single `## SEVERITY (n)` header this isn't
115
- // a DeepSec doc; bail out so the chain moves on to
116
- // parseMarkdownFindings.
117
- const parts = text.split(SECTION_RE)
118
- if (parts.length === 1) return null
119
-
120
- const findings = []
121
- for (let i = 1; i < parts.length; i += 2) {
122
- const sev = mapSeverity(parts[i])
123
- // Each finding inside a severity section starts with `### Title`.
124
- for (const block of parts[i + 1].split(/^### /mu).slice(1)) {
125
- const f = parseBlock(block, sev)
126
- if (f) findings.push(f)
127
- }
128
- }
129
- if (findings.length === 0) return null
130
-
131
- // Report-level 'security' for the document.title fallback. No
132
- // per-finding `type`: DeepSec categorizes by severity alone, as codex
133
- // does, and ingest.js's `data.source` gate keeps the report-level one
134
- // off the findings.
135
- return { type: 'security', source: 'deepsec', findings }
136
- }
137
-
138
- function parseBlock(block, severity) {
139
- // The `---` separator after each finding in a section is shed.
140
- const { title, body: rawBody } = splitHeadingLine(block)
141
- if (!title) return null
142
- const body = rawBody.replace(/\n---\s*$/u, '').trim()
143
-
144
- // Bullet metadata: `- **Field:** value`. Field names case-folded.
145
- const fields = {}
146
- for (const m of body.matchAll(/^- \*\*([^:*]+):\*\*\s*(.+)$/gmu)) {
147
- fields[m[1].trim().toLowerCase()] = m[2].trim()
148
- }
149
-
150
- // A bold inline label in the body, not a `## Recommended fix` H2 as
151
- // Claude Security writes — so the split is there.
152
- const recMatch = /^\*\*Recommendation:\*\*\s*/mu.exec(body)
153
- let prose = body
154
- let recommendation = ''
155
- if (recMatch) {
156
- prose = body.slice(0, recMatch.index)
157
- recommendation = body.slice(recMatch.index + recMatch[0].length).trim()
158
- }
159
-
160
- // Prose minus the bullet metadata, with `**bold**` stripped — the
161
- // renderer escapes HTML, so the markers would print literally.
162
- const description = prose
163
- .split('\n')
164
- .filter((line) => !/^\s*- \*\*/u.test(line))
165
- .join('\n')
166
- .replaceAll('**', '')
167
- .trim()
168
-
169
- // Title first, as parse-md and parse-codex write it, so the table
170
- // view's first line is the headline.
171
- const fullDescription = [title, description].filter(Boolean).join('\n\n')
172
-
173
- // The path arrives backticked (`path/file.js`); the backticks are
174
- // notation, not part of it.
175
- const file = (fields.file || 'unknown').replace(/^`(.*)`$/u, '$1')
176
- // First non-empty line only — the renderer takes a single `f.line`,
177
- // and lineLink wraps it as a `#L<n>` anchor when a fileUrl is
178
- // available. The siblings of a `26, 28` list are dropped.
179
- const line = (fields.lines || '').split(',').map((s) => s.trim()).find(Boolean) || '?'
180
-
181
- const finding = { file, line, severity, description: fullDescription }
182
- if (recommendation) finding.recommendation = recommendation.replaceAll('**', '')
183
- const confidence = mapConfidence(fields.confidence)
184
- if (confidence !== undefined) finding.confidence = confidence
185
- // What the pass concluded, where the report has been through it: the
186
- // verdict as one of the app's outcomes, the reasoning under it as the
187
- // pass's remark, DeepSec named as whose pass said so. The two are read
188
- // as a pair because the document writes them as one — a `Reasoning:`
189
- // line is the pass's, not the finding's. First line only, like every
190
- // field here; a wrapped remainder stays in the prose where it was.
191
- const revalidate = REVALIDATION.get(word(fields.revalidation))
192
- if (revalidate) {
193
- finding.revalidate = revalidate
194
- finding.revalidateSource = 'deepsec'
195
- if (fields.reasoning) finding.revalidateVerdict = fields.reasoning.replaceAll('**', '')
196
- }
197
- if (fields.slug) finding.slug = fields.slug
198
- return normalizeFindingSeverity(finding, severity === 'informative' ? 'medium' : severity)
199
- }