Table of Contents generated with DocToc
pr createdThe 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.
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.
The reporter sends the issue to the adopting project's <security-list> (or to security@apache.org, which forwards to the project list).
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.
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:
@-mentioned reviewers.The six disposition classes route to different next-steps once team consensus lands:
| Class | Next step after consensus |
|---|---|
VALID | security-cve-allocate → Step 6 |
DEFENSE-IN-DEPTH | Close as wontfix + open a public PR for the hardening |
INFO-ONLY | security-issue-invalidate with the matching canned-response template |
INVALID | security-issue-invalidate |
PROBABLE-DUP | security-issue-deduplicate |
FIX-ALREADY-PUBLIC | After 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.
If discussion stalls for ~30 days, escalate in two phases:
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).
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[].
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.
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.
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.
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.
pr createdThe remediation developer links the PR in the tracker description and applies pr created on <tracker>.
When the <upstream> PR merges, swap pr created → pr 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).
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 merged → fix 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:
_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 created → pr merged transition (Step 11), so the remediation developer is nudged to fill fields as soon as the PR merges.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.
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:
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_channel — mailing-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.)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.)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.
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:
announced - emails sent and announced, removes fix released.descriptions[].value and the URL as a vendor-advisory reference, and now records the tracker's promotion to public.<cve-tool> adapter's push_update() method.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.Announced.completed.Announced column via the archiveProjectV2Item GraphQL mutation (see tools/github/project-board.md — Archive a board item).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.
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.
If credits need correction post-announcement, the release manager:
cve.org.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/<PROJECT>/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.
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.
| Label | Meaning | Added at step | Removed at step |
|---|---|---|---|
needs triage | Freshly filed; assessment not yet started. | 1 | 5 |
<scope> | Scope of the vulnerability. Exactly one project-specific scope label is set. | 5 | never (sticks for the lifetime of the issue) |
cve allocated | A 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. | 6 | never |
pr created | A public fix PR has been opened on <upstream> but has not yet merged. | 10 | 11 (replaced by pr merged) |
pr merged | The fix PR has merged into <upstream>; no release with the fix has shipped yet. | 11 | 12 (replaced by fix released when the release ships) |
fix released | A 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). | 12 | 14 (replaced by announced - emails sent at the archive-URL combined apply) |
announced - emails sent | The 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) |
announced | The 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. | 14 | never (stays on the issue after closing) |
wontfix / invalid / not CVE worthy / duplicate | Closing 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.