@ulisesgascon/openjs-cna-tools 1.0.0-alpha.3 → 1.0.0-alpha.4

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/README.md CHANGED
@@ -252,6 +252,32 @@ const { blocked, results } = await publishRecords(entries, {
252
252
  })
253
253
  ```
254
254
 
255
+ ### HackerOne weaknesses
256
+
257
+ HackerOne's REST API reports a finding's weakness as HackerOne's own numeric
258
+ `weakness_id`, not as a CWE number (weakness 26 is "Improper Access Control",
259
+ CWE-284). The taxonomy is shared by every program, so the mapping ships with
260
+ the package as data:
261
+
262
+ ```js
263
+ import { hackerOneWeakness, hackerOneWeaknessProblemType } from '@ulisesgascon/openjs-cna-tools/hackerone'
264
+
265
+ hackerOneWeakness(26)
266
+ // { cwe: 'CWE-284', name: 'Improper Access Control - Generic' }
267
+ hackerOneWeakness(75)
268
+ // { cwe: null, name: 'Privilege Escalation' } (a CAPEC entry on HackerOne's side)
269
+ hackerOneWeakness(999999)
270
+ // null (unknown id)
271
+
272
+ // Ready-made problemTypes[] entry, or null when there is no CWE to put on the record.
273
+ hackerOneWeaknessProblemType(26)
274
+ ```
275
+
276
+ `lib/hackerone/weaknesses.json` is regenerated with
277
+ `node scripts/build-hackerone-weaknesses.js <dump.json>` from the GraphQL
278
+ response the HackerOne web app loads for a report's weakness picker; the
279
+ script documents the shape it expects.
280
+
255
281
  ### Record helpers
256
282
 
257
283
  Converters only know their own source. The parts of a record that are about
@@ -260,13 +286,14 @@ hardcodes the OpenJS CNA org id, the generator string or the CVSS block
260
286
  layout:
261
287
 
262
288
  ```js
263
- import { buildCvssMetric, buildProblemType, buildRecordEnvelope } from '@ulisesgascon/openjs-cna-tools/record'
289
+ import { buildCvssMetric, buildDescription, buildProblemType, buildRecordEnvelope } from '@ulisesgascon/openjs-cna-tools/record'
264
290
 
