From 53f22544589d369376639d446f1ece67ee19e3c5 Mon Sep 17 00:00:00 2001 From: Tester Date: Tue, 7 Jul 2026 17:19:30 +0200 Subject: [PATCH] feat(skills): reconcile GitHub repository security advisories in security-issue-sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GHSA-sourced trackers (reports filed via GitHub "Report a vulnerability") were assumed to have "no GitHub API", so every reporter reply was relayed by email to the hosting security team. GitHub's repository security advisories REST API now exposes the advisory record for read + field-edit at the advisory-collaborator tier. Add skills/security-issue-sync/github-advisory.md: a self-contained contract for reconciling the advisory record with the tracker — an access-tier probe (collaborator = read + field-edit; collaborator-mgmt and publish need admin/security-manager), Step 1 field/state/access drift detection plus recording the advisory link as a clickable tracker field, Step 4 field reconcile (cve_id/severity/cwe/affected/credits) with an admin hand-off for the gated ops, and a direct-post reply path (open the advisory thread + copy-pastable block; no comment API at any tier) with the email relay kept as the no-access fallback. Generated-by: Claude Code (Claude Opus 4.8) --- skills/security-issue-sync/SKILL.md | 14 ++ skills/security-issue-sync/github-advisory.md | 146 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 skills/security-issue-sync/github-advisory.md diff --git a/skills/security-issue-sync/SKILL.md b/skills/security-issue-sync/SKILL.md index f8a0f73c4..c4bef969e 100644 --- a/skills/security-issue-sync/SKILL.md +++ b/skills/security-issue-sync/SKILL.md @@ -365,6 +365,20 @@ gating, also detect active release-vote threads.) The full per-sub-step recipe — 1a through 1h, with the Gmail search queries, PonyMail fallback path, signal-detection rules, and process- step decision table — lives in [`gather.md`](gather.md). + +**GHSA-sourced trackers** — when a tracker's report arrived through +GitHub's *"Report a vulnerability"* flow (a `GHSA-…` repository security +advisory on ``) and the operator is an advisory collaborator, +the sync reconciles the advisory **record** directly via the GitHub +*repository security advisories* REST API (link `cve_id`, mirror +`severity`/`cwe_ids`/`vulnerabilities`/`credits`, record the advisory +link as a clickable tracker field) and replaces the email relay with a +direct-post reply path — with an admin hand-off for the operations that +need admin / security-manager rights (collaborator-management, publish). +The full contract — access tiers, the Step 1 reconcile, the Step 4 +writes, and the reply path — lives in +[`github-advisory.md`](github-advisory.md). + ## Step 2 — Build a proposal (do not apply anything yet) Produce a single, compact summary for the user with three sections: diff --git a/skills/security-issue-sync/github-advisory.md b/skills/security-issue-sync/github-advisory.md new file mode 100644 index 000000000..339d2350d --- /dev/null +++ b/skills/security-issue-sync/github-advisory.md @@ -0,0 +1,146 @@ + + +# security-issue-sync — GitHub repository security advisory reconciliation + +> Extracted from [`SKILL.md`](SKILL.md) so subagents that only need this +> slice can load just this file. Loaded when a tracker is **GHSA-sourced** +> — i.e. its report arrived through GitHub's *"Report a vulnerability"* +> flow, which creates a **repository security advisory** (a `GHSA-…` +> draft) on ``. + +This subdoc covers reconciling that GitHub advisory **record** with the +tracker, and the reporter-reply path when the operator has advisory API +access. It applies only when `` is hosted on GitHub and the +operator is a **collaborator** on the repository's security advisories. + +Historically the skills assumed *"GHSA threads have no GitHub API"* and +routed every reporter reply through an email relay to the hosting +foundation's security team (see [`tools/gmail/asf-relay.md`](../../tools/gmail/asf-relay.md)). +That is now only the **fallback**: the *repository security advisories +REST API* exposes the advisory record for read + field-edit, so the sync +reconciles it directly. Only the reporter⟷maintainer **discussion +thread** still has no API. + +--- + +## Access tiers — probe before acting + +The GitHub *repository security advisories* API grants different +operations at different tiers. Probe the operator's tier once per run and +record it in the observed-state bag: + +| Operation | Endpoint | Tier required | +|---|---|---| +| List / read advisories (incl. `triage`/`draft`) | `GET /repos//security-advisories[/]` | **advisory collaborator** (or admin / security-manager) | +| Edit advisory **fields** (`cve_id`, `credits`, `severity`, `cwe_ids`, `vulnerabilities`) | `PATCH …/security-advisories/` | **advisory collaborator** | +| Manage **collaborators** (`collaborating_users`/`_teams`) | `PATCH …` with those fields | **admin / security-manager** | +| Change **state** / publish | `PATCH …` with `state`, or the publish call | **admin / security-manager** | +| Comment on the reporter discussion thread | *(none — `…/comments` → 404)* | **web UI only** | + +Probe: `gh api /repos//security-advisories --jq 'length'` +(a `200` with a list ⇒ at least collaborator read). Attempting a +collaborator-management `PATCH` returns +`403 "Cannot update advisory collaborators unless you have +administrative/security management rights"` when the operator is a plain +collaborator — treat that 403 as the definitive *"not admin"* signal and +**do not retry**; route the corresponding action to the admin hand-off +below. + +The common tier for a project committee member is **advisory +collaborator**: read + field-edit, but **not** collaborator-management +and **not** publish. Design the flow so the sync does what the +collaborator tier allows and *hands off* the rest — never blind-fire a +`state=published` PATCH hoping it works, because a success is a +**permanent public advisory**. + +--- + +## Step 1 add-on — fetch + reconcile (read; always runs) + +Detect GHSA-sourced trackers by grepping the body / provenance for +`GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}` ids scoped to `` +(reuse the import skill's GHSA grep). For each id, fetch the advisory and +record it. Surface drift in the Step 2 proposal: + +- **cve_id drift** — advisory `cve_id` ≠ the tracker's CVE (or empty). +- **field drift** — `credits` / `severity` / `cwe_ids` / `vulnerabilities` + (affected ranges) differ between advisory and tracker. The tracker + + CVE record is **authoritative**; the advisory is the mirror. +- **state drift** — advisory `state` vs the tracker's process step (e.g. + tracker `announced` + CVE published while the advisory is still + `triage`). Informational at the collaborator tier (state changes are an + admin hand-off). +- **access drift** — security-team roster + (`gh api repos//collaborators --jq '.[].login'`) members + missing from the advisory's `collaborating_users`. Informational at the + collaborator tier (adding collaborators is an admin hand-off). + +**Record the advisory link(s) as a dedicated, clickable tracker field.** +So the GHSA is one click away from the tracker (not buried in prose), the +reconcile ensures the tracker carries a distinct field — e.g. a +`### GitHub Security Advisory (GHSA)` section — listing each advisory as a +markdown link +(`- [GHSA-…](https://github.com//security/advisories/GHSA-…)`). +Populate it on every GHSA-sourced tracker (retroactively on first touch). + +--- + +## Step 4 add-on — advisory writes (propose-then-confirm each) + +Each is a separate confirmable proposal item (SKILL Golden rule 1). + +1. **Link cve_id + reconcile fields — collaborator tier, sync performs it.** + `gh api -X PATCH /repos//security-advisories/ -f cve_id=`, + and where they diverge, mirror the tracker's authoritative + `severity` / `cwe_ids` / `vulnerabilities` / `credits` onto the + advisory. **Never** copy an advisory-supplied CVSS back into the + tracker's severity field (the project's severity rule governs the + tracker; the advisory mirrors *it*). +2. **Mirror state (publish) — admin tier, HAND OFF.** Do not PATCH + `state` at the collaborator tier. Surface it as a hand-off to a repo + admin / the hosting security team, and note that many foundation + advisories stay `triage` by design (the CVE ships through the + foundation's own CVE tool, not GitHub's advisory flow), so publishing + is often not even desirable. +3. **Provide collaborator access — admin tier, HAND OFF.** Surface the + missing-roster access drift as a hand-off to an admin; the sync cannot + add collaborators at the collaborator tier. + +--- + +## Reporter reply — direct-post primary, relay fallback + +There is **no** REST API for the advisory discussion thread, so the reply +text cannot be posted programmatically at any tier. Replace the email +relay as the *primary* path when the operator is an advisory collaborator: + +- **Primary (operator IS a collaborator).** Do **not** draft a relay + email. Instead the sync (a) surfaces the exact reply text, (b) **opens + the advisory discussion in the browser** (`open`/`xdg-open` on the + advisory `html_url`), and (c) prints the **copy-pastable** reply block — + GitHub advisory discussions have no comment-prefill URL parameter, so + the operator pastes it into the web UI at the already-open thread. The + sync never claims to have *posted* the reply. +- **Fallback (operator is NOT a collaborator on that advisory).** Fall + back to the [`tools/gmail/asf-relay.md`](../../tools/gmail/asf-relay.md) + email relay to the hosting security team. Surface which path was taken + in the proposal + recap. + +Non-GitHub forwarders (HackerOne, huntr, direct email relays) are +unaffected — they keep the [`tools/forwarder-relay/`](../../tools/forwarder-relay/README.md) +path. + +--- + +## Guardrails + +- **Propose-then-confirm every advisory write** — the advisory is a + surface on a public project. +- **Confidentiality** — private-advisory content never lands on a public + surface; it is already private, and so is the tracker. +- **Source of truth** — the tracker + CVE record is authoritative for CVE + fields; the advisory is reconciled *to* it. +- **No blind state writes** — never PATCH `state`/publish speculatively; a + success is permanent and public. Publishing is an explicit admin + hand-off, opt-in per tracker.