@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -2
  3. package/bin/openjs-cna.js +128 -0
  4. package/components/config.js +92 -0
  5. package/components/convert.js +81 -0
  6. package/components/get.js +35 -0
  7. package/components/health.js +30 -0
  8. package/components/list.js +36 -0
  9. package/components/llm-review-prompt.js +84 -0
  10. package/components/preview.js +42 -0
  11. package/components/publish.js +238 -0
  12. package/components/reserve.js +48 -0
  13. package/components/review.js +170 -0
  14. package/components/update.js +104 -0
  15. package/lib/auth.js +20 -0
  16. package/lib/batch.js +141 -0
  17. package/lib/cli.js +81 -0
  18. package/lib/client.js +244 -0
  19. package/lib/component-utils.js +74 -0
  20. package/lib/config.js +121 -0
  21. package/lib/convert/ghsa.js +450 -0
  22. package/lib/convert/version-range.js +251 -0
  23. package/lib/cvss.js +113 -0
  24. package/lib/errors.js +44 -0
  25. package/lib/index.js +9 -0
  26. package/lib/preview.js +145 -0
  27. package/lib/prompt.js +170 -0
  28. package/lib/record.js +123 -0
  29. package/lib/review/index.js +78 -0
  30. package/lib/review/rules.js +433 -0
  31. package/lib/review/schema.js +99 -0
  32. package/lib/review/schemas/cve-record-format.json +1362 -0
  33. package/lib/review/schemas/imports/cvss/cvss-v2.0.json +195 -0
  34. package/lib/review/schemas/imports/cvss/cvss-v3.0.json +577 -0
  35. package/lib/review/schemas/imports/cvss/cvss-v3.1.json +577 -0
  36. package/lib/review/schemas/imports/cvss/cvss-v4.0.json +784 -0
  37. package/lib/review/schemas/tags/adp-tags.json +7 -0
  38. package/lib/review/schemas/tags/cna-tags.json +7 -0
  39. package/lib/review/schemas/tags/reference-tags.json +27 -0
  40. 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
- # Temporary Holding Version
1
+ # OpenJS CNA Tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@ulisesgascon/openjs-cna-tools.svg?style=flat-square)](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
+ }