Table of Contents generated with DocToc

Security workflow — process and label lifecycle

The authoritative reference for the 16-step security-issue lifecycle and the label-lifecycle state diagram. The role guides point into specific steps; the security skills execute the steps; the threat model maps the steps and skills to trust boundaries, adversaries, and mitigations.

Process reference: the 16 steps

This is the authoritative outline of the 16-step lifecycle. Each step links to the skill or document that owns the deep mechanics — the brief descriptions below are an overview, not a substitute for the linked skill's SKILL.md. If the role sections above conflict with what is here, this reference wins.

flowchart TD
    S1[1. Report arrives on security-list]
    S2[2. Import the report]
    ALT2([alt: import from public PR])
    S3[3. Discuss CVE-worthiness]
    S4[4. Escalate stalled discussions]
    S5{5. Valid or invalid?}
    INV([Invalid: close + reporter reply])
    S6[6. Allocate CVE]
    S7[7. Self-assign + implement]
    S8[8. Open public PR]
    S9[9. Open private PR exceptional]
    S10[10. Link PR + apply pr created]
    S11[11. PR merged]
    S12[12. Fix released]
    S13[13. Send advisory]
    S14[14. Sync close-out at archive URL]
    S15[15. RM verifies close-out]
    S16[16. Credit corrections]

    S1 --> S2
    ALT2 --> S5
    S2 --> S3
    S3 --> S4
    S3 --> S5
    S4 --> S5
    S5 -->|valid| S6
    S5 -->|invalid| INV
    S6 --> S7
    S7 --> S8
    S7 --> S9
    S8 --> S10
    S9 --> S10
    S10 --> S11
    S11 --> S12
    S12 --> S13
    S13 --> S14
    S14 --> S15
    S15 --> S16

    classDef triager fill:#fff3cd,stroke:#664d03,color:#000
    classDef remed fill:#cfe2ff,stroke:#055160,color:#000
    classDef releaseMgr fill:#d4edda,stroke:#0f5132,color:#000
    classDef terminal fill:#f8d7da,stroke:#842029,color:#000

    class S1,S2,ALT2,S3,S4,S5 triager
    class S6,S7,S8,S9,S10,S11 remed
    class S12,S13,S14,S15,S16 releaseMgr
    class INV terminal

Colour key: yellow = triager (Steps 1–5), blue = remediation developer (Steps 6–11), green = release manager (Steps 12–16), red = terminal close.

Step 1 — Report arrives on security@

The reporter sends the issue to the adopting project's <security-list> (or to security@apache.org, which forwards to the project list).

Step 2 — Import the report

security-issue-import scans <security-list> for un-imported threads, classifies each candidate (real / automated-scan / consolidated / spam), extracts the issue-template fields from the root message, and proposes one tracker per valid report plus a Gmail receipt-of-confirmation draft. Nothing is applied without explicit confirmation. The newly-created tracker lands with needs triage.

If the report matches a known-invalid pattern, the skill drafts the matching canned reply from [<project-config>/canned-responses.md](/canned-responses.md) and does not create a tracker — invalid noise never enters the board.

Alternate entry — fix already opened as a public PR. Use security-issue-import-from-pr. The tracker lands directly in the Assessed column with the scope label applied (validity already decided informally), so Step 5 is skipped and the tracker is ready for security-cve-allocate immediately.

Alternate entry — bulk import from markdown. Use security-issue-import-from-md when triaging the output of an AI security review or third-party scanner. Each finding becomes one tracker.

Step 3 — Discuss CVE-worthiness

Drive the validity assessment in tracker comments. Pull at least one other security-team member into the discussion. Use canned responses from [<project-config>/canned-responses.md](/canned-responses.md) for negative assessments so the tone stays polite-but-firm.

For larger batches landed by security-issue-import (or the -from-md / -from-pr variants), the security-issue-triage skill automates the first half of this step: for each tracker in Needs triage, it reads the body + comments, applies the project's Security Model framing, and — on user confirmation — posts a top-level triage-proposal comment that classifies the candidate disposition into one of six classes and @-mentions 2-3 security-team members for input. The proposal-comment shape is:

  • a one-paragraph technical summary in the triager's own words;
  • the proposed class, with severity guess (the team scores independently per the no-reporter-CVSS rule);
  • a one-sentence fix shape (or “why not” framing for negative classes);
  • a specific question for the @-mentioned reviewers.

