Table of Contents generated with DocToc
- Security workflow — process and label lifecycle
- Process reference: the 16 steps
- Step 1 — Report arrives on security@
- Step 2 — Import the report
- Step 3 — Discuss CVE-worthiness
- Step 4 — Escalate stalled discussions
- Step 5 — Land the valid/invalid consensus
- Step 6 — Allocate the CVE
- Step 7 — Self-assign and implement the fix
- Step 8 — Open a public PR (straightforward cases)
- Step 9 — Open a private PR (exceptional cases)
- Step 10 — Link the PR and apply
pr created - Step 11 — PR merged
- Step 12 — Fix released
- Step 13 — Send the advisory
- Step 14 — Capture the public advisory URL and close out
- Step 15 — RM verifies the close-out landed
- Step 16 — Credit corrections
- Label lifecycle
- Process reference: the 16 steps
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](
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](
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:
| 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.
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](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-config>/title-normalization.md](@-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 created → pr merged
and set the milestone of the release the fix will ship in (per
[<project-config>/milestones.md](<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 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 (REVIEW → READY, send advisory, sync
closes the rest) and links to the Vulnogram #source and
#email tabs.
The two gates:
- 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 thepr created→pr mergedtransition (Step 11), so the remediation developer is nudged to fill fields as soon as the PR merges. - CVE record state is
REVIEW. Sync pushes the regenerated CVE JSON to Vulnogram in the same pass (see State auto-promote in the sync skill); the generator emitsstate = "REVIEW"once Stage 1 is clear, and Vulnogram accepts the field verbatim. Sync then verifies the saved state.
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 state is still DRAFT 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 in DRAFT. 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 to
Vulnogram, and the record is in REVIEW state. The RM’s job is
the three-step checklist in the hand-off comment, all of it
single clicks in Vulnogram — no shell commands, no JSON
paste:
- Address reviewer feedback (if any) and promote to
READY. Open the record’s#sourcetab. If the CVE reviewer has posted comments, work through them on the same thread; when the thread is clear, change the State dropdown fromREVIEWtoREADYand save. Most CVEs go throughREVIEWwith no reviewer comments — in that case theREVIEW → READYflip is immediate. - Preview and send the advisory email. Open the
#emailtab. 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. - Stop. Sync drives the rest at the archive-URL trigger
(Step 14). The RM does not paste JSON anywhere, does not
click
READY → 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:
- Populates the Public advisory URL body field (a dedicated field on the issue template — never reuses the “Security mailing list thread” field).
- 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.
- Flips the tracker labels: adds
announced - emails sentandannounced, removesfix released. - Regenerates the CVE JSON attachment — the generator picks up
the new short summary as
descriptions[].valueand the URL as avendor-advisoryreference, and now emitsstate = "PUBLIC". - Re-pushes the regenerated JSON to the Vulnogram record over the OAuth API.
- Moves the Vulnogram record
READY → PUBLICviavulnogram-api-record-publish— the CNA-feed dispatch tocve.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. - Moves the project-board column to
Announced. - Closes the tracker as
completed. - Archives the tracker from the
Announcedcolumn via thearchiveProjectV2ItemGraphQL mutation (seetools/github/project-board.md— Archive a board item). - If every sibling on the tracker’s milestone is also closed at that moment, closes the milestone too.
- 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 —
publishing a CVE with an empty vendor-advisory reference would
leak a broken record into cve.org.
When the OAuth session is not available (no credentials, expired
cookie, transient HTTP error), the JSON re-push and the
READY → PUBLIC advance 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, moves the Vulnogram record to PUBLIC, 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/<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.
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> 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). | 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 Vulnogram record has been moved to PUBLIC 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.