Skip to content

GHSA draft cards: place on the board for the affected line, and include affected versions + reporter #2462

Description

@cliffhall

Blocked until we get a Github app with a credential that can see the advisories and write to the board.

Problem

GHSA draft cards are created on the wrong board and carry too little triage information:

  1. Every advisory draft goes on Add tab and approval flow for server -> client sampling #28, whatever line it affects. GHSA-6x67-4w4w-9f3w affects @modelcontextprotocol/inspector@0.22.0 (v1), but its card was put on the v2 board. When the cards were checked on 2026-09-23, 9 of the 16 GHSA drafts on Add tab and approval flow for server -> client sampling #28 were v1-only.
  2. The card shows neither the affected versions nor the reporter. To decide which line a card belongs to, a maintainer has to follow the link into the advisory.

Expected behavior

Whatever creates the draft cards should:

  • Put each card on the board for the line it affects, using the advisory's vulnerabilities[].vulnerable_version_range. Every version below 2.0.0 is v1.x (0.x and 1.x, including @modelcontextprotocol/inspector-server@1.0.x). v1 is deprecated but still gets maintenance releases for Urgent and High security issues.
    • A range entirely below 2.0.0 → board get tools working #11, Status only, since get tools working #11 has no Priority field.
    • A range that reaches 2.x → board Add tab and approval flow for server -> client sampling #28.
    • A range spanning both lines (for example >= 0.14.1 or <= 2.4.0) → decide explicitly. One card per line is the natural reading, since the fix ships on each line separately.
    • A missing or null range → do not default to v2. Fall back to the code paths the report cites (server/src/… and client/src/… are v1; clients/… and core/… are v2), or flag the card for a maintainer.
  • Include these lines in the card body, under the **Advisory:** link:
    • **Affected versions:** followed by each package and range (for example `@modelcontextprotocol/inspector` `0.22.0`)
    • **Reporter:** @<login>, written with the @ so it links. The login comes from the advisory's credits_detailed[type=reporter], falling back to author.

There is no nightly job, so new advisories go uncarded

No nightly job creates these cards, and none ever has. A nightly was requested on 2026-09-18, and the idea was dropped the same session. The job would need organization projects: write, which GITHUB_TOKEN cannot hold, and the org has no PROJECT_TOKEN. All 16 existing drafts were created by hand in one batch on 2026-09-18 between 23:46 and 23:47 UTC, using a maintainer's local gh token. No card has been created automatically since.

So any advisory reported after that batch goes uncarded until someone runs the skill's step 1 by hand. On 2026-09-23, two advisories awaiting triage had no card on either board:

Advisory Reported Severity Affected versions Board it belongs on
GHSA-rgh2-rcwx-8qr6 2026-09-22 medium @modelcontextprotocol/inspector-server >= 1.0.0, <= 1.0.2 #11 (v1)
GHSA-hr3j-35f8-r7xw 2026-09-22 critical @modelcontextprotocol/inspector >= 2.0.0, <= 2.7.0 #28 (v2)

