@ulisesgascon/openjs-cna-tools 0.0.0-stage → 1.0.0-alpha.1

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 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,204 @@
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 update <cve-id> -f container.json` | Update a published CVE record. Same review semantics as `publish`. |
33
+ | `openjs-cna-experimental get <cve-id>` | Fetch a published CVE's container from MITRE. |
34
+ | `openjs-cna-experimental list` | List every CVE id in the bucket. |
35
+ | `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. |
36
+ | `openjs-cna-experimental review <container.json>` | Run the offline reviewer against a container. Defaults to showing a preview + issues. Exits non-zero on blockers. |
37
+ | `openjs-cna-experimental preview <container.json>` | Print a compact summary of a container — title, description, affected, CVSS, CWE, credits, refs, etc. |
38
+ | `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. |
39
+ | `openjs-cna-experimental health` | Unauthenticated liveness check against the CNA API. |
40
+ | `openjs-cna-experimental config` | Read / write `openjs-cna-experimental` configuration. |
41
+
42
+ Run `openjs-cna-experimental <command> --help` for command-specific options.
43
+
44
+ ## Usage
45
+
46
+ ### Install
47
+
48
+ ```sh
49
+ pnpm add -g @ulisesgascon/openjs-cna-tools
50
+ # or
51
+ npm install -g @ulisesgascon/openjs-cna-tools
52
+ ```
53
+
54
+ To build from source:
55
+
56
+ ```sh
57
+ git clone https://github.com/UlisesGascon/openjs-cna-tools.git
58
+ cd openjs-cna-tools
59
+ pnpm install
60
+ pnpm link --global
61
+ ```
62
+
63
+ 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.
64
+
65
+ ### Setting up CNA credentials
66
+
67
+ `openjs-cna-experimental` needs two values to talk to the CNA API:
68
+
69
+ - `openjs_cna_token` — bearer in `bucket:secret` form. Your CNA coordinator issues this.
70
+ - `openjs_cna_base_url` — the API base URL (e.g. `https://cna.openjsf.org`).
71
+
72
+ Configure them with `openjs-cna-experimental config`:
73
+
74
+ ```sh
75
+ openjs-cna-experimental config set openjs_cna_token "<bucket>:<hex-secret>"
76
+ openjs-cna-experimental config set openjs_cna_base_url "https://cna.openjsf.org"
77
+ ```
78
+
79
+ > **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:
80
+ >
81
+ > ```sh
82
+ > export OPENJS_CNA_TOKEN="<bucket>:<hex-secret>"
83
+ > export OPENJS_CNA_BASE_URL="https://cna.openjsf.org"
84
+ > ```
85
+
86
+ Configuration is layered. Precedence (highest first):
87
+
88
+ 1. CLI flag (`--token`, `--base-url`)
89
+ 2. Environment (`OPENJS_CNA_*`)
90
+ 3. Project config (`./.openjs-cna.json` in CWD)
91
+ 4. Global config (`~/.config/openjs-cna/config.json` or `$XDG_CONFIG_HOME/openjs-cna/config.json`)
92
+
93
+ ### Protecting your credentials
94
+
95
+ 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):
96
+
97
+ ```
98
+ # openjs-cna-tools project-scope config
99
+ .openjs-cna.json
100
+ ```
101
+
102
+ 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.
103
+
104
+ If you ever commit a token, revoke it with your CNA coordinator and request a new one.
105
+
106
+ ### Typical workflow
107
+
108
+ ```sh
109
+ # 1. Reserve a CVE id for an upcoming release.
110
+ CVE=$(openjs-cna-experimental reserve)
111
+ echo "Reserved $CVE"
112
+
113
+ # 2. Draft a container from a published GitHub Security Advisory.
114
+ # `--cve` stamps cveMetadata.cveId into the container so step 5 can
115
+ # cross-check the CLI argument against it.
116
+ openjs-cna-experimental convert https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx \
117
+ --cve "$CVE" > container.json
118
+
119
+ # 3. (Optional) Editorial polish via an LLM. Pipes a prompt to your
120
+ # clipboard; paste it plus container.json into the LLM of your choice.
121
+ openjs-cna-experimental llm-review-prompt container.json | pbcopy
122
+
123
+ # 4. Review before you publish. The output renders a preview of the
124
+ # container plus the issues list (tip: edit visually at
125
+ # https://vulnogram.org/#source).
126
+ openjs-cna-experimental review container.json
127
+
128
+ # 5. Publish. Review runs again automatically (strict, warnings block);
129
+ # if `container.cveMetadata.cveId` is set, it must match "$CVE" or
130
+ # publish aborts before sending anything to MITRE.
131
+ openjs-cna-experimental publish "$CVE" -f container.json
132
+ ```
133
+
134
+ Useful flags:
135
+
136
+ - `--no-strict` (on `review`, `publish`, `update`): let warnings pass as warnings. True blockers still abort.
137
+ - `--skip-review` (on `publish`, `update`): bypass the reviewer entirely. Discouraged.
138
+ - `--no-preview` (on `review`): drop the container summary; print issues only. Useful in CI.
139
+ - `--tier mitre-common|openjs-extra` (on `review`): filter the printed issues to a single tier without changing the exit code.
140
+ - `--yes` / `OPENJS_CNA_YES=1`: accept confirmation prompts non-interactively.
141
+
142
+ ### Troubleshooting
143
+
144
+ - **Review reports schema blockers**: paste the container into <https://vulnogram.org/#source> to edit it visually; rerun `openjs-cna-experimental review` when done.
145
+ - **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.
146
+ - **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.)
147
+
148
+ ## Library
149
+
150
+ The same module powers the CLI and is importable for automation:
151
+
152
+ ```js
153
+ import { CnaClient } from '@ulisesgascon/openjs-cna-tools/client'
154
+ import { reviewRecord } from '@ulisesgascon/openjs-cna-tools/review'
155
+ import { convertGhsaToRecord } from '@ulisesgascon/openjs-cna-tools/convert'
156
+
157
+ const cna = new CnaClient({
158
+ token: process.env.OPENJS_CNA_TOKEN,
159
+ baseUrl: process.env.OPENJS_CNA_BASE_URL
160
+ })
161
+
162
+ const { cveId } = await cna.reserveCve()
163
+
164
+ // `source` is a published GitHub advisory URL. References (including any
165
+ // vendor-advisory blog post you want surfaced on the CVE record) come
166
+ // from the advisory's own `references[]` — set them on GitHub, not via
167
+ // any per-call override here. Pass `cveId` to stamp it into `cveMetadata`.
168
+ const record = await convertGhsaToRecord({
169
+ source: 'https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx',
170
+ cveId
171
+ })
172
+
173
+ // reviewRecord is strict by default — warnings count as blockers.
174
+ // Pass `{ strict: false }` to surface warnings as warnings.
175
+ const report = reviewRecord(record)
176
+ if (report.summary.blockers > 0) {
177
+ throw new Error('record is not ready to publish')
178
+ }
179
+
180
+ await cna.publishCve(cveId, record.containers.cna)
181
+ ```
182
+
183
+ Error classes (`CnaAuthError`, `CnaTimeoutError`, `CnaOperationError`, `CnaReviewBlockedError`) are exported from the package root.
184
+
185
+ ## Design
186
+
187
+ - **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.
188
+ - **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.
189
+ - **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.
190
+ - **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>.
191
+ - **Strict by default.** `review`, `publish`, and `update` treat warnings as blockers. Pass `--no-strict` only when you understand the warning is acceptable.
192
+ - **CVE-id consistency.** When `container.cveMetadata.cveId` is set and the CLI argument differs, `publish` and `update` abort before touching MITRE.
193
+ - **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.
194
+ - **GHSA descriptions are Markdown.** The converter declares `descriptions[].supportingMedia[]` with `type: 'text/markdown'` so the formatting isn't lost on the way to MITRE.
195
+ - **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.
196
+ - **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.
197
+ - **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.
198
+ - **The constructor does no I/O.** `new CnaClient()` makes zero network calls. Credentials are validated lazily on the first operation that needs them.
199
+
200
+ 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.
201
+
202
+ ## License
203
+
204
+ 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
+ }
@@ -0,0 +1,30 @@
1
+ // openjs-cna-experimental health
2
+ // Unauthenticated liveness check against the configured CNA API.
3
+
4
+ import { parseComponentArgs, buildClient, buildCli } from '../lib/component-utils.js'
5
+
6
+ export const command = 'health'
7
+ export const describe = 'Check that the CNA API is reachable.'
8
+
9
+ export async function handler (argv) {
10
+ const parsed = parseComponentArgs(argv)
11
+ if (parsed.values.help) return printHelp()
12
+ const cli = buildCli(parsed)
13
+ const client = buildClient(parsed)
14
+ const status = await client.health()
15
+ cli.ok(`API is reachable (${status?.status ?? 'ok'})`)
16
+ process.stdout.write(JSON.stringify(status, null, 2) + '\n')
17
+ }
18
+
19
+ function printHelp () {
20
+ process.stderr.write(
21
+ `openjs-cna-experimental health
22
+
23
+ Probe the configured CNA API's /health endpoint. Useful to catch base-url
24
+ misconfiguration before burning a workflow dispatch.
25
+
26
+ Usage:
27
+ openjs-cna-experimental health [global flags]
28
+ `
29
+ )
30
+ }
@@ -0,0 +1,36 @@
1
+ // openjs-cna-experimental list
2
+ // Lists all CVE ids in the bucket. One id per line by default; --json for array.
3
+
4
+ import { parseComponentArgs, buildClient, buildCli } from '../lib/component-utils.js'
5
+
6
+ export const command = 'list'
7
+ export const describe = 'List all CVEs in the bucket.'
8
+
9
+ export async function handler (argv) {
10
+ const parsed = parseComponentArgs(argv)
11
+ if (parsed.values.help) return printHelp()
12
+ const cli = buildCli(parsed)
13
+ const client = buildClient(parsed)
14
+ cli.info('Listing CVEs…')
15
+ const ids = await client.listCves({
16
+ onPoll: (run) => { if (run.status !== 'completed') cli.info(`run status: ${run.status}`) }
17
+ })
18
+ cli.ok(`Found ${ids.length} CVE id(s)`)
19
+ if (parsed.values.json) {
20
+ process.stdout.write(JSON.stringify(ids, null, 2) + '\n')
21
+ } else {
22
+ for (const id of ids) process.stdout.write(`${id}\n`)
23
+ }
24
+ }
25
+
26
+ function printHelp () {
27
+ process.stderr.write(
28
+ `openjs-cna-experimental list
29
+
30
+ List every CVE id in the bucket. Default output: one id per line.
31
+
32
+ Options:
33
+ --json emit a JSON array instead
34
+ `
35
+ )
36
+ }