The six disposition classes route to different next-steps once team consensus lands:

ClassNext step after consensus
VALIDsecurity-cve-allocate → Step 6
DEFENSE-IN-DEPTHClose as wontfix + open a public PR for the hardening
INFO-ONLYsecurity-issue-invalidate with the matching canned-response template
INVALIDsecurity-issue-invalidate
PROBABLE-DUPsecurity-issue-deduplicate
FIX-ALREADY-PUBLICAfter reporter confirms the cited public PR fixes their report: security-issue-invalidate. If the reporter says it does not fix it, re-triage with --retriage. No finder credit is recorded per the no-credit-when-fix-is-already-public policy.

The triage skill is read-only on tracker state — it never flips needs triage to a scope label, never closes, never allocates a CVE. The valid/invalid decision belongs to team consensus; this skill opens the discussion that produces it, and one of the next-step skills above (or a hand-applied label change via Step 5) lands the actual state transition. A --retriage mode is available for re-litigating passed-triage decisions when substantive new comment activity lands.

Step 4 — Escalate stalled discussions

If discussion stalls for ~30 days, escalate in two phases:

  • Phase 1 — short call for ideas. A 3-4-paragraph message that states the report exists and asks the wider audience for input. No AI analysis, no proposed fixes — phase 1 is deliberately bare so domain experts can weigh in with novel ideas without being anchored to a pre-baked solution.
  • Phase 2 — AI-generated design-space analysis. Triggered if phase 1 stays silent for ~7 more days. The agent drafts a structured analysis (TL;DR, why-the-obvious-fix-is-insufficient, options A/B/C with trade-offs, open design questions, tagged reviewers per a documented selection methodology). The triager reviews and approves before posting.

Audiences are the same for both phases: <private-list>, security@apache.org, the original reporter. Both phases land as rollup entries on the tracker (per tools/github/status-rollup.md) with the action label Sync (Step 4 escalation).

Step 5 — Land the valid/invalid consensus

If valid, apply exactly one scope label from [<project-config>/scope-labels.md](/scope-labels.md); remove needs triage. If invalid, security-issue-invalidate labels invalid, posts a closing comment, archives the board item, and (for <security-list>-imported trackers) drafts a polite-but-firm reporter reply. If consensus cannot be reached, follow ASF voting on <security-list>.

If a candidate duplicate is detected, security-issue-deduplicate merges two trackers in place — preserving every reporter‘s credit, every mailing-list thread reference, and every independent attack-vector description. The kept issue’s body is updated, the duplicate is closed with the duplicate label, and the CVE JSON attachment is regenerated so both finders land in credits[].

Step 6 — Allocate the CVE