Fix: make the nightly real. Any of these would work:

  1. Get the credential described below (a GitHub App, or a fine-grained PAT stored as PROJECT_TOKEN, the secret dependabot-alerts.yml already expects), and use it in a scheduled advisory workflow.
    • The dependabot sweep already reads secrets.PROJECT_TOKEN, but the secret does not exist. The repo has no Actions secrets, and the only org secret it can see is ANTHROPIC_API_KEY. Its 2026-09-23 run logged PROJECT_TOKEN: as empty. The sweep works without it: it files its issues with GITHUB_TOKEN and leaves the card to /issue-triage (chore(deps): bump @vitest/mocker to 4.1.11 in clients/web/package-lock.json (1 advisory) #2310chore(deps): bump vitest to 4.1.11 in package-lock.json (1 advisory) #2313 were filed that way and have no board card). An advisory sweep has no such fallback. It cannot file a public issue, because that would disclose the advisory, so the draft card is its only output, and that needs the token. One secret, created by an org admin, turns on the dependabot sweep's board write and unblocks this.
    • Unverified: can GITHUB_TOKEN read private repository advisories? GET /repos/{owner}/{repo}/security-advisories needs repository security advisory read access, and it is not yet confirmed that workflow permissions: can grant that to GITHUB_TOKEN. Test this on a runner before designing around it (compare vulnerability-alerts: read, which turned out to be valid despite stale public docs saying otherwise). If GITHUB_TOKEN cannot read advisories, the sweep reads them with the credential instead (the GitHub App's permission list already includes advisory read), and option 3 below needs the credential as well. It also needs a deterministic script, like the other sweeps, with no model. The script:
    • lists advisories in triage, paginated
    • removes duplicates against both boards by the [GHSA-…] title prefix. It must check that the listing is complete (totalCount), because board Add tab and approval flow for server -> client sampling #28 already has more items than the default --limit returns.
    • chooses the board from the version range
    • writes the title and body
    • sets Status to Incoming
  2. A scheduled Claude routine running under a maintainer's account, which already has the project scope. This needs no new secret, but it depends on one person's token and cannot run as part of CI.
  3. Keep it manual, and make that visible. A scheduled workflow with read-only scope would compare the advisories in triage against the boards and fail loudly when one has no card. It cannot write the card, but a gap would stop going unnoticed. This needs no board write, so it works with GITHUB_TOKEN alone, but only if that token can read advisories (the unverified question under option 1).

Option 1 is the only one that fully closes the gap. Option 3 is worth having in any case, as the check that catches a credential that has expired, been revoked, or been uninstalled.

The credential: Actions needs to be able to write to the org project board

What we need is a credential that lets GitHub Actions in the Inspector repo write to our org project board, stored as an org Actions secret available to the Inspector repo. The built-in Actions token (GITHUB_TOKEN) is scoped to the repo and can never be granted write access to an org-owned project board, whatever permissions: says. The sweeps may also need the credential to read the repo's security advisories; that part is unverified (see option 1). PROJECT_TOKEN is only the name of the secret dependabot-alerts.yml reads, not a kind of token.

  • Preferred: a GitHub App installed on the org, with

    • Organization → Projects: read & write
    • Repository → Issues: read & write
    • Repository → Repository security advisories: read

    installed on the inspector repo only, with its app id and private key stored as org Actions secrets limited to the inspector repo. Each run mints a short-lived token (actions/create-github-app-token), and nothing is tied to one person or expires. With this route, each workflow needs a token-minting step whose output takes PROJECT_TOKEN's place.

  • Alternative: a fine-grained personal access token with the org as resource owner and the same permissions, stored as an org Actions secret named PROJECT_TOKEN limited to the inspector repo. It is quicker to set up and works with the existing secrets.PROJECT_TOKEN wiring as it is, but it belongs to one person and has to be renewed.

This has been requested from a core maintainer (2026-09-23).

Setting up the GitHub App

The App is not a program. Here it is only an identity with permissions attached: no code, no server, nothing running anywhere. The work is still done by our existing workflows, on GitHub's normal Actions runners.

One-time setup by an org admin, all in the GitHub UI:

  1. Register it under the org's Settings → Developer settings → GitHub Apps → New GitHub App. Give it a name (for example "Inspector Board Bot") and a homepage URL; the repo URL is fine.
  2. Untick "Webhook: Active". That is what makes it identity-only: GitHub has nothing to send events to, so no server is needed.
  3. Set its permissions: Organization → Projects: read & write; Repository → Issues: read & write; Repository → Repository security advisories: read.
  4. Generate a private key. GitHub downloads a .pem file.
  5. Install it on the org, limited to the inspector repo.
  6. Store the app id and the .pem contents as org Actions secrets available to the inspector repo.

How a workflow uses it. Each job that needs board access adds one step at the start. The step mints an installation token that is good for one hour, and the scripts use it exactly as they would a PAT:

- uses: actions/create-github-app-token@v2
  id: app
  with:
    app-id: ${{ secrets.BOARD_APP_ID }}
    private-key: ${{ secrets.BOARD_APP_KEY }}
    owner: modelcontextprotocol
- run: node scripts/dependabot-alerts.mjs
  env:
    PROJECT_TOKEN: ${{ steps.app.outputs.token }}

The secret names are placeholders; use whatever the admin chooses. ⚠️ Keep the token-minting step out of any job that runs a model. AGENTS.md's SDK-watch rule, "no model runs in a write-capable job", applies to this token as much as to issues: write.

What it gets us over a PAT:

  • Board changes are credited to the App, not to a maintainer.
  • There is no expiry to renew; the key stays valid until someone revokes it.
  • It survives a maintainer leaving the org.
  • Each run's token lasts an hour, so a leaked log is worth very little.

The cost is about six setup screens instead of two, plus one step per workflow.

The same credential also lets the other sweeps add their own cards

No sweep has ever added a card by itself. Every card on #28 and #11 was added by a maintainer, by hand or in a session running under their gh token.

Sweep Files Adds its own card today? With the credential
dependabot-alerts.yml (daily) one issue per bump No, because the secret is missing (the 2026-09-23 run logged an empty PROJECT_TOKEN:) Works right away; the placement code already exists
sdk-watch.yml (nightly) one issue per SDK release No. It has no board code by design (scripts/sdk-watch.mjs, "No board write"). For example, #2309 was filed 2026-09-10 05:44 UTC and added to #28 by hand the next day. Needs a board step
dependency-refresh.yml (monthly) one tracking issue No, by design Needs a board step
GHSA drafts (this issue) a draft card only Not automated at all Needs the new sweep

For those two, add a board step that follows the dependabot sweep's pattern: add the card when the credential is present, otherwise leave the issue for /issue-triage. Update the "never boards" wording in AGENTS.md and in the scripts' header comments to match.

What has to change

The process is currently step 1 of the security-advisory skill, done by hand. No workflow or routine creates these cards: PROJECT_TOKEN does not exist in this org, and GITHUB_TOKEN cannot hold organization projects: write. A nightly job that writes to the board needs an org-project PAT, and that decision belongs in this issue. The docs currently encode "advisory drafts live on #28 only", so each of these places needs updating:

  • .claude/skills/security-advisory/SKILL.md:
    • step 1: choosing the board from the affected range, and the new body lines
    • step 2: re-checking the range
    • step 6: the v1-only path no longer deletes a Add tab and approval flow for server -> client sampling #28 draft. The v1 draft is already on get tools working #11. A draft card has no Convert-to-issue that targets the right repo and labels, so decide whether to convert on get tools working #11 or to file an issue and delete the draft.
    • the "Do not propose a nightly workflow" note, if a nightly is adopted with a PAT
    • the "Do not copy the vulnerability description onto the card" warning in step 1. That rule assumes the board is visible to more people than the advisory is. That is no longer true: advisory access and board access are both limited to maintainers and core maintainers. So the rule should say that a card carries the affected versions and the reporter (@login), and the skill should stop presenting the two audiences as different. Also update the other places that repeat the claim: step 1's reason for keeping the score arithmetic free of explanations, and the issue-triage note that the boards are private.
  • .claude/skills/board-ops/SKILL.md: the GHSA lookup searches get tools working #11 as well as Add tab and approval flow for server -> client sampling #28 ("Advisory drafts live on Add tab and approval flow for server -> client sampling #28 only").
  • .claude/skills/issue-triage/SKILL.md: the audit's [GHSA- draft carve-out must accept drafts on get tools working #11 instead of reporting them as misplaced.
  • AGENTS.md → Issue-driven Work Style: update the advisory draft-card exception, which currently says v1 is filed on get tools working #11 and the Add tab and approval flow for server -> client sampling #28 draft is deleted.
  • Get the credential (GitHub App preferred, or a fine-grained PAT named PROJECT_TOKEN; see above), stored as org Actions secrets limited to the inspector repo. Requested from a core maintainer on 2026-09-23.
  • Add a board step to sdk-watch.mjs and dependency-refresh.mjs, following the dependabot sweep's pattern, and update the "never boards" wording in AGENTS.md and in the scripts' header comments.
  • Adopt one of the options above. For option 1: wire in the credential, add the workflow and script, and write tests for version range → board classification, for duplicate removal against both boards, and for the incomplete-listing guard.
  • Card the two uncarded advisories in the table above. Done by hand on 2026-09-23: GHSA-hr3j on Add tab and approval flow for server -> client sampling #28, GHSA-rgh2 on get tools working #11.

Current state (cleaned up by hand on 2026-09-23)

The v1-only drafts on #28 were deleted and recreated on #11, and every GHSA draft on either board now has the affected-versions and reporter lines. There is no API to move a project item between projects, so moving a card means deleting it and creating it again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

blockedchoreMaintenance: deps, build tooling, CI, cleanup — no user-facing behavior changev2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions