@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 +29 -2
- package/lib/convert/ghsa.js +6 -54
- package/lib/hackerone/index.js +42 -0
- package/lib/hackerone/weaknesses.json +6550 -0
- package/lib/record.js +54 -4
- package/package.json +3 -2
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
|
-
|
|
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
|
package/lib/convert/ghsa.js
CHANGED
|
@@ -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
|
|
188
|
-
//
|
|
189
|
-
//
|
|
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
|
|
196
|
-
|
|
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
|
+
}
|