@ulisesgascon/openjs-cna-tools 0.0.0-stage → 1.0.0-alpha.3
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/LICENSE +21 -0
- package/README.md +312 -2
- package/bin/openjs-cna.js +128 -0
- package/components/config.js +92 -0
- package/components/convert.js +81 -0
- package/components/get.js +35 -0
- package/components/health.js +30 -0
- package/components/list.js +36 -0
- package/components/llm-review-prompt.js +84 -0
- package/components/preview.js +42 -0
- package/components/publish.js +238 -0
- package/components/reserve.js +48 -0
- package/components/review.js +170 -0
- package/components/update.js +104 -0
- package/lib/auth.js +20 -0
- package/lib/batch.js +141 -0
- package/lib/cli.js +81 -0
- package/lib/client.js +244 -0
- package/lib/component-utils.js +74 -0
- package/lib/config.js +121 -0
- package/lib/convert/ghsa.js +450 -0
- package/lib/convert/version-range.js +251 -0
- package/lib/cvss.js +113 -0
- package/lib/errors.js +44 -0
- package/lib/index.js +9 -0
- package/lib/preview.js +145 -0
- package/lib/prompt.js +170 -0
- package/lib/record.js +123 -0
- package/lib/review/index.js +78 -0
- package/lib/review/rules.js +433 -0
- package/lib/review/schema.js +99 -0
- package/lib/review/schemas/cve-record-format.json +1362 -0
- package/lib/review/schemas/imports/cvss/cvss-v2.0.json +195 -0
- package/lib/review/schemas/imports/cvss/cvss-v3.0.json +577 -0
- package/lib/review/schemas/imports/cvss/cvss-v3.1.json +577 -0
- package/lib/review/schemas/imports/cvss/cvss-v4.0.json +784 -0
- package/lib/review/schemas/tags/adp-tags.json +7 -0
- package/lib/review/schemas/tags/cna-tags.json +7 -0
- package/lib/review/schemas/tags/reference-tags.json +27 -0
- package/package.json +55 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ulises Gascón
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,313 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenJS CNA Tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmjs.org/package/@ulisesgascon/openjs-cna-tools)
|
|
4
|
+
|
|
5
|
+
EXPERIMENTAL CLI and library for operating the [OpenJS Foundation CNA API](https://cna.openjsf.org/api.html).
|
|
6
|
+
|
|
7
|
+
For project maintainers under the OpenJS CNA who need to reserve, publish, update, and inspect CVEs against the OpenJS CNA API (MITRE wrapper) without leaving their terminal.
|
|
8
|
+
|
|
9
|
+
<!-- TOC -->
|
|
10
|
+
|
|
11
|
+
- [Commands](#commands)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Setting up CNA credentials](#setting-up-cna-credentials)
|
|
15
|
+
- [Protecting your credentials](#protecting-your-credentials)
|
|
16
|
+
- [Typical workflow](#typical-workflow)
|
|
17
|
+
- [Troubleshooting](#troubleshooting)
|
|
18
|
+
- [Library](#library)
|
|
19
|
+
- [Design](#design)
|
|
20
|
+
- [License](#license)
|
|
21
|
+
|
|
22
|
+
<!-- /TOC -->
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
`openjs-cna-experimental` is a single binary that dispatches subcommands.
|
|
27
|
+
|
|
28
|
+
| Command | Description |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `openjs-cna-experimental reserve` | Reserve a CVE id and print it on stdout. |
|
|
31
|
+
| `openjs-cna-experimental publish <cve-id> -f container.json` | Publish a v5.2 CNA Container. Runs the reviewer first by default. |
|
|
32
|
+
| `openjs-cna-experimental publish -f a.json -f b.json …` | Batch publish. Reviews every record first and sends nothing unless all pass, then publishes each in order. `--skip-published` makes reruns idempotent, `--json` prints one result per record. |
|
|
33
|
+
| `openjs-cna-experimental update <cve-id> -f container.json` | Update a published CVE record. Same review semantics as `publish`. |
|
|
34
|
+
| `openjs-cna-experimental get <cve-id>` | Fetch a published CVE's container from MITRE. |
|
|
35
|
+
| `openjs-cna-experimental list` | List every CVE id in the bucket. |
|
|
36
|
+
| `openjs-cna-experimental convert <advisory-url>` | Turn a GitHub Security Advisory into a draft CNA Container. Accepts published per-repo URLs (`https://github.com/<owner>/<repo>/security/advisories/<GHSA>`) and global-database URLs (`https://github.com/advisories/<GHSA>`) for public ones. |
|
|
37
|
+
| `openjs-cna-experimental review <container.json> [more.json …]` | Run the offline reviewer against one or more records. Defaults to showing a preview + issues. Exits non-zero on blockers. With several files `--json` emits an array. |
|
|
38
|
+
| `openjs-cna-experimental preview <container.json>` | Print a compact summary of a container — title, description, affected, CVSS, CWE, credits, refs, etc. |
|
|
39
|
+
| `openjs-cna-experimental llm-review-prompt <record.json>` | Print a prompt for an LLM-driven editorial review of the record's title and description. The tool does not call an LLM; paste the prompt into the LLM of your choice. |
|
|
40
|
+
| `openjs-cna-experimental health` | Unauthenticated liveness check against the CNA API. |
|
|
41
|
+
| `openjs-cna-experimental config` | Read / write `openjs-cna-experimental` configuration. |
|
|
42
|
+
|
|
43
|
+
Run `openjs-cna-experimental <command> --help` for command-specific options.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Install
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pnpm add -g @ulisesgascon/openjs-cna-tools
|
|
51
|
+
# or
|
|
52
|
+
npm install -g @ulisesgascon/openjs-cna-tools
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To build from source:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
git clone https://github.com/UlisesGascon/openjs-cna-tools.git
|
|
59
|
+
cd openjs-cna-tools
|
|
60
|
+
pnpm install
|
|
61
|
+
pnpm link --global
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires Node.js **22.0.0** or newer. The package is ESM-only and uses native `fetch`, `node:util parseArgs`, and `node:readline/promises`. Runtime dependencies are [`ajv`](https://github.com/ajv-validator/ajv) and [`ajv-formats`](https://github.com/ajv-validator/ajv-formats), used solely for JSON Schema validation against MITRE's official v5.2 CNA Container schema.
|
|
65
|
+
|
|
66
|
+
### Setting up CNA credentials
|
|
67
|
+
|
|
68
|
+
`openjs-cna-experimental` needs two values to talk to the CNA API:
|
|
69
|
+
|
|
70
|
+
- `openjs_cna_token` — bearer in `bucket:secret` form. Your CNA coordinator issues this.
|
|
71
|
+
- `openjs_cna_base_url` — the API base URL (e.g. `https://cna.openjsf.org`).
|
|
72
|
+
|
|
73
|
+
Configure them with `openjs-cna-experimental config`:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
openjs-cna-experimental config set openjs_cna_token "<bucket>:<hex-secret>"
|
|
77
|
+
openjs-cna-experimental config set openjs_cna_base_url "https://cna.openjsf.org"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Stored values are not encrypted at rest.** They live as plaintext in `~/.config/openjs-cna/config.json` with file mode `0600`. If you'd rather not put a token on disk, pass them via environment variables instead:
|
|
81
|
+
>
|
|
82
|
+
> ```sh
|
|
83
|
+
> export OPENJS_CNA_TOKEN="<bucket>:<hex-secret>"
|
|
84
|
+
> export OPENJS_CNA_BASE_URL="https://cna.openjsf.org"
|
|
85
|
+
> ```
|
|
86
|
+
|
|
87
|
+
Configuration is layered. Precedence (highest first):
|
|
88
|
+
|
|
89
|
+
1. CLI flag (`--token`, `--base-url`)
|
|
90
|
+
2. Environment (`OPENJS_CNA_*`)
|
|
91
|
+
3. Project config (`./.openjs-cna.json` in CWD)
|
|
92
|
+
4. Global config (`~/.config/openjs-cna/config.json` or `$XDG_CONFIG_HOME/openjs-cna/config.json`)
|
|
93
|
+
|
|
94
|
+
### Protecting your credentials
|
|
95
|
+
|
|
96
|
+
Add the following to your [global `gitignore`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile) (or the per-repo `.gitignore` if you only need it locally):
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
# openjs-cna-tools project-scope config
|
|
100
|
+
.openjs-cna.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The global config at `~/.config/openjs-cna/config.json` lives outside any repo, so it doesn't need a gitignore entry. The project-scope `.openjs-cna.json` does.
|
|
104
|
+
|
|
105
|
+
If you ever commit a token, revoke it with your CNA coordinator and request a new one.
|
|
106
|
+
|
|
107
|
+
### Typical workflow
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
# 1. Reserve a CVE id for an upcoming release.
|
|
111
|
+
CVE=$(openjs-cna-experimental reserve)
|
|
112
|
+
echo "Reserved $CVE"
|
|
113
|
+
|
|
114
|
+
# 2. Draft a container from a published GitHub Security Advisory.
|
|
115
|
+
# `--cve` stamps cveMetadata.cveId into the container so step 5 can
|
|
116
|
+
# cross-check the CLI argument against it.
|
|
117
|
+
openjs-cna-experimental convert https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx \
|
|
118
|
+
--cve "$CVE" > container.json
|
|
119
|
+
|
|
120
|
+
# 3. (Optional) Editorial polish via an LLM. Pipes a prompt to your
|
|
121
|
+
# clipboard; paste it plus container.json into the LLM of your choice.
|
|
122
|
+
openjs-cna-experimental llm-review-prompt container.json | pbcopy
|
|
123
|
+
|
|
124
|
+
# 4. Review before you publish. The output renders a preview of the
|
|
125
|
+
# container plus the issues list (tip: edit visually at
|
|
126
|
+
# https://vulnogram.org/#source).
|
|
127
|
+
openjs-cna-experimental review container.json
|
|
128
|
+
|
|
129
|
+
# 5. Publish. Review runs again automatically (strict, warnings block);
|
|
130
|
+
# if `container.cveMetadata.cveId` is set, it must match "$CVE" or
|
|
131
|
+
# publish aborts before sending anything to MITRE.
|
|
132
|
+
openjs-cna-experimental publish "$CVE" -f container.json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Useful flags:
|
|
136
|
+
|
|
137
|
+
- `--no-strict` (on `review`, `publish`, `update`): let warnings pass as warnings. True blockers still abort.
|
|
138
|
+
- `--skip-review` (on `publish`, `update`): bypass the reviewer entirely. Discouraged.
|
|
139
|
+
- `--no-preview` (on `review`): drop the container summary; print issues only. Useful in CI.
|
|
140
|
+
- `--tier mitre-common|openjs-extra` (on `review`): filter the printed issues to a single tier without changing the exit code.
|
|
141
|
+
- `--yes` / `OPENJS_CNA_YES=1`: accept confirmation prompts non-interactively.
|
|
142
|
+
- `--skip-published` (on batch `publish`): ask the CNA first (`get-cve`, one sequential call per id) and skip ids that already have a published record, so a rerun after a partial failure only sends what is missing. Only a record-shaped answer counts as published; a failed or empty lookup sends the record, since a second publish fails loudly at the CNA while a wrong skip would silently send nothing.
|
|
143
|
+
|
|
144
|
+
Publishing a release with several CVEs from CI:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
# Every record carries cveMetadata.cveId. Review all, send nothing on any
|
|
148
|
+
# blocker, otherwise publish each in order; exit 1 if anything failed.
|
|
149
|
+
openjs-cna-experimental review records/*.json --no-preview
|
|
150
|
+
openjs-cna-experimental publish -f records/*.json --yes --skip-published --json > publish-result.json
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
How a batch behaves, and what to expect on a retry:
|
|
154
|
+
|
|
155
|
+
- **Everything is sequential.** Records are reviewed, checked and published
|
|
156
|
+
one at a time, in the order given. There is no concurrency against the
|
|
157
|
+
CNA API.
|
|
158
|
+
- **Without `--skip-published` the batch assumes a green field.** No lookup
|
|
159
|
+
is made; every record is sent. An id that was already published fails at
|
|
160
|
+
the CNA with its `correlation_id` / `run_id`, the remaining records still
|
|
161
|
+
go out, and the exit code is 1. Nothing is ever published twice, MITRE
|
|
162
|
+
rejects the second publish.
|
|
163
|
+
- **With `--skip-published` the batch is a safe retry.** Each id is looked
|
|
164
|
+
up first with `get-cve`; ids that already have a published record are
|
|
165
|
+
reported as `skipped`, the rest are sent, and the exit code only reflects
|
|
166
|
+
what was sent. Use it whenever a run may repeat a previous one: a manual
|
|
167
|
+
re-run after a partial failure, or a pipeline that triggers more than
|
|
168
|
+
once for the same release. The lookups roughly double the run time.
|
|
169
|
+
- **Review blockers stop everything.** If any record has a blocker nothing
|
|
170
|
+
is sent, with or without `--skip-published`.
|
|
171
|
+
|
|
172
|
+
### Troubleshooting
|
|
173
|
+
|
|
174
|
+
- **Review reports schema blockers**: paste the container into <https://vulnogram.org/#source> to edit it visually; rerun `openjs-cna-experimental review` when done.
|
|
175
|
+
- **Publish fails with a CVE-id mismatch**: the CLI argument and `container.cveMetadata.cveId` disagree. Re-check which container you meant to publish before retrying.
|
|
176
|
+
- **An operation errors out**: the CLI prints `correlation_id` and `run_id`. Forward both to your CNA coordinator — that's everything they need to look up the underlying GitHub Actions run. (We deliberately don't surface the run URL because most operators don't have access to it.)
|
|
177
|
+
|
|
178
|
+
## Library
|
|
179
|
+
|
|
180
|
+
The same module powers the CLI and is importable for automation:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import { CnaClient } from '@ulisesgascon/openjs-cna-tools/client'
|
|
184
|
+
import { reviewRecord } from '@ulisesgascon/openjs-cna-tools/review'
|
|
185
|
+
import { convertGhsaToRecord } from '@ulisesgascon/openjs-cna-tools/convert'
|
|
186
|
+
|
|
187
|
+
const cna = new CnaClient({
|
|
188
|
+
token: process.env.OPENJS_CNA_TOKEN,
|
|
189
|
+
baseUrl: process.env.OPENJS_CNA_BASE_URL
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const { cveId } = await cna.reserveCve()
|
|
193
|
+
|
|
194
|
+
// `source` is a published GitHub advisory URL. References (including any
|
|
195
|
+
// vendor-advisory blog post you want surfaced on the CVE record) come
|
|
196
|
+
// from the advisory's own `references[]` — set them on GitHub, not via
|
|
197
|
+
// any per-call override here. Pass `cveId` to stamp it into `cveMetadata`.
|
|
198
|
+
const record = await convertGhsaToRecord({
|
|
199
|
+
source: 'https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx',
|
|
200
|
+
cveId
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// reviewRecord is strict by default — warnings count as blockers.
|
|
204
|
+
// Pass `{ strict: false }` to surface warnings as warnings.
|
|
205
|
+
const report = reviewRecord(record)
|
|
206
|
+
if (report.summary.blockers > 0) {
|
|
207
|
+
throw new Error('record is not ready to publish')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await cna.publishCve(cveId, record.containers.cna)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Error classes (`CnaAuthError`, `CnaTimeoutError`, `CnaOperationError`, `CnaReviewBlockedError`) are exported from the package root.
|
|
214
|
+
|
|
215
|
+
### CVSS
|
|
216
|
+
|
|
217
|
+
Trackers such as HackerOne store only the CVSS vector, while MITRE requires
|
|
218
|
+
`baseScore` and `baseSeverity` on every metric block. The package ships a
|
|
219
|
+
dependency-free CVSS v3.0 / v3.1 base score calculator for that gap:
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import { calculateBaseScore } from '@ulisesgascon/openjs-cna-tools/cvss'
|
|
223
|
+
|
|
224
|
+
calculateBaseScore('CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N')
|
|
225
|
+
// { version: '3.0', vectorString: 'CVSS:3.0/…', baseScore: 6.5, baseSeverity: 'MEDIUM' }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Only the base metric group is scored; temporal and environmental components
|
|
229
|
+
are kept in the vector string and ignored. `parseCvssVector`, `roundUp` and
|
|
230
|
+
`severityFromScore` are exported as well. CVSS 4.0 is not supported yet.
|
|
231
|
+
|
|
232
|
+
### Batch review and publish
|
|
233
|
+
|
|
234
|
+
The CLI's multi-file mode is a thin wrapper over two library functions:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
import { reviewRecords, publishRecords } from '@ulisesgascon/openjs-cna-tools/batch'
|
|
238
|
+
|
|
239
|
+
const entries = files.map((file) => ({ file, record: JSON.parse(readFileSync(file, 'utf8')) }))
|
|
240
|
+
|
|
241
|
+
// [{ file, cveId, ready, summary, issues }] — a record without
|
|
242
|
+
// cveMetadata.cveId gets a `cve-id-present` blocker.
|
|
243
|
+
const review = reviewRecords(entries, { strict: true })
|
|
244
|
+
|
|
245
|
+
// Reviews again, sends nothing if any record has a blocker, then publishes
|
|
246
|
+
// each one; `results` is [{ file, cveId, status, correlationId, runId, error }]
|
|
247
|
+
// with status published | skipped | failed.
|
|
248
|
+
const { blocked, results } = await publishRecords(entries, {
|
|
249
|
+
client: cna,
|
|
250
|
+
skipPublished: true,
|
|
251
|
+
onProgress: (event) => console.error(event.cveId, event.status)
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Record helpers
|
|
256
|
+
|
|
257
|
+
Converters only know their own source. The parts of a record that are about
|
|
258
|
+
MITRE's format rather than the source live in one place, so no consumer
|
|
259
|
+
hardcodes the OpenJS CNA org id, the generator string or the CVSS block
|
|
260
|
+
layout:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import { buildCvssMetric, buildProblemType, buildRecordEnvelope } from '@ulisesgascon/openjs-cna-tools/record'
|
|
264
|
+
|
|
265
|
+
const record = buildRecordEnvelope({
|
|
266
|
+
cveId: 'CVE-2026-0001',
|
|
267
|
+
cna: {
|
|
268
|
+
title: '…',
|
|
269
|
+
descriptions: [{ lang: 'en', value: '…' }],
|
|
270
|
+
affected: [/* … */],
|
|
271
|
+
references: [/* … */],
|
|
272
|
+
// id normalised to CWE-<number>; description is the name, or the id
|
|
273
|
+
problemTypes: [buildProblemType({ cweId: 400, name: 'Uncontrolled Resource Consumption' })],
|
|
274
|
+
// key (cvssV3_0 / cvssV3_1 / cvssV4_0) follows the vector prefix;
|
|
275
|
+
// a missing v3.x score is computed, a v4.0 score must be passed in
|
|
276
|
+
metrics: [buildCvssMetric('CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N')]
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
// record.dataType / dataVersion / cveMetadata set, providerMetadata.orgId
|
|
280
|
+
// and x_generator.engine filled on the container unless already present.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The GHSA converter is built on the same helpers.
|
|
284
|
+
|
|
285
|
+
The same calculator backs two behaviours:
|
|
286
|
+
|
|
287
|
+
- the reviewer's `cvss-score-matches-vector` rule (blocker) recomputes the
|
|
288
|
+
score of every v3.x block and rejects a `baseScore` that disagrees with its
|
|
289
|
+
vector, since MITRE accepts the record as sent and never re-scores it;
|
|
290
|
+
- the GHSA converter fills `baseScore` and `baseSeverity` from the vector when
|
|
291
|
+
the advisory carries a v3.1 vector without a score.
|
|
292
|
+
|
|
293
|
+
## Design
|
|
294
|
+
|
|
295
|
+
- **Two runtime dependencies.** Built on Node.js 22+ built-ins (`fetch`, `node:test`, `node:util parseArgs`, `node:readline/promises`) with [`ajv`](https://github.com/ajv-validator/ajv) + [`ajv-formats`](https://github.com/ajv-validator/ajv-formats) — used only for validating containers against MITRE's official v5.2 schema before publish.
|
|
296
|
+
- **Scores vectors itself.** A bundled CVSS v3.0 / v3.1 calculator (`lib/cvss.js`) lets the reviewer verify that every `baseScore` matches its vector and lets converters fill a missing score. No external calculator, no network.
|
|
297
|
+
- **Validates against MITRE's official schema.** The CVE Record Format schema and its companion files (CVSS v2/v3/v4, CNA/ADP/reference tags) ship bundled from the [CVE Project](https://github.com/CVEProject/cve-schema). `openjs-cna-experimental review` and the default behavior of `openjs-cna-experimental publish` catch anything that would fail at MITRE.
|
|
298
|
+
- **Two-tier rule classification.** Issues carry a `tier`: `mitre-common` (MITRE's schema itself would reject) or `openjs-extra` (OpenJS Foundation editorial preference, stricter than MITRE). The `--tier mitre-common` flag on `review` filters the printed list without changing the exit code.
|
|
299
|
+
- **Preview before commit.** `openjs-cna-experimental preview` and the default `openjs-cna-experimental review` render a full summary of the container (title, full description, every affected package, CVSS, CWE, credits, references) so an operator can see *exactly* what they're about to send. The output includes a tip to edit the JSON visually at <https://vulnogram.org/#source>.
|
|
300
|
+
- **Strict by default.** `review`, `publish`, and `update` treat warnings as blockers. Pass `--no-strict` only when you understand the warning is acceptable.
|
|
301
|
+
- **CVE-id consistency.** When `container.cveMetadata.cveId` is set and the CLI argument differs, `publish` and `update` abort before touching MITRE.
|
|
302
|
+
- **Multi-package advisories handled.** One CNA Container can carry every package an advisory affects (the lodash family of per-method packages is a common case). The converter expands GHSA's `vulnerabilities[]` array into MITRE's `affected[]` entries one-to-one.
|
|
303
|
+
- **GHSA descriptions are Markdown.** The converter declares `descriptions[].supportingMedia[]` with `type: 'text/markdown'` so the formatting isn't lost on the way to MITRE.
|
|
304
|
+
- **Identified at the source.** Every container the converter produces carries `cna.x_generator = { engine: '@ulisesgascon/openjs-cna-tools <version>' }`, and every outbound HTTP call uses the same string as `User-Agent`. A downstream reviewer can always trace a record back to the exact tool version that drafted it.
|
|
305
|
+
- **Operator-friendly error surfacing.** Failures emit `correlation_id` and `run_id` (not opaque GitHub URLs) so operators can forward a single line to a coordinator.
|
|
306
|
+
- **Stdout is data; stderr is the show.** Every command's primary output goes to stdout so `CVE=$(openjs-cna-experimental reserve)` works without flags. Spinners, prompts, and progress go to stderr.
|
|
307
|
+
- **The constructor does no I/O.** `new CnaClient()` makes zero network calls. Credentials are validated lazily on the first operation that needs them.
|
|
308
|
+
|
|
309
|
+
The design draws heavily on [`node-core-utils`](https://github.com/nodejs/node-core-utils) — lazy auth accessors, errors that double as setup instructions, yargs-as-router (`node:util parseArgs` here), stdout/stderr discipline, and no mocking library. A longer design write-up will land in `docs/` as the package matures.
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Router. Autoloads components/<verb>.js and hands the remaining argv to its
|
|
4
|
+
// `handler`.
|
|
5
|
+
|
|
6
|
+
import { readdirSync } from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import {
|
|
11
|
+
CnaAuthError,
|
|
12
|
+
CnaTimeoutError,
|
|
13
|
+
CnaOperationError,
|
|
14
|
+
CnaReviewBlockedError
|
|
15
|
+
} from '../lib/errors.js'
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
const pkg = require('../package.json')
|
|
19
|
+
|
|
20
|
+
const componentsDir = new URL('../components/', import.meta.url)
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
|
|
23
|
+
function listVerbs () {
|
|
24
|
+
try {
|
|
25
|
+
return readdirSync(componentsDir)
|
|
26
|
+
.filter(name => name.endsWith('.js'))
|
|
27
|
+
.map(name => name.slice(0, -3))
|
|
28
|
+
.sort()
|
|
29
|
+
} catch {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function showHelp () {
|
|
35
|
+
const verbs = listVerbs()
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`openjs-cna-experimental — operate the OpenJS Foundation CNA API
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
openjs-cna-experimental <command> [options]
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
${verbs.map(v => ' ' + v).join('\n')}
|
|
44
|
+
|
|
45
|
+
Global flags:
|
|
46
|
+
--token <value> override resolved token
|
|
47
|
+
--base-url <url> override resolved base URL
|
|
48
|
+
--timeout <seconds> per-operation timeout
|
|
49
|
+
--poll-interval <secs> polling interval
|
|
50
|
+
--json force JSON output where applicable
|
|
51
|
+
--quiet suppress non-essential stderr
|
|
52
|
+
--yes accept all confirmation prompts
|
|
53
|
+
--no-color disable color output
|
|
54
|
+
--version, -v
|
|
55
|
+
--help, -h
|
|
56
|
+
|
|
57
|
+
Run 'openjs-cna-experimental <command> --help' for command-specific options.
|
|
58
|
+
`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showVersion () {
|
|
63
|
+
process.stdout.write(`${pkg.version}\n`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mapErrorToExit (e) {
|
|
67
|
+
if (e instanceof CnaAuthError) return 2
|
|
68
|
+
if (e instanceof CnaTimeoutError) return 3
|
|
69
|
+
if (e instanceof CnaReviewBlockedError) return 1
|
|
70
|
+
if (e instanceof CnaOperationError) return 1
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main () {
|
|
75
|
+
const argv = process.argv.slice(2)
|
|
76
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
77
|
+
showHelp()
|
|
78
|
+
process.exit(argv.length === 0 ? 0 : 0)
|
|
79
|
+
}
|
|
80
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
81
|
+
showVersion()
|
|
82
|
+
process.exit(0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const verb = argv[0]
|
|
86
|
+
const rest = argv.slice(1)
|
|
87
|
+
|
|
88
|
+
let mod
|
|
89
|
+
try {
|
|
90
|
+
mod = await import(new URL(`../components/${verb}.js`, import.meta.url))
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
93
|
+
process.stderr.write(`Unknown command: ${verb}\n\n`)
|
|
94
|
+
showHelp()
|
|
95
|
+
process.exit(64) // EX_USAGE
|
|
96
|
+
}
|
|
97
|
+
throw e
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await mod.handler(rest)
|
|
102
|
+
process.exit(0)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// The CLI components already log structured error context; we just need
|
|
105
|
+
// to map the error class to an exit code here. Anything not in the
|
|
106
|
+
// CnaError family bubbles its stack so unexpected bugs aren't silent.
|
|
107
|
+
if (
|
|
108
|
+
e instanceof CnaAuthError ||
|
|
109
|
+
e instanceof CnaTimeoutError ||
|
|
110
|
+
e instanceof CnaOperationError ||
|
|
111
|
+
e instanceof CnaReviewBlockedError
|
|
112
|
+
) {
|
|
113
|
+
process.stderr.write(`✖ ${e.message}\n`)
|
|
114
|
+
if (e.correlationId || e.runId) {
|
|
115
|
+
process.stderr.write(` Forward to coordinator: correlation_id=${e.correlationId ?? '?'}, run_id=${e.runId ?? '?'}\n`)
|
|
116
|
+
}
|
|
117
|
+
process.exit(mapErrorToExit(e))
|
|
118
|
+
}
|
|
119
|
+
throw e
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main()
|
|
124
|
+
|
|
125
|
+
// Make the import order quiet for linters/tools that expect a non-empty
|
|
126
|
+
// epilogue when this file is imported as a module (it never is, but keep
|
|
127
|
+
// the symbol stable).
|
|
128
|
+
export { here }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// openjs-cna-experimental config <set|get|list|delete> ...
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseComponentArgs,
|
|
5
|
+
buildCli
|
|
6
|
+
} from '../lib/component-utils.js'
|
|
7
|
+
import {
|
|
8
|
+
getConfigValue,
|
|
9
|
+
setConfigValue,
|
|
10
|
+
deleteConfigValue,
|
|
11
|
+
listConfig
|
|
12
|
+
} from '../lib/config.js'
|
|
13
|
+
|
|
14
|
+
export const command = 'config'
|
|
15
|
+
export const describe = 'Read / write openjs-cna-experimental configuration.'
|
|
16
|
+
|
|
17
|
+
export async function handler (argv) {
|
|
18
|
+
const parsed = parseComponentArgs(argv, {
|
|
19
|
+
project: { type: 'boolean' }
|
|
20
|
+
}, { allowPositionals: true })
|
|
21
|
+
if (parsed.values.help) return printHelp()
|
|
22
|
+
|
|
23
|
+
const cli = buildCli(parsed)
|
|
24
|
+
const [subverb, ...rest] = parsed.positionals
|
|
25
|
+
const scope = parsed.values.project ? 'project' : 'global'
|
|
26
|
+
|
|
27
|
+
switch (subverb) {
|
|
28
|
+
case 'set': {
|
|
29
|
+
const [key, value] = rest
|
|
30
|
+
if (!key || value === undefined) {
|
|
31
|
+
process.stderr.write('✖ Usage: openjs-cna-experimental config set <key> <value> [--project]\n')
|
|
32
|
+
process.exit(64)
|
|
33
|
+
}
|
|
34
|
+
const target = setConfigValue(key, value, { scope })
|
|
35
|
+
cli.ok(`Wrote ${key} to ${target}`)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
case 'get': {
|
|
39
|
+
const [key] = rest
|
|
40
|
+
if (!key) {
|
|
41
|
+
process.stderr.write('✖ Usage: openjs-cna-experimental config get <key>\n')
|
|
42
|
+
process.exit(64)
|
|
43
|
+
}
|
|
44
|
+
const value = getConfigValue(key)
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
cli.error(`No config value for '${key}'`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`${value}\n`)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
case 'list': {
|
|
53
|
+
const config = listConfig()
|
|
54
|
+
process.stdout.write(JSON.stringify(config, null, 2) + '\n')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
case 'delete': {
|
|
58
|
+
const [key] = rest
|
|
59
|
+
if (!key) {
|
|
60
|
+
process.stderr.write('✖ Usage: openjs-cna-experimental config delete <key> [--project]\n')
|
|
61
|
+
process.exit(64)
|
|
62
|
+
}
|
|
63
|
+
const removed = deleteConfigValue(key, { scope })
|
|
64
|
+
if (removed) cli.ok(`Deleted ${key} from ${scope} config`)
|
|
65
|
+
else cli.warn(`Key '${key}' was not present in ${scope} config`)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
process.stderr.write(`✖ Unknown config subcommand: ${subverb ?? '(none)'}\n`)
|
|
70
|
+
printHelp()
|
|
71
|
+
process.exit(64)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function printHelp () {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`openjs-cna-experimental config <subcommand> [options]
|
|
78
|
+
|
|
79
|
+
Subcommands:
|
|
80
|
+
set <key> <value> write a value (plaintext at ~/.config/openjs-cna/config.json,
|
|
81
|
+
file mode 0600). For tokens you don't want on disk, use the
|
|
82
|
+
OPENJS_CNA_TOKEN environment variable instead.
|
|
83
|
+
get <key> read a value
|
|
84
|
+
list print the merged config (secrets redacted)
|
|
85
|
+
delete <key> remove a value
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--project operate on the project config (./.openjs-cna.json)
|
|
89
|
+
default is the global config (~/.config/openjs-cna/config.json)
|
|
90
|
+
`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// openjs-cna-experimental convert <advisory-url> [--cve <CVE-...>]
|
|
2
|
+
//
|
|
3
|
+
// Reads a public GitHub Security Advisory and emits a v5.2 CNA Container
|
|
4
|
+
// draft on stdout. Two URL shapes accepted: per-repository and global-
|
|
5
|
+
// database. Pre-fetched JSON can be piped via stdin (`openjs-cna-experimental convert -`).
|
|
6
|
+
// When `--cve` is provided, the produced container carries
|
|
7
|
+
// `cveMetadata.cveId` so a downstream publish/update can cross-check the
|
|
8
|
+
// id against its CLI argument.
|
|
9
|
+
|
|
10
|
+
import { parseComponentArgs, buildCli, readFileOrStdin } from '../lib/component-utils.js'
|
|
11
|
+
import { convertGhsaToRecord } from '../lib/convert/ghsa.js'
|
|
12
|
+
|
|
13
|
+
export const command = 'convert'
|
|
14
|
+
export const describe = 'Convert a GitHub Security Advisory into a v5.2 CNA Container draft.'
|
|
15
|
+
|
|
16
|
+
export async function handler (argv) {
|
|
17
|
+
const parsed = parseComponentArgs(argv, {
|
|
18
|
+
cve: { type: 'string' }
|
|
19
|
+
}, { allowPositionals: true })
|
|
20
|
+
if (parsed.values.help) return printHelp()
|
|
21
|
+
|
|
22
|
+
const cli = buildCli(parsed)
|
|
23
|
+
const positional = parsed.positionals[0]
|
|
24
|
+
|
|
25
|
+
let source
|
|
26
|
+
if (positional === '-') {
|
|
27
|
+
// Read raw GHSA JSON from stdin and pass through as a pre-fetched object.
|
|
28
|
+
const raw = await readFileOrStdin('-')
|
|
29
|
+
try { source = JSON.parse(raw) } catch (e) {
|
|
30
|
+
process.stderr.write(`✖ stdin is not valid JSON: ${e.message}\n`)
|
|
31
|
+
process.exit(64)
|
|
32
|
+
}
|
|
33
|
+
} else if (typeof positional === 'string' && positional.length > 0) {
|
|
34
|
+
source = positional
|
|
35
|
+
} else {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
'✖ Missing advisory URL. Usage:\n' +
|
|
38
|
+
' openjs-cna-experimental convert <advisory-url>\n' +
|
|
39
|
+
' openjs-cna-experimental convert - (read raw GHSA JSON from stdin)\n'
|
|
40
|
+
)
|
|
41
|
+
process.exit(64)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
cli.info('Converting → CVE Record')
|
|
45
|
+
const record = await convertGhsaToRecord({ source, cveId: parsed.values.cve })
|
|
46
|
+
cli.ok('Conversion complete')
|
|
47
|
+
process.stdout.write(JSON.stringify(record, null, 2) + '\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printHelp () {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`openjs-cna-experimental convert <advisory-url>
|
|
53
|
+
|
|
54
|
+
Convert a GitHub Security Advisory into a v5.2 CNA Container draft. The
|
|
55
|
+
result is the CNA-container portion of a CVE Record (cveMetadata is
|
|
56
|
+
supplied by 'openjs-cna-experimental publish' at publish time). Output is a starting
|
|
57
|
+
point — run 'openjs-cna-experimental review' on it before publish.
|
|
58
|
+
|
|
59
|
+
This tool only reads PUBLIC published advisories. Both URL shapes hit
|
|
60
|
+
api.github.com anonymously.
|
|
61
|
+
|
|
62
|
+
Input shapes:
|
|
63
|
+
|
|
64
|
+
openjs-cna-experimental convert https://github.com/<owner>/<repo>/security/advisories/<GHSA>
|
|
65
|
+
Per-repository advisory URL. Resolves via
|
|
66
|
+
GET https://api.github.com/repos/<owner>/<repo>/security-advisories/<GHSA>.
|
|
67
|
+
|
|
68
|
+
openjs-cna-experimental convert https://github.com/advisories/<GHSA>
|
|
69
|
+
Global advisory database URL. Resolves via
|
|
70
|
+
GET https://api.github.com/advisories/<GHSA>.
|
|
71
|
+
|
|
72
|
+
openjs-cna-experimental convert -
|
|
73
|
+
Read raw GHSA advisory JSON from stdin (for pre-fetched payloads
|
|
74
|
+
or private/draft advisories the operator has fetched themselves).
|
|
75
|
+
|
|
76
|
+
References for the CNA Container are derived from the advisory's
|
|
77
|
+
\`references[]\` array. Put your vendor-advisory URL there before
|
|
78
|
+
converting.
|
|
79
|
+
`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// openjs-cna-experimental get <cve-id>
|
|
2
|
+
// Fetches the v5.2 container for a published CVE from MITRE via the CNA API.
|
|
3
|
+
|
|
4
|
+
import { parseComponentArgs, buildClient, buildCli } from '../lib/component-utils.js'
|
|
5
|
+
|
|
6
|
+
export const command = 'get'
|
|
7
|
+
export const describe = 'Fetch the CNA Container for a published CVE.'
|
|
8
|
+
|
|
9
|
+
export async function handler (argv) {
|
|
10
|
+
const parsed = parseComponentArgs(argv, {}, { allowPositionals: true })
|
|
11
|
+
if (parsed.values.help) return printHelp()
|
|
12
|
+
const cveId = parsed.positionals[0]
|
|
13
|
+
if (!cveId) {
|
|
14
|
+
process.stderr.write('✖ Missing CVE id. Usage: openjs-cna-experimental get <CVE-...>\n')
|
|
15
|
+
process.exit(64)
|
|
16
|
+
}
|
|
17
|
+
const cli = buildCli(parsed)
|
|
18
|
+
const client = buildClient(parsed)
|
|
19
|
+
cli.info(`Fetching ${cveId}…`)
|
|
20
|
+
const record = await client.getCve(cveId, {
|
|
21
|
+
onPoll: (run) => { if (run.status !== 'completed') cli.info(`run status: ${run.status}`) }
|
|
22
|
+
})
|
|
23
|
+
cli.ok(`Got ${cveId}`)
|
|
24
|
+
process.stdout.write(JSON.stringify(record, null, 2) + '\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function printHelp () {
|
|
28
|
+
process.stderr.write(
|
|
29
|
+
`openjs-cna-experimental get <cve-id>
|
|
30
|
+
|
|
31
|
+
Fetch a published CVE's v5.2 container from MITRE. Prints the container
|
|
32
|
+
JSON on stdout.
|
|
33
|
+
`
|
|
34
|
+
)
|
|
35
|
+
}
|