- name
- generate-cve-json
- description
- Generate a CVE 5.x JSON document from an <tracker> tracking
issue, ready to paste into the Vulnogram `#source` tab of the ASF CVE tool
at https://cveprocess.apache.org/cve5/<CVE-ID>#source. The conversion is
deterministic: same issue in, same JSON bytes out. Handles multiple
credits (one per line) and multiple references (URLs extracted from the
issue's "Public advisory URL" and "PR with the fix" fields; the
"Security mailing list thread" field is treated as internal-only and
never exported).
- when_to_use
- Invoke when a security team member says "generate CVE JSON for NNN",
"update the CVE tool entry for NNN", "paste-ready CVE for NNN", or is
about to publish the advisory for a tracking issue and wants the CVE
record filled in from the issue body in one paste-and-save step. Not
appropriate before the CVE has been allocated (the script needs a CVE
ID either from the issue body's `CVE tool link` field or from a
`--cve-id` override).
# generate-cve-json
This skill produces a CVE 5.x JSON document from a tracking issue in
[`<tracker>`](https://github.com/<tracker>), ready to
paste into the Vulnogram **"#source"** tab of the ASF CVE tool. The goal is
to eliminate the manual "copy each field from the issue into the right
Vulnogram form input" step when you are preparing to publish an advisory.
> **Project-agnostic by design.** All project-specific values
> (vendor, top-level product / package name, project display map,
> CNA org id, generator tag, …) are loaded from a TOML config the
> adopting project ships at `<project-config>/tools/cve-tool-vulnogram/cve-json-config.toml`.
> Concrete `apache-foo-project-*` strings appearing in this
> document are **illustrative examples** of how a project with a
> project-style package layout would configure things; replace
> them mentally with the adopter's own package taxonomy. The
> schema is documented in the package [README](README.md).
**Golden rule:** the script generates a *proposal* JSON document. It
parses a handful of structured fields from the issue body, but it cannot
read the security team member's mind. Always review the generated JSON
before pasting, and always do the final review inside Vulnogram before
moving the CVE from DRAFT → REVIEW → READY → PUBLIC.
**Release-vote gating (opt-in, recommended for ASF projects).** The
emitted `CNA_private.state` follows a tri-state state machine:
- `DRAFT` — the CNA is incomplete *or* the project has opted into
release-vote gating and no vote is in progress yet.
- `REVIEW` — the CNA is review-ready (CVE ID + title + description +
affected versions + CWE + severity + ≥ 1 credit + ≥ 1 reference)
*and* either the project hasn't opted into gating (legacy: ready ⇒
REVIEW) or an RC vote is in progress (signalled by the configured
tracker label or a `--review` CLI flag).
- `PUBLIC` — the CNA is review-ready *and* the public advisory has
shipped (a `vendor-advisory` reference is present).
Projects opt into gating by setting `[workflow].release_vote_gating
= true` in their `cve-json-config.toml` and choosing the label name
via `[workflow].rc_voting_label` (default `"rc voting"`). The sync
skill is responsible for detecting [VOTE] threads on the project's
dev list (e.g. `dev@<project>.apache.org`) and proposing the label
add/remove; the generator only reads the label on the tracker. Non-
ASF adopters who publish advisories without a separate release-vote
step typically leave gating off — the legacy "ready ⇒ REVIEW"
behaviour is the right default for that workflow.
**Determinism:** the same input issue body produces exactly the same JSON
bytes on every run. The script uses only the Python standard library, has
no timestamps or machine-dependent values in its output, sorts JSON keys,
and sorts references alphabetically. This lets you paste the result into
Vulnogram, tweak fields in the tool, re-run the script later, and cleanly
diff the two to see what the tool has added / what you changed by hand.
---
## Inputs
- **Issue number** (required) — e.g. `232`.
- Optional CLI overrides:
- `--cve-id CVE-YYYY-NNNN+` — override the CVE ID if the issue body's
`CVE tool link` field has not yet been filled in, or retarget the
JSON to a different CVE ID.
- `--title "<vendor>: <product>: …"` — override the CVE title.
Default is the GitHub issue title with the project's
`<vendor>: <product>:` prefix (sourced from the TOML config) when
it does not already start with that phrase.
- `--version-start X.Y.Z` — override the start of the affected version
range (the `affected[].versions[].version` field). Default is the
lower bound parsed from the Affected versions field when it uses
`>= X, < Y` syntax, otherwise `"0"`.
- `--remediation-developer "Name"` — append a `type: "remediation
developer"` credit on top of whatever the body's *Remediation
developer* field already lists (auto-populated by the
`security-issue-sync` skill from the linked PR's author). Repeat
the flag to add multiple developers; duplicates between the
body field and CLI flags are dropped silently. The reporter
credit(s) from the *Reporter credited as* field are always
emitted with `type: "finder"`.
- `--vendor` / `--product` / `--package-name` / `--collection-url` —
override the product identity fields. The defaults come from the
project's TOML config (`product.vendor`, `product.default_product`,
`product.default_package_name`, `product.default_collection_url`).
They are used as the identity for *Affected versions* lines that
don't start with a recognisable per-package directory name
(see the multi-product note below). `--collection-url` reaches the
record only through the purl it is used to derive (see
`product.purl_type` below) — except for a product that opts out of
purls, the one case a record still carries a `collectionURL`.
- **Helm charts — `purl_type = "helm"`, and you must know where the
chart is published.** A chart is identified by its repository, not by
its name: two projects may both ship a chart called `superset`. So the
generator emits
```text
pkg:helm/<chart>?repository_url=<base URL>
```
taking the base URL from `product.default_collection_url` (or the
package's `collection_url` override), and **refuses to emit a purl at
all without one** rather than producing a chart identifier that matches
somebody else's chart.
**If the base URL is not recorded anywhere, ask whoever publishes the
chart.** It is whatever a user would put in `helm repo add` — the
`https://…` site serving `index.yaml`, or the `oci://…` registry path
for a chart published as an OCI artifact. Either is accepted verbatim;
a trailing slash is dropped so the same repository does not produce two
identifiers. Do not infer it from the project's homepage: charts are
routinely served from a different host than the project site.
Note that `helm` is **not** one of the types the purl specification
registers. It is used because the scanners that would match this
advisory to a deployed chart emit it, and because the two spec-correct
encodings — `pkg:oci/` for an OCI registry and `pkg:generic/` for a
classic repository — would give the same chart two different identities
depending on how it happened to be published. A strict validator may
object to the type; being invisible to every scanner is the worse
outcome.
- `product.purl_type` *(config only)* — the Package URL type
for the project's packages (`pypi`, `npm`, `cargo`, …). Every
`affected[]` entry carries a `packageURL` (`pkg:<type>/<packageName>`).
Per the CVE Record Format the purl never includes a version — the
entry's `versions[]` carries the range.
**The purl is the only package identifier the record carries.** The
ASF CVE tool treats the Package URL as the recommended identifier and
derives the legacy `collectionURL` / `packageName` pair from it when
the record is serialised for publication, so the generator emits the
purl alone. Writing the pair as well would hand the tool two
identifiers that can disagree — which it reports rather than
silently reconciles.
**A purl is required, not optional.** A record needs one to be
promoted, so the generator refuses to emit a record it cannot build
a purl for rather than producing one the CNA will reject later.
Leaving the key unset is fine when the type can be **derived from
`product.default_collection_url`** — PyPI, npm, crates.io,
RubyGems and NuGet hosts are recognised — and only hosts whose type
is unambiguous are mapped, because a guessed type yields a
valid-looking identifier pointing at the wrong ecosystem.
Set `purl_type = "none"` to state deliberately that a product has no
package host — a source-only release published to `dist.apache.org`
and nowhere else, for instance. That is an explicit decision rather than an omission, which
is the distinction the previous optional behaviour lost. Those
entries — and only those — carry `collectionURL` / `packageName`
instead, because there is no purl for the CVE tool to derive them
from.
Name normalisation follows the package-url spec per type, and is
implemented for the types whose rules have been read from it:
`pypi` (lowercased, `_` becomes `-`) and `npm` (scope becomes the
namespace, so `@angular/animation` renders as
`pkg:npm/%40angular/animation`; names keep their case, since
mixed-case npm packages were grandfathered in). Any other type has
its name passed through unchanged, and a name needing namespace
semantics is covered by `product.purl_namespace` below, and without
one such a type is an error rather than a guessed purl.
- `product.purl_namespace` *(optional, config only)* — the namespace
for purl types that require one: a Maven `groupId`, a Composer
vendor, a Go module prefix. Types the spec gives no namespace
(`pypi`, `cargo`, `gem`, `nuget`) ignore it, and an npm scope
carried in the package name itself wins over it. A type that
requires a namespace and has none is an error naming this key — a
guessed Maven groupId would point at another organisation's artifact,
and silently omitting the purl produces an unpromotable record.
- `[packages.overrides."<packageName>"]` *(config only)* — per-package
distribution identity, for a project that ships to more than one
ecosystem. Accepts `product`, `collection_url` and `purl_type`; anything
omitted falls through to the `product.*` default, so a package that
differs only in purl type need not restate its collection URL.
The case this exists for: a project whose packages are on PyPI **and**
which also ships, say, a Helm chart from its own site. `product.*`
describes only the majority ecosystem, so without an override the
odd-one-out inherits it and the record claims `pkg:pypi/<chart>` — a
package that host does not carry. Since purls are what scanners match
on, a wrong one is acted upon, unlike the wrong free-text product name
it replaced.
```toml
[packages.overrides."apache-example-helm-chart"]
product = "Apache Example Helm Chart"
collection_url = "https://example.apache.org/"
purl_type = "none"
```
The key is the resolved `packageName`, so the package must be one the
configured `package_pattern` matches — extending that pattern is a
prerequisite, not an extra. `--product-for` still wins over the
`product` set here.
- `--product-for PACKAGE=PRODUCT` — override the CVE product display
name for a specific `packageName`. Repeat to override multiple
packages. Useful when a package is not in the project's
`project_display_map` config, or when an acronym needs different
casing from the title-cased fallback. Example:
`--product-for apache-foo-project-baz='Apache Foo Project Baz'`.
- `--org-id <uuid>` — override the CNA assigner org id (defaults to
the ASF org id).
- `--discovery <word>` — override `source.discovery` (default
`"UNKNOWN"`; valid CVE 5.x values include `UNKNOWN`, `INTERNAL`,
`EXTERNAL`, `USER`).
- `--no-envelope` — emit only the inner `cna` container instead of
the full CVE 5.x record (envelope is the default).
- `--review` / `--draft` (mutually exclusive) — force the emitted
`CNA_private.state` to `REVIEW` or `DRAFT` regardless of the
tracker's labels. Useful in two cases:
- `--review` lets a release manager nudge a record forward by
hand when the `rc voting` label is not yet set on the tracker.
- `--draft` walks a record back when an RC vote was cancelled or
failed and the label is still around.
Both flags only matter when release-vote gating is enabled in
the project's TOML config (see below); otherwise the state is
derived from the CNA's readiness alone and these flags have no
effect beyond what the legacy logic produces.
- `--attach` — after generating the JSON, embed it at the end of
the tracking issue's **body** (after the *CVE tool link* field),
wrapped in a collapsible `<details>` block. The block is bracketed
by HTML-comment markers
(``<!-- generate-cve-json: cve=CVE-YYYY-NNNN+ version=v1 -->`` …
``<!-- generate-cve-json:end cve=CVE-YYYY-NNNN+ version=v1 -->``)
that the script uses on later runs to find the existing block and
**replace it in place**, so re-runs update the embedded attachment
instead of duplicating it or breaking other body fields. The
attachment lives in the body — not as a comment — so it stays
above every status-change comment in the timeline (effectively
"pinned" without needing any pin mechanism). Requires the
positional issue argument; incompatible with `--stdin`.
---
## Prerequisites on the tracking issue
For the generated JSON to be useful, the issue body should already be
filled in through a prior `security-issue-sync` run. In particular:
- **Short public summary for publish** — becomes the CVE description.
- **Affected versions** — becomes the CVE `affected[]` list. The script
understands the common version-expression shapes (`< 3.2.2`,
`>= 2.0.0, < 3.2.2`, `<= 3.2.1`, a bare version like `3.1.5`, and a
bare lower bound like `>= 2.0.0`).
**Multi-product CVEs are supported** — put one package per line,
prefixing each with the package directory name as it appears in
the adopter's repo, and the script emits one `affected[]` entry
per line with the right `product` and package identity. Example
(illustrative — using a hypothetical `apache-foo` project's
sub-project layout):
apache-foo-project-alpha <=6.5.0
apache-foo-project-beta <=1.9.0
Known package directory names are resolved to the vendor-preferred
display casing via the project's `packages.project_display_map`
config table; unknown packages fall back to title-cased dash-split
and can be overridden with `--product-for`. A line without a
package prefix (or a single-line field) falls back to the
`--product` / `--package-name` defaults, which preserves the
single-product behaviour.
**`< NEXT VERSION` placeholder** — multi-package trackers don't
know which package version will ship the fix until the wave's
release manager picks it during a release cut. Until then, the
*Affected versions* lines use the literal token `NEXT VERSION` as
the upper bound, e.g.:
apache-foo-project-alpha < NEXT VERSION
apache-foo-project-beta < NEXT VERSION
The generator strips `< NEXT VERSION` before parsing each line and
emits a `versions[]` entry without `lessThan` (open-ended upper
bound — *"affected from \<low\> onwards, no fix released yet"*).
When the wave ships and the version is known, the
`security-issue-sync` skill replaces each `NEXT VERSION` with the
actual `< X.Y.Z` and the next regen produces a fully-bounded entry.
Case-insensitive; combines with a lower bound (e.g.
`>= 2.0.0, < NEXT VERSION` becomes `{version: "2.0.0", status: "affected"}`).
- **Security mailing list thread** — internal navigation reference
only; the script **does not** export URLs from this field into
`references[]`. Keep whatever the reporter or triager put there.
- **Public advisory URL** — each URL in this field is extracted and
added to `references[]` with `tags: ["vendor-advisory"]`. Populated
by the release manager (or the `security-issue-sync` skill) once
the advisory is archived on `<users-list>`. The
`--advisory-url` CLI flag still exists for ad-hoc overrides.
- **PR with the fix** — each URL in this field becomes a reference URL.
**Multiple URLs are supported**: paste them on separate lines, as a
bullet list, or comma-separated — the script extracts every
`https?://…` token it finds.
- **Reporter credited as** — each line becomes one CVE credit entry
with `type: "finder"`. **Multiple credits are supported**: put each
person on their own line. `Full Name, Affiliation` on a single line
is treated as **one** credit, not two, so the common
`Jed Cunningham, Astronomer` pattern works as expected. Bullets
(`- `, `* `, `1. `) are stripped. Blank lines are ignored. If you
need to credit many people::
Jed Cunningham
Saurabh Banawar
selen (Huntr bounty 3e88d364-5047-4768-a52c-6568f21ef35b)
- **Remediation developer** — each line becomes one CVE credit entry
with `type: "remediation developer"`. Same parsing rules as
*Reporter credited as* (newline-separated, `Full Name, Affiliation`
is one credit, bullets stripped). Auto-populated by the
`security-issue-sync` skill from the linked PR's author the first
time *PR with the fix* is set; manual edits survive subsequent
syncs (the skill only proposes appending names that aren't already
there). The `--remediation-developer` CLI flag adds further names
on top of whatever the body already lists.
> **Bot / AI credit policy.** This generator is intentionally
> neutral on credit content: whatever a tracker's *Reporter credited
> as* or *Remediation developer* field carries is what lands in
> `credits[]`. The filtering of obvious bot / AI accounts (e.g.
> `dependabot[bot]`, `*-scanner`, `automated-*`) happens **upstream
> in the skills** at extraction time — see
> [`bot-credits-policy.md`](../bot-credits-policy.md) for the
> detection rule and the per-skill enforcement sites. Keeping the
> filter upstream means an intentional human override (typed
> directly into the field) survives every JSON regeneration without
> needing a special bypass flag here.
- **CWE** — `CWE-285: Improper Authorization` style works; so does a bare
`CWE-285` or a plain sentence. The script extracts the `CWE-\d+` token
for the `cweId` field and uses the rest as the human-readable
description.
- **Severity** — `None`, `Low`, `Medium`, `High`, `Critical`
(case-insensitive) are emitted as the text content of a `metrics[].other`
block. Vulnogram lets you replace this with a CVSS vector in its form if
you want a numeric score.
- **CVE tool link** — the ASF CVE tool URL, e.g.
`https://cveprocess.apache.org/cve5/CVE-2026-40913`. The script extracts
the `CVE-YYYY-NNNN+` token from this field. If the field is still
`_No response_`, pass the CVE ID with `--cve-id`.
If one of these fields is missing, the JSON still generates, but the
reviewer will need to fill the gap in Vulnogram. The skill surfaces any
empty field in the proposal so nothing is silently skipped.
---
## Prerequisites
- **`gh` CLI authenticated** with collaborator access to
`<tracker>` — the script reads the tracker via `gh`.
- **`uv` installed** — the script is a small `uv`-managed Python
project and is invoked as `uv run --project
tools/cve-tool-vulnogram/generate-cve-json generate-cve-json <N>`.
See
GitHubで見る