265
291
  const record = buildRecordEnvelope({
266
292
  cveId: 'CVE-2026-0001',
267
293
  cna: {
268
294
  title: '…',
269
- descriptions: [{ lang: 'en', value: '…' }],
295
+ // plain text in `value`; the Markdown, if any, kept in supportingMedia
296
+ descriptions: [buildDescription('A flaw in `dns.resolve()` allows **remote** attackers …')],
270
297
  affected: [/* … */],
271
298
  references: [/* … */],
272
299
  // id normalised to CWE-<number>; description is the name, or the id
@@ -24,7 +24,7 @@
24
24
  // shape-translation logic is testable without touching the network.
25
25
 
26
26
  import { USER_AGENT } from '../client.js'
27
- import { buildCvssMetric, buildProblemType, buildRecordEnvelope } from '../record.js'
27
+ import { buildCvssMetric, buildDescription, buildProblemType, buildRecordEnvelope } from '../record.js'
28
28
  import { OPENJS_CNA_ORG_ID } from '../review/schema.js'
29
29
  import { parseVersionRange } from './version-range.js'
30
30
 
@@ -149,63 +149,15 @@ function stripReferencesSection (md) {
149
149
  return refIdx >= 0 ? normalized.slice(0, refIdx).trimEnd() : normalized
150
150
  }
151
151
 
152
- // Strip Markdown formatting down to readable plain text. Generic — no
153
- // hard-coded section names beyond the References block (which is dropped
154
- // upstream by stripReferencesSection because cna.references[] is its
155
- // structured home). Headers become "Label:" lines (matches the published
156
- // OpenJS CVE convention), emphasis/code/links collapse to their text
157
- // content, list bullets drop the bullet marker, blockquote markers drop.
158
- function stripMarkdown (md) {
159
- if (typeof md !== 'string') return ''
160
- // Normalize line endings — GitHub serves CRLF, every regex below uses
161
- // `\n` as the line boundary.
162
- let text = md.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
163
- // Headers → "Label:" (any ATX heading, level 1–6, generic). We use
164
- // [ \t]* (not \s*) for leading/trailing whitespace so we don't eat the
165
- // preceding/following blank lines — those are what visually separate
166
- // the heading label from neighboring paragraphs.
167
- text = text.replace(/^[ \t]*#{1,6}[ \t]+(.+?)[ \t]*$/gm, '$1:')
168
- // **bold** / __bold__ / *italic* / _italic_ → inner text
169
- text = text.replace(/\*\*(.+?)\*\*/g, '$1')
170
- text = text.replace(/__(.+?)__/g, '$1')
171
- text = text.replace(/(?<![*\w])\*(?!\s)(.+?)(?<!\s)\*(?![*\w])/g, '$1')
172
- text = text.replace(/(?<![_\w])_(?!\s)(.+?)(?<!\s)_(?![_\w])/g, '$1')
173
- // `inline code` → inner text
174
- text = text.replace(/`(.+?)`/g, '$1')
175
- // [link](url) → link
176
- text = text.replace(/\[(.+?)\]\([^)]+\)/g, '$1')
177
- // Bullet / ordered-list markers at line start → strip the marker
178
- text = text.replace(/^\s*(?:[-*+]|\d+\.)\s+/gm, '')
179
- // Blockquote markers
180
- text = text.replace(/^\s*>\s?/gm, '')
181
- // Collapse 3+ blank lines to a single blank line.
182
- text = text.replace(/\n{3,}/g, '\n\n')
183
- return text.trim()
184
- }
185
-
186
152
  function buildDescriptions (advisory) {
187
- // GHSA advisory descriptions are authored in Markdown. MITRE's
188
- // descriptions[0].value field is plain-text in every published OpenJS
189
- // CVE on record, so we strip the Markdown for the value and put the
190
- // (de-duplicated) original Markdown into supportingMedia[0]. The
191
- // References block is removed from both representations because its
153
+ // GHSA advisory descriptions are authored in Markdown; buildDescription
154
+ // puts the plain text in `value` and keeps the Markdown in
155
+ // supportingMedia. The References block is removed first because its
192
156
  // URLs already live in cna.references[] as structured data.
193
157
  const original = (advisory.description ?? '').trim() || (advisory.summary ?? '').trim()
194
158
  if (!original) return []
195
- const raw = stripReferencesSection(original).trim()
196
- if (!raw) return []
197
- const plain = stripMarkdown(raw) || raw
198
- const description = {
199
- lang: 'en',
200
- value: plain
201
- }
202
- // Only attach supportingMedia when the raw Markdown still carries
203
- // formatting after the References strip. If plain === raw the markdown
204
- // form has nothing extra to offer.
205
- if (raw !== plain) {
206
- description.supportingMedia = [{ type: 'text/markdown', base64: false, value: raw }]
207
- }
208
- return [description]
159
+ const description = buildDescription(stripReferencesSection(original))
160
+ return description ? [description] : []
209
161
  }
210
162
 
211
163
  function buildAffected (advisory) {
@@ -0,0 +1,42 @@
1
+ // HackerOne weakness ids -> CWE.
2
+ //
3
+ // HackerOne identifies a report's weakness by its own numeric id, which is
4
+ // what its REST API exposes as `weakness_id` and what projects end up
5
+ // storing. It is not a CWE number: weakness 26 is "Improper Access Control"
6
+ // (CWE-284), 48 is "Uncontrolled Resource Consumption" (CWE-400). The CWE
7
+ // is a separate attribute of the weakness, and HackerOne's taxonomy is the
8
+ // same for every program, so the mapping ships here as data
9
+ // (./weaknesses.json, regenerated with scripts/build-hackerone-weaknesses.js).
10
+
11
+ import { createRequire } from 'node:module'
12
+ import { buildProblemType } from '../record.js'
13
+
14
+ const require = createRequire(import.meta.url)
15
+ const { weaknesses, _generated: generated } = require('./weaknesses.json')
16
+
17
+ export const HACKERONE_WEAKNESSES_GENERATED = generated
18
+
19
+ /**
20
+ * Look a HackerOne weakness id up. Accepts a number or a string.
21
+ *
22
+ * Returns `{ cwe, name }` where `cwe` is `CWE-<number>` or `null` for a
23
+ * weakness HackerOne maps to no CWE (CAPEC entries), or `null` when the id
24
+ * is unknown.
25
+ */
26
+ export function hackerOneWeakness (id) {
27
+ const key = String(id ?? '').trim()
28
+ if (!/^\d+$/.test(key)) return null
29
+ const entry = weaknesses[key]
30
+ return entry ? { cwe: entry.cwe, name: entry.name } : null
31
+ }
32
+
33
+ /**
34
+ * The `problemTypes[]` entry for a HackerOne weakness id, or `null` when
35
+ * the id is unknown or has no CWE. The CWE name on the record is
36
+ * HackerOne's weakness name.
37
+ */
38
+ export function hackerOneWeaknessProblemType (id) {
39
+ const entry = hackerOneWeakness(id)
40
+ if (!entry?.cwe) return null
41
+ return buildProblemType({ cweId: entry.cwe, name: entry.name })
42
+ }