Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/git-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,37 @@ $ ncu-config --global set h1_username $H1_TOKEN
access.
- `h1_username`: HackerOne API Token username.

#### Optional: reserve CVEs through the OpenJS Foundation CNA

By default `--request-cve` reserves CVE ids through HackerOne, which acts as
the Node.js CNA. Set `cve_source` to `openjs-cna` to reserve them through the
OpenJS Foundation CNA instead. HackerOne is still used for the bug-bounty
workflow (triage, `--sync`, disclosure) and is updated with the resulting CVE
id, so reports stay in sync.

```console
$ ncu-config --global set cve_source openjs-cna
$ ncu-config --global set -x openjs_cna_token
$ ncu-config --global set openjs_cna_worker_url https://<deployment>.workers.dev
```

- `cve_source`: `hackerone` (default) or `openjs-cna`.
- `openjs_cna_token`: **secret**, store it encrypted with `-x`. The bucket
token issued by the OpenJS CNA coordinators, in `<bucket>:<secret>` form
(for example `prod_nodejs:<256 hex chars>`).
- `openjs_cna_worker_url`: the OpenJS CNA API base URL.
- `openjs_cna_timeout`: optional, seconds to wait for a reservation before
pausing (default `600`).

Reservations are asynchronous. The command saves the correlation id of each
dispatched reservation to `vulnerabilities.json` (`pendingCve`) before
waiting, so an interrupted or timed-out run never loses a reserved id. On a
supervised bucket the reservation waits for an OpenJS CNA coordinator to
approve it, which can exceed the timeout: re-run
`git node security --request-cve` to resume the pending reservations.

To revert to HackerOne, delete `cve_source` or set it back to `hackerone`.

### `git node security --start`

This command prepares `vulnerabilities.json` and can open the Next Security
Expand Down
29 changes: 29 additions & 0 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,35 @@ async function auth(
const h1 = encode(h1_username, h1_token);
setOwnProperty(result, 'h1', h1);
return h1;
},

// OpenJS Foundation CNA bucket token and Worker URL. Only used when
// `cve_source` is `openjs-cna`; HackerOne stays the bug-bounty source.
get cna() {
const { openjs_cna_token, openjs_cna_worker_url } = getMergedConfig();
if (!openjs_cna_token || !openjs_cna_worker_url) {
throw new Error(
'OpenJS CNA credentials are not configured. Run:\n' +
' ncu-config --global set -x openjs_cna_token <bucket:secret>\n' +
' ncu-config --global set openjs_cna_worker_url <https://...workers.dev>'
);
}
if (!/^[a-z0-9_]+:[0-9a-f]+$/i.test(openjs_cna_token)) {
throw new Error(
'openjs_cna_token is misformatted; expected `<bucket>:<hex-secret>`'
);
}
if (!/^https:\/\//.test(openjs_cna_worker_url)) {
throw new Error(
'openjs_cna_worker_url is misformatted; expected an https:// URL'
);
}
const cna = {
token: openjs_cna_token,
worker_url: openjs_cna_worker_url.replace(/\/+$/, '')
};
setOwnProperty(result, 'cna', cna);
return cna;
}
};
if (options.github) {
Expand Down
83 changes: 83 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,87 @@ export default class Request {

return all;
}

// ---------------------------------------------------------------------------
// OpenJS Foundation CNA
//
// The Worker is a thin edge in front of `workflow_dispatch`. POST /dispatch
// returns a correlation id; GET /runs/{id} polls until the backing workflow
// run completes and surfaces the operation result.
// ---------------------------------------------------------------------------

cnaHeaders(extra = {}) {
const { token } = this.credentials.cna;
return {
Authorization: `Bearer ${token}`,
'User-Agent': 'node-core-utils',
Accept: 'application/json',
...extra
};
}

async cnaDispatch(operation, inputs = {}) {
const { worker_url } = this.credentials.cna;
const url = `${worker_url}/dispatch`;
const options = {
method: 'POST',
headers: this.cnaHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ operation, inputs })
};
const response = await this.json(url, options);
if (response.error) {
throw new Error(
`OpenJS CNA dispatch failed (${operation}): ${response.error}`
);
}
if (!response.correlation_id) {
throw new Error(
`OpenJS CNA dispatch (${operation}) returned no correlation_id`
);
}
return response;
}

async cnaPoll(correlationId) {
const { worker_url } = this.credentials.cna;
const url = `${worker_url}/runs/${correlationId}`;
const options = {
method: 'GET',
headers: this.cnaHeaders()
};
const response = await this.json(url, options);
if (response.error) {
throw new Error(
`OpenJS CNA poll failed (${correlationId}): ${response.error}`
);
}
return response;
}

// Polls /runs/{id} until the run reports status === 'completed'. Throws a
// `CnaTimeoutError` when the deadline passes (the run keeps going on the
// server, so callers should persist the correlation id and resume later),
// and a plain Error on a `conclusion` other than 'success'.
async cnaWaitForCompletion(correlationId, { timeoutMs = 600_000, intervalMs = 5_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const run = await this.cnaPoll(correlationId);
if (run.status === 'completed') {
if (run.conclusion !== 'success') {
throw new Error(
`OpenJS CNA run ${correlationId} concluded with ` +
`'${run.conclusion}'. See ${run.url} for details.`
);
}
return run;
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
const error = new Error(
`OpenJS CNA run ${correlationId} did not complete within ${timeoutMs}ms`
);
error.name = 'CnaTimeoutError';
error.correlationId = correlationId;
throw error;
}
}
Loading
Loading