<!-- SPDX-License-Identifier: Apache-2.0
     https://www.apache.org/licenses/LICENSE-2.0 -->

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [`tools/cve-tool/`](#toolscve-tool)
  - [Prerequisites](#prerequisites)
  - [What this is](#what-this-is)
  - [Today's adapters](#todays-adapters)
  - [Interface](#interface)
    - [`allocate(scope, fields) to cve_id`](#allocatescope-fields-to-cve_id)
    - [`fetch_current_state(cve_id) to {state, fields}`](#fetch_current_statecve_id-to-state-fields)
    - [`push_update(cve_id, fields, state_transition=None) to diff`](#push_updatecve_id-fields-state_transitionnone-to-diff)
    - [`publish(cve_id) to ok`](#publishcve_id-to-ok)
    - [`retract(cve_id, reason) to ok`](#retractcve_id-reason-to-ok)
  - [Generic state verbs](#generic-state-verbs)
  - [Skills that consume this contract](#skills-that-consume-this-contract)
  - [ASF default — Vulnogram](#asf-default--vulnogram)
  - [Configuration](#configuration)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

<!-- SPDX-License-Identifier: Apache-2.0
     https://www.apache.org/licenses/LICENSE-2.0 -->

# `tools/cve-tool/`

**Capability:** contract:cve-authority

**Kind:** interface

**Vendor:** agnostic

## Prerequisites

- **Runtime:** None — this directory is a Markdown contract spec; no executable code ships here. It is read by the framework, not run.
- **CLIs / credentials / network:** Provided entirely by the resolved adapter — the sibling `tools/cve-tool-<name>/` directory named by `cve_authority.tool` in `project.md` (the ASF default is Vulnogram at `tools/cve-tool-vulnogram/`). See that adapter for its concrete prerequisites.

## What this is

The framework's CVE-tooling adapter contract. Today every CVE-aware
skill in this repository — `security-cve-allocate`,
`security-issue-sync`, `security-issue-invalidate`,
`security-issue-deduplicate` — speaks Vulnogram. They embed
Vulnogram's URL prefix (`cveprocess.apache.org`), Vulnogram's
`#source` / `#json` / `#email` tab shape, and Vulnogram's `DRAFT` →
`REVIEW` to `READY` to `PUBLIC` state machine directly in the
skill prose. That is fine for the ASF — the foundation runs a single
CNA tool and the skills can afford to be Vulnogram-shaped — but it
forecloses every other plausible adopter. A project running CVE.org's
direct submission portal, the MITRE form for non-CNAs, GHSA's own
CNA flow, or no CVE allocation at all has no seam to plug into.
This contract is that seam. It defines the methods every CVE-tool
backend must implement, the lifecycle moments at which the skills
fire each method, and the generic state verbs the skills use to
talk about a CVE record without baking in any one tool's vocabulary.
Skills consume the contract; the adapter consumes the wire protocol
of whichever CNA tool the adopter actually uses.

The contract is read by the framework, not by humans during normal
operation. New adopters declare `cve_authority.tool: <adapter>` in
`projects/<project>/project.md`; the skills resolve that to a
sibling directory under `tools/` and call the methods named here.
Adopters who run Vulnogram inherit the ASF defaults verbatim.

## Today's adapters

Only one CVE-tool adapter ships today:

| Adapter | Directory | Status | Notes |
|---|---|---|---|
| Vulnogram (ASF) | `tools/cve-tool-vulnogram/` | Shipping | The reference implementation. PR4 of the ASF-pluggable security flow renames the directory to `tools/cve-tool-vulnogram/` to match the contract's adapter-naming rule. **Not renamed in this PR.** |
| CVE.org direct submission | `tools/cve-tool-cve-org-direct/` *(planned)* | Placeholder | For adopters who are themselves a CNA and submit records straight to `cve.org` via the CVE Services API rather than through an intermediate CNA tool. |
| MITRE form | `tools/cve-tool-mitre-form/` *(planned)* | Placeholder | For adopters who are not a CNA and request CVE IDs via the [MITRE CVE Request form](https://cveform.mitre.org/). Allocation is asynchronous and operator-mediated; `fetch_current_state` will frequently return `unknown` until the form's email reply arrives. |
| GHSA-as-CNA | `tools/cve-tool-ghsa/` *(planned)* | Placeholder | For adopters who already drive their advisory flow through GitHub Security Advisories (GHSA) and use GitHub as the CNA. `allocate` becomes a GHSA draft create; `publish` becomes a GHSA publish. |
| None | `tools/cve-tool-none/` *(planned)* | Placeholder | The disable-CVE-allocation backend. All methods are no-ops returning a synthetic `not-applicable` result. For adopters who triage security issues but do not allocate CVEs (e.g. an internal product with no public distribution). |

The four placeholder adapters do not exist on disk yet; they exist
in this table so the contract can describe the surface they will fit
into when an adopter actually needs one. PR4 of the ASF-pluggable
security flow will create `tools/cve-tool-vulnogram/` (the rename
target) but will not pre-create the other three — those land when an
adopter implements them.

## Interface

Every CVE-tool adapter exposes five methods. The names here are the
generic verbs the skills use; an adapter is free to name its internal
CLIs whatever fits its tool's vocabulary, as long as the skill-facing
surface uses these names.

### `allocate(scope, fields) to cve_id`

Reserve a CVE ID and create the initial record.

- **Lifecycle moment.** Fires once per tracking issue, inside
  `security-cve-allocate` after the security team has agreed the
  report is valid and a CVE should be allocated. The pre-allocation
  consensus discussion happens on the tracker; `allocate` is the
  step that turns that consensus into a reserved ID.
- **Inputs.**
  - `scope` — a string identifying the project scope under which
    the CVE belongs. Concretely for ASF: the scope label
    (`affects: airflow` / `affects: providers` / `affects: chart`)
    drives which `vendor` / `product` / `packageName` triad the
    adapter writes into the initial record. For other adopters this
    is whatever string their CVE tool needs to route the allocation
    to the right CNA pool.
  - `fields` — a dict of the tracker's body fields at the moment
    allocation fires: `title`, `description`, `affected_versions`,
    `cwe`, `severity`, `credits`, `references`. The adapter is free
    to ignore fields its tool cannot accept at allocation time and
    fill them in later via `push_update`. The Vulnogram adapter
    accepts the full set at allocation; the MITRE-form adapter
    only sends `title` + `affected_versions` + a short description
    in the initial form submission.
- **Output.** The allocated CVE ID as a string in canonical
  `CVE-YYYY-NNNN+` form (`CVE-2026-12345`). If the adapter cannot
  allocate synchronously (the MITRE-form case), it returns a
  provisional placeholder string and the skill stores that on the
  tracker as `cve allocation pending`; the next `fetch_current_state`
  call upgrades it once the real ID arrives.
- **No-op case.** The `none` adapter raises `NotApplicable` and
  the skill skips allocation entirely. The `security-cve-allocate`
  skill detects this and posts a "this adopter does not allocate
  CVEs; closing without an ID" comment on the tracker instead.

### `fetch_current_state(cve_id) to {state, fields}`

Read the current record state and the fields the tool stores.

- **Lifecycle moment.** Fires inside `security-issue-sync` at
  Step 1e (the reviewer-comment sync) and Step 5b (the
  state-progression gate before the release-manager hand-off).
  Also fires from `security-issue-invalidate`'s Step 0 gate, to
  refuse closure when a record exists and is past `review-ready`.
- **Inputs.** The CVE ID. Nothing else — the adapter is responsible
  for resolving the tool-specific URL or API call from the ID.
- **Output.** A two-field dict:
  - `state` — one of the generic state verbs defined below
    (`allocated`, `review-ready`, `publish-ready`, `public`,
    `retracted`, `unknown`). The adapter is responsible for
    mapping its tool's native state vocabulary onto these verbs.
  - `fields` — a dict of the fields the tool currently stores
    for the record. Shape matches the `allocate` input dict; the
    skills diff this against the tracker body to detect drift.
- **No-op case.** The `none` adapter returns `{state: "unknown",
  fields: {}}`. Skills treat `unknown` as "do not gate progression
  on tool state"; they fall back to the tracker body's view.

### `push_update(cve_id, fields, state_transition=None) to diff`

Write field updates to the record, optionally with a state move.

- **Lifecycle moment.** Fires inside `security-issue-sync` at
  Step 5c, whenever the tracker body has drifted from what
  `fetch_current_state` returned. Also fires from
  `security-issue-deduplicate` when the kept tracker absorbs
  credits or references from the merged-in tracker.
- **Inputs.**
  - `cve_id` — as for `fetch_current_state`.
  - `fields` — the new desired fields. The adapter is responsible
    for translating these into whatever wire format its tool
    accepts (JSON record for Vulnogram, web-form fields for the
    MITRE form, GraphQL mutation for GHSA).
  - `state_transition` *(optional)* — one of the generic state
    verbs. When supplied, the adapter performs the state move as
    part of the same write; when omitted, the adapter only writes
    fields and leaves state untouched. The Vulnogram adapter today
    embeds the state inside the JSON body and so does both in one
    PUT; other adapters may need a separate API call after the
    field write. The contract does not constrain how — only that
    the call appear atomic from the skill's point of view.
- **Output.** A diff dict — `{added: [...], removed: [...],
  changed: [...]}` — that the skill can show the user as part of
  the sync confirmation. Empty diff means the record was already
  up to date; the skill suppresses the confirmation prompt in
  that case.
- **No-op case.** The `none` adapter returns `{added: [], removed:
  [], changed: []}` and writes nothing. The `unknown`-state
  return from `fetch_current_state` also causes the skill to skip
  `push_update` — the adapter has nowhere to push to.

### `publish(cve_id) to ok`

Move the record to its terminal public state.

- **Lifecycle moment.** Fires inside `security-issue-sync` at
  the publication step, **after** the public advisory archive URL
  has been captured on the tracker. The captured archive URL is
  the real-world signal that the advisory has actually shipped to
  the users-list; before that point the adapter would be
  publishing a record before the world has heard about the issue,
  which is the opposite of the desired ordering.
- **Inputs.** The CVE ID.
- **Output.** `ok` (a sentinel; the skill does not inspect the
  return body — failure raises). The skill follows up with
  `fetch_current_state` to confirm the state landed at `public`
  and posts a confirmation comment on the tracker.
- **No-op case.** The `none` adapter raises `NotApplicable`. The
  skill treats this as "this adopter does not publish CVEs; the
  tracker close is the only terminal action" and proceeds to the
  archive-from-board step directly. The `ghsa` adapter implements
  this as a GraphQL `publishSecurityAdvisory` mutation.

### `retract(cve_id, reason) to ok`

Mark an allocated-but-not-public record as rejected.

- **Lifecycle moment.** Fires inside `security-issue-invalidate`
  for trackers that already carry a CVE ID. CVE retraction is
  governance-sensitive; the skill requires explicit confirmation
  from a release-vote-gating role (`governance.cve_allocation_gate`
  from `project.md`) before invoking `retract`. The retracted
  state is terminal — once a record has been retracted, no
  subsequent `push_update` or `publish` is valid.
- **Inputs.**
  - `cve_id` — as above.
  - `reason` — a short string captured from the tracker
    discussion explaining the retraction. The adapter is
    responsible for writing this into the appropriate field of
    its tool's record (Vulnogram's `CNA_private.justification`,
    CVE.org's `rejectedReason`, etc.).
- **Output.** `ok` sentinel as for `publish`.
- **No-op case.** The `none` adapter raises `NotApplicable`. The
  skill falls back to "close the tracker as invalid; there is no
  CVE record to retract." For adapters whose tool does not
  support retraction of `public` records (Vulnogram refuses this;
  CVE.org has a separate REJECT flow), the adapter must raise a
  distinguishable `AlreadyPublic` error and the skill escalates
  to the configured governance contact.

## Generic state verbs

The skills speak in generic verbs about a CVE record's lifecycle.
The adapter is responsible for mapping its tool's native states
onto these verbs. The verbs are:

| Verb | Meaning | Vulnogram-native state |
|---|---|---|
| `allocated` | Record exists, ID is reserved, content is being filled in. Not visible publicly. | `DRAFT` |
| `review-ready` | Record content is complete and ready for CNA review. Reviewer comments may arrive at this state. Not visible publicly. | `REVIEW` |
| `publish-ready` | Record content is final, reviewer comments addressed, staged for the advisory-send step. The advisory emails are dispatched from the CVE tool while in this state. Not visible publicly. | `READY` |
| `public` | Record pushed to `cve.org` and world-readable. Terminal in the success path. | `PUBLIC` |
| `retracted` | Record marked rejected post-allocation. Terminal in the failure path. | `REJECTED` (in `CNA_private.state`) |
| `unknown` | The adapter cannot determine the state (network failure, asynchronous tool that hasn't replied yet, `none` backend). Skills treat this as "fall back to the tracker body's view." | n/a |

The map is **adapter-internal**. Skills never write `DRAFT` or
`REVIEW` — they write `allocated` and `review-ready`. An adapter
that needs a more granular internal model is free to introduce
sub-states inside its `tool/` directory, as long as the
contract-facing methods normalise on the verbs above.

The `DRAFT` to `REVIEW` transition is sync-driven (the tracker body
fields determine readiness) in the Vulnogram adapter; the
`REVIEW` to `READY` transition is release-manager-driven because the
adapter cannot tell from the tracker body alone whether reviewer
comments are still pending. Other adapters may collapse these two
transitions into one (the GHSA adapter has no separate
review/publish-ready distinction) or split them further (a
hypothetical multi-stage CNA tool with `REVIEW` to `ADDRESSING` →
`READY`). The contract does not constrain how the adapter maps
its internal states onto the four generic verbs — only that the
verbs are what the skills see.

## Skills that consume this contract

The CVE-tool contract is consumed by four skills today:

- **`security-cve-allocate`** — calls `allocate` to reserve the
  CVE ID and create the initial record, then writes the ID back
  onto the tracker's *CVE tool link* body field. The skill is
  PMC-gated (or `governance.cve_allocation_gate`-gated for
  non-ASF adopters); the adapter does not enforce that gate
  itself — the skill does.
- **`security-issue-sync`** — calls `fetch_current_state` at
  Step 1e (to surface reviewer-comment signals) and at Step 5b
  (to gate the release-manager hand-off on the post-push state
  being `review-ready`). Calls `push_update` at Step 5c whenever
  the tracker body has drifted from the tool's view. Calls
  `publish` at the publication step after the public advisory
  archive URL has been captured.
- **`security-issue-invalidate`** — uses `fetch_current_state` at
  its Step 0 gate to refuse closure when a CVE record exists and
  is past `review-ready`. Calls `retract` when the tracker
  carries a CVE ID and the consensus decision is "invalid after
  allocation" (escalates first via `governance.escalation_contact`,
  since CVE retraction has public consequences).
- **`security-issue-deduplicate`** — calls `push_update` on the
  kept tracker's CVE record after merging in credits and
  references from the duplicate tracker. The duplicate tracker's
  CVE record (if any) is the subject of a separate `retract`
  call once the dedup decision has been confirmed.

These four skills are the only consumers in the current shape of
the framework. Future skills that need to touch a CVE record
(e.g. a hypothetical `security-cve-rotate-credit` action) will
extend the contract rather than reaching past it into a specific
adapter.

## ASF default — Vulnogram

The ASF reference adapter lives at [`tools/cve-tool-vulnogram/`](../cve-tool-vulnogram/).
It is the only adapter shipping today, and the only adapter the
skills are tested against. Key properties of the ASF default:

- **URL prefix:** `https://cveprocess.apache.org/cve5/<CVE-ID>`.
  The record page, `#source` tab, `#json` tab, and `#email` tab
  are all rooted there. See [`tools/cve-tool-vulnogram/record.md`](../cve-tool-vulnogram/record.md#record-urls)
  for the canonical URL table.
- **Email preview URL:** `https://cveprocess.apache.org/cve5/<CVE-ID>#email`.
  Renders the advisory exactly as Vulnogram will dispatch it to
  the users-list and announce-list — same subject, same body,
  same recipient list. The release-manager checklist calls this
  out as a load-bearing checkpoint before the advisory-send step.
- **Source tab URL:** `https://cveprocess.apache.org/cve5/<CVE-ID>#source`.
  The copy-paste fallback target for the JSON record. The default
  write path is the OAuth-authenticated API (`vulnogram-api-record-update`);
  copy-paste is the documented fallback when the OAuth session
  has expired or the operator has opted out.
- **State machine:** `DRAFT` to `REVIEW` to `READY` to `PUBLIC`,
  carried inside `CNA_private.state` on the CVE 5.x record. The
  adapter maps these onto the generic verbs as shown in the
  state-verb table above.
- **Reviewer-comment channel:** mailing-list (the ASF CNA
  reviewers email the project's `security_list` rather than
  surfacing comments on the tracking issue directly). This is
  the `cve_authority.reviewer_channel: mailing-list` setting in
  `project.md`; an adapter could equally well declare
  `reviewer_channel: github-pr` (for a CNA tool that uses pull
  requests as its review surface) or `reviewer_channel: none`
  (for a CNA tool with no separate review step).
- **Publication propagation:** poll (the skills check the public
  advisory archive on every sync rather than waiting for a
  webhook). This is the `cve_authority.publication_propagation:
  poll` setting in `project.md`.

**Rename pending.** PR4 of the ASF-pluggable security flow renames
`tools/cve-tool-vulnogram/` to `tools/cve-tool-vulnogram/` to match the
contract's adapter-naming rule. **This PR does not perform the
rename** — the existing skill prose and the `cve_authority.tool:
vulnogram` setting in `project.md` continue to point at
`tools/cve-tool-vulnogram/` until PR4 lands.

## Configuration

Every adopter declares its CVE-tool choice in
`projects/<project>/project.md` under the `cve_authority` block.
The shape is:

```yaml
cve_authority:
  tool: vulnogram              # vulnogram | cve-org-direct | mitre-form | ghsa | none
  allocate_url: "https://cveprocess.apache.org/cve5/new"
  record_url_template: "https://cveprocess.apache.org/cve5/{cve_id}"
  source_tab_url_template: "https://cveprocess.apache.org/cve5/{cve_id}#source"
  email_preview_url_template: "https://cveprocess.apache.org/cve5/{cve_id}#email"
  states: [allocated, review-ready, publish-ready, public]
  publication_propagation: poll        # poll | webhook | manual
  emits_allocation_email: true
  reviewer_channel: mailing-list       # mailing-list | github-pr | none
```

Field-by-field:

- **`tool`** — names the adapter directory the skills resolve to.
  The ASF default is `vulnogram` (resolves to `tools/cve-tool-vulnogram/`;
  becomes `tools/cve-tool-vulnogram/` after PR4). Adopters using
  a different CVE-tool backend pick one of the other four
  enumerated values, each of which is expected to resolve to a
  sibling `tools/cve-tool-<name>/` directory.
- **`allocate_url`** — the URL the human operator opens to begin
  the allocation flow (for tool-mediated allocation paths) or
  the API endpoint the adapter POSTs to (for fully automated
  paths). The Vulnogram default is the `/cve5/new` form; the
  MITRE-form default would be `https://cveform.mitre.org/`.
- **`record_url_template`** — the per-record URL pattern. The
  `{cve_id}` placeholder is the only token the skills substitute.
  Skills use this for the "open the CVE record" links they post
  on the tracker.
- **`source_tab_url_template`** — the copy-paste fallback target.
  Optional for adapters that have no copy-paste fallback (e.g.
  GHSA — there is no JSON form to paste into). When the field is
  null, the skills suppress the copy-paste fallback proposal.
- **`email_preview_url_template`** — the advisory-email preview
  URL. Optional for adapters whose CVE tool does not dispatch
  the advisory itself; when null, the release-manager checklist
  omits the preview step. The ASF default points at the `#email`
  tab.
- **`states`** — the ordered list of generic state verbs the
  adapter exposes. Adapters with fewer states (the GHSA adapter
  collapses `review-ready` and `publish-ready` into a single
  pre-publish state; the `none` adapter has only `allocated` and
  `unknown`) declare a shorter list. Skills branch on the
  declared list when deciding which lifecycle steps apply.
- **`publication_propagation`** — how the skills learn that a
  record has reached `public`. `poll` (the ASF default) means
  the skills check the public archive on every sync; `webhook`
  means an external hook updates the tracker directly; `manual`
  means a human flips the tracker label and the skills trust it.
- **`emits_allocation_email`** — whether the CVE tool sends an
  allocation-confirmation email at `allocate` time. The ASF
  default is `true` (Vulnogram emails the project's
  `security_list` on every allocation); the MITRE-form default
  is also `true` (the form replies with the allocated ID);
  GHSA's default is `false` (no email — the allocation result is
  the API response). Skills that wait on this email surface a
  "do not close this tracker until the allocation email lands"
  hint when the flag is `true`.
- **`reviewer_channel`** — where reviewer comments arrive.
  `mailing-list` (ASF default) means `security-issue-sync` reads
  reviewer comments off the security mailing list; `github-pr`
  means it reads them off a backing PR; `none` means there is
  no separate review step.

The contract does not constrain how the adapter implements any of
these settings — only that the settings are present and that the
adapter respects them. Adapters are free to add their own
tool-specific sub-keys under `cve_authority.<adapter-name>:` (e.g.
`cve_authority.vulnogram.asf_org_id`, `cve_authority.vulnogram.cna_private_owner`)
for fields the contract does not surface.