security-cve-allocate opens the project‘s CVE allocation tool (URL + tool name declared in [<project-config>/project.md → CVE tooling](/project.md#cve-tooling)), normalises the title per [<project-config>/title-normalization.md](/title-normalization.md), and — if the triager isn’t on the PMC — builds an @-mention relay message for a PMC member. Once the allocated CVE-YYYY-NNNNN is pasted back, the skill wires it into the tracker (CVE tool link body field, cve allocated label, status-change comment, refreshed CVE-JSON attachment) and hands off to security-issue-sync to reconcile the rest.

Step 7 — Self-assign and implement the fix

A security team member self-assigns and implements the fix. Optional automation: security-issue-fix proposes an implementation plan, writes the change in your local <upstream> clone, runs local tests, and opens a public PR via gh pr create --web with a scrubbed title + body. Every public surface (commit message, branch name, PR title, PR body, newsfragment) is grep-checked for CVE-, the <tracker> slug, “vulnerability”, “security fix”, and similar leakage before being written or pushed.

The skill refuses to proceed for reports still being assessed, reports not yet classified as valid, and changes that require the private-PR fallback (Step 9). Even when it succeeds end-to-end, you remain the PR's author and reviewer-facing contact — stay on the PR through review and merge.

Delegation to a trusted third-party individual is permitted under LAZY CONSENSUS, sharing only the information required to implement the fix.

Step 8 — Open a public PR (straightforward cases)

The PR description must not reveal the CVE, the security nature of the change, or link back to <tracker>. See AGENTS.md → Confidentiality. Request the appropriate backport-to-vN-N-test label on the public PR when the fix should ship on a patch train.

Step 9 — Open a private PR (exceptional cases)

For highly critical fixes or code that needs private review, open the PR against <tracker>'s main branch first (not the tracker_default_branch set in <project-config>/project.md). CI does not run there — run static checks + tests manually. Once approved, push the branch to <upstream> and re-open the PR there.

Step 10 — Link the PR and apply pr created

The remediation developer links the PR in the tracker description and applies pr created on <tracker>.

Step 11 — PR merged

When the <upstream> PR merges, swap pr createdpr merged and set the milestone of the release the fix will ship in (per [<project-config>/milestones.md](/milestones.md)). Close any private variant in <tracker>. The tracker waits at pr merged until the release ships — this can be hours (a hot-fix patch release) or weeks (a regular project-cadence release).

Step 12 — Fix released

When the release containing the fix ships to users (PyPI / Helm registry / equivalent), security-issue-sync detects the release version on the next run and — provided a two-stage gate is clear — proposes the pr mergedfix released swap (and the assignee swap from remediation developer to release manager) plus a one-shot release-manager hand-off comment with a numbered checklist of the three RM actions (promote the record from review-ready to publish-ready, send the advisory, sync closes the rest) and the per-record URLs the checklist points at — the record page (cve_authority.record_url_template) and the advisory-email preview (cve_authority.email_preview_url_template), both declared in [<project-config>/project.md → cve_authority](/project.md#cve-authority). For the airflow-s adopter, those templates resolve to https://cveprocess.apache.org/cve5/<CVE-ID> and the same URL with #email appended — the Vulnogram #source and #email tabs.

The two gates:

  1. Mandatory body fields populated. Six fields must be non-empty and non-_No response_: CWE, Affected versions, Severity, Reporter credited as, Short public summary for publish, PR with the fix. The same check fires earlier, at the pr createdpr merged transition (Step 11), so the remediation developer is nudged to fill fields as soon as the PR merges.
  2. CVE record state is review-ready. Sync pushes the regenerated CVE JSON to the CVE tool via the <cve-tool> adapter‘s push_update() method in the same pass (see State auto-promote in the sync skill); the generator promotes the record from allocated to review-ready once Stage 1 is clear, and push_update() is responsible for translating the generic state verb into whatever the adapter’s tool requires. Sync then verifies the saved state via <cve-tool>.fetch_current_state(). (For the Vulnogram adapter, that translates to state = "REVIEW" on the JSON record, which cveprocess.apache.org accepts verbatim.)

If either gate fails, sync instead posts (or PATCH-updates) a Remediation-developer fill-fields comment @-mentioning the remediation developer with the specific blocker (which fields are missing, or that the record is still in allocated after the push). The tracker stays assigned to the remediation developer and the RM hand-off comment is not posted on this run — the RM never sees a hand-off while the record is still in allocated. A later sync run that finds both gates clear proceeds with the hand-off.

Step 13 — Send the advisory

By the time the release manager receives the hand-off comment, every mandatory CVE body field is already populated on the tracker (Step 12‘s gate), the CVE JSON has been pushed via <cve-tool>.push_update(), and the record is in review-ready state. The RM’s job is the three-step checklist in the hand-off comment, all of it single clicks in the CVE tool — no shell commands, no JSON paste:

  1. Address reviewer feedback (if any) and promote the record to publish-ready. Open the record at cve_authority.record_url_template substituted with the tracker‘s CVE ID. If the CVE reviewer has posted comments (the channel is declared in cve_authority.reviewer_channelmailing-list for the ASF default), work through them on the same channel; when the thread is clear, drive the record from review-ready to publish-ready per the tool’s UI. Most CVEs go through review-ready with no reviewer comments — in that case the promotion is immediate. (For the Vulnogram adapter, that is the State dropdown flipping from REVIEW to READY on the record's #source tab.)
  2. Preview and send the advisory email. Open cve_authority.email_preview_url_template substituted with the CVE ID. The page renders the exact advisory email that will go out. Verify the recipients (<users-list> and <announce-list>) and the body, then click Send Email. This is the only manual send action. (For the Vulnogram adapter, that is the record's #email tab.)
  3. Stop. Sync drives the rest at the archive-URL trigger (Step 14). The RM does not paste JSON anywhere, does not promote the record from publish-ready to public, does not close the tracker.

The severity score follows the ASF severity rating (lazy consensus during discussion; voting if there's disagreement; the RM has the final say to keep the announcement on schedule). The RM may still need to adjust body fields before sending if reviewer feedback prompts it; the regenerated JSON is re-pushed automatically by the next sync.

Sync does not flip fix released → announced - emails sent at this step; that label transition fires at Step 14 along with the rest of the post-advisory close-out. The issue stays open at this point — it closes at Step 14.

Step 14 — Capture the public advisory URL and close out

Once the announcement is archived on the users@ list, the next security-issue-sync run detects the archive URL and fires a single combined apply that drives the entire post-advisory close-out — there is no separate RM “publish + close” step. In one pass sync:

  1. Populates the Public advisory URL body field (a dedicated field on the issue template — never reuses the “Security mailing list thread” field).
  2. Extracts the short public summary from the archived advisory email body (the prose between the CVE header and the Affected version range block) and writes it back to the Short public summary for publish body field so the tracker's summary matches what actually shipped.
  3. Flips the tracker labels: adds announced - emails sent and announced, removes fix released.
  4. Regenerates the CVE JSON attachment — the generator picks up the new short summary as descriptions[].value and the URL as a vendor-advisory reference, and now records the tracker's promotion to public.
  5. Re-pushes the regenerated JSON via the <cve-tool> adapter's push_update() method.
  6. Promotes the record from publish-ready to public via the adapter's publish() method — the CNA-feed dispatch to cve.org, formerly a manual UI click but now driven by sync since the archive URL is the real-world signal that the advisory has shipped. The exact wire mechanism depends on the adapter (cve_authority.publication_propagation declares whether sync polls for the result, awaits a webhook, or treats the move as manual); for the Vulnogram adapter the implementation is vulnogram-api-record-publish under poll.
  7. Moves the project-board column to Announced.
  8. Closes the tracker as completed.
  9. Archives the tracker from the Announced column via the archiveProjectV2Item GraphQL mutation (see tools/github/project-board.mdArchive a board item).
  10. If every sibling on the tracker's milestone is also closed at that moment, closes the milestone too.
  11. Posts a purely-informational wrap-up comment tagging the RM as a timeline marker that the lifecycle is complete. No manual asks — everything actionable was already taken care of by the steps above.

Until Public advisory URL is populated, the sync skill will not propose announced or any of the downstream steps — promoting a CVE record to public with an empty vendor-advisory reference would leak a broken record into cve.org.

When the adapter's write path is not available (no credentials, expired session, transient HTTP error on the push_update() or publish() call), the JSON re-push and the publish-ready → public promotion and the tracker close all defer to the next sync that resolves the push issue; the manual-paste variant of the publication-ready notification comment is posted in that case explaining the deferral.

Step 15 — RM verifies the close-out landed

There is no manual close step. The release manager's last post-Send-Email action is none — sync at Step 14 closes the tracker, promotes the CVE record to public via <cve-tool>.publish(), archives the board item, and (conditionally) closes the milestone. The RM receives the wrap-up comment as a timeline event marker.

A tracker that sits on announced - emails sent without announced for more than a day or two is a signal that sync did not see the advisory in the <users-list> archive yet (propagation lag, search-engine miss); re-run sync or wait for the next scheduled pass.

Step 16 — Credit corrections

If credits need correction post-announcement, the release manager:

  • responds to the announcement emails with the missing credits;
  • updates the project's CVE tool with the missing credits;
  • asks the ASF security team to push the information to cve.org.

Label lifecycle

State diagram

The diagram below shows the typical state flow. Each node is a label (or a cluster of labels that co-exist); each edge is a process step that moves the issue forward. Closing dispositions (invalid, not CVE worthy, duplicate, wontfix) can terminate the flow at any point after needs triage.

flowchart TD
    A([report on project security list]) -->|step 2: security-issue-import| B[needs triage]
    A2([security-relevant fix in public PR]) -->|step 2 alt: security-issue-import-from-pr| C
    B -->|step 5: consensus invalid| X1([invalid / not CVE worthy / duplicate / wontfix])
    B -->|step 5: consensus valid| C["scope label<br/>(project-specific — see<br/>projects/&lt;PROJECT&gt;/scope-labels.md)"]
    C -->|step 6: CVE reserved by PMC member| D[cve allocated]
    D -->|step 10: public PR opened| E[pr created]
    E -->|step 11: PR merges| F[pr merged]
    F -->|step 12: release ships| G[fix released]
    G -->|step 13: RM sends advisory| H[announced - emails sent]
    H -->|step 14: sync close-out at archive URL| J[announced + closed]
    J -->|step 15: RM timeline marker| Z([issue closed])

    classDef closed fill:#f8d7da,stroke:#842029,color:#000;
    classDef done fill:#d1e7dd,stroke:#0f5132,color:#000;
    class X1,Z closed;
    class H,J done;

The dashed-equivalent entry from A2 represents the deliberate-import path described in Step 2 above: trackers opened from a public PR skip the needs triage column and land directly at scope label (the Assessed column on the project board) because the validity assessment has already happened informally before invocation.

Label reference

The table below repeats the same flow in tabular form. An issue typically moves through these labels left-to-right.

Scope labels are project-specific — the adopting project's concrete scope labels live in <project-config>/scope-labels.md (for the currently adopting project, [<project-config>/scope-labels.md](/scope-labels.md)). The table below uses <scope> as a placeholder for whichever scope labels the adopting project defines.

LabelMeaningAdded at stepRemoved at step
needs triageFreshly filed; assessment not yet started.15
<scope>Scope of the vulnerability. Exactly one project-specific scope label is set.5never (sticks for the lifetime of the issue)
cve allocatedA CVE has been reserved for the issue. Allocation itself is PMC-gated (only the adopting project's PMC members can submit the CVE-tool allocation form); a non-PMC triager relays a request to a PMC member via the security-cve-allocate skill.6never
pr createdA public fix PR has been opened on <upstream> but has not yet merged.1011 (replaced by pr merged)
pr mergedThe fix PR has merged into <upstream>; no release with the fix has shipped yet.1112 (replaced by fix released when the release ships)
fix releasedA release containing the fix has shipped to users; advisory has not been sent yet. Gated on the two-stage check (six mandatory body fields populated + CVE record state review-ready).1214 (replaced by announced - emails sent at the archive-URL combined apply)
announced - emails sentThe public advisory has been sent to the project's announce and users mailing lists (see <project-config>/project.md → Mailing lists). The issue stays open after this label is applied; closing happens at Step 14 once sync sees the advisory archived on <users-list>.14 (combined apply with announced)never (stays on the issue after closing for audit history)
announcedThe public advisory URL has been captured in the tracking issue's Public advisory URL body field and the attached CVE JSON has been regenerated so its references[] now carries the vendor-advisory URL. The CVE record has been promoted to public via <cve-tool>.publish() and the tracker has been closed and archived from the board — all in the same Step 14 combined apply. No label changes at close — the issue closes with announced still set.14never (stays on the issue after closing)
wontfix / invalid / not CVE worthy / duplicateClosing dispositions for reports that are not valid or not CVE-worthy.5 / 6

The security-issue-sync skill keeps these labels honest: on every run it detects the current state of the issue, the fix PR, and the release train, and proposes the label transitions the process requires.