pvr-watchlist
General↓ 0 installsUpdated 92d ago
Curatedaaronjmars
Probe of repos on the security watchlist — check if private vulnerability reporting has been enabled, notify when status flips, re-submit any queued advisories or flag for re-research when draft was lost
SKILL.md preview
---
name: pvr-watchlist
category: dev
description: Probe of repos on the security watchlist — check if private vulnerability reporting has been enabled, notify when status flips, re-submit any queued advisories or flag for re-research when draft was lost
var: ""
tags: [security, meta]
requires: [GH_GLOBAL?]
---
> **${var}** — Optional `owner/repo` to probe on demand instead of running the full watchlist.
Today is ${today}. Read `memory/MEMORY.md` before starting.
## Voice
If `soul/SOUL.md` and `soul/STYLE.md` are populated, match the operator's voice in the notification. If empty or absent, use a clear, direct, neutral tone.
## Why this skill exists
When `vuln-scanner` finds a HIGH/CRITICAL issue in a repo with no PVR, no SECURITY.md, and no reachable maintainer contact, it has no safe disclosure channel — so it logs the finding in `memory/vuln-scanned.json` as `"channel": "skipped"` and marks the advisory as pending a PVR re-check. Without a weekly probe, those findings silently age until the responsible-disclosure window closes. This skill closes that loop.
Active watchlist: `memory/security-watchlist.md`
## Steps
### 1. Load the watchlist
Read `memory/security-watchlist.md`. Parse each row in the table:
```
| owner/repo | severity | short-title | first-checked | last-checked | status |
```
If `${var}` is set, skip the file and probe only that target (one-off mode).
If the watchlist is empty or the file doesn't exist:
```
PVRL_SKIP: watchlist empty
```
Log it and stop. No notification needed.
### 2. Probe each entry for PVR status
For each repo, run:
```bash
REPO="owner/repo"
gh api "repos/${REPO}/private-vulnerability-reporting" --jq '.enabled' 2>&1
```
Expected responses:
- `true` — PVR is now enabled. **This is the flip we're watching for.**
- `false` — PVR still disabled. Note it, move on.
- `404` — Repo may have been deleted / renamed / made private. Flag as `not-found`.
- `403` — Token lacks scope or it's a private repo. Flag as `access-denied`.
**Sandbox note:** `gh` CLI handles auth internally — no token-in-URL needed. If `gh api` is blocked by the sandbox, fall back to:
```bash
curl -s -H "Authorization: Bearer $GH_GLOBAL" \
"https://api.github.com/repos/${REPO}/private-vulnerability-reporting" | grep -o '"enabled":[a-z]*'
```
### 3. Handle PVR-enabled flips
For each repo where PVR flipped to `true`:
**a) Check for a recoverable draft**
Look in `memory/pending-disclosures/` for a file whose name starts with the repo slug (replacing `/` with `-`).
```bash
SLUG=$(echo "$REPO" | tr '/' '-')
ls memory/pending-disclosures/${SLUG}*.md 2>/dev/null
```
**b) If a draft exists and status is not `shipped`:**
Attempt auto-submission via the PVR **`/reports`** endpoint (NOT the bare
`/security-advisories` endpoint — that *creates* an advisory and needs
admin/security-manager rights on the target repo, so it `403`s on any repo you
don't own). Classic `repo` scope is sufficient.
```bash
gh api -X POST "repos/${REPO}/security-advisories/reports" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--input <draft-content-as-json>
```
Build the JSON body from the draft file fields:
- `summary` → first heading line
- `description` → full advisory body
- `severity` → from `**Severity:**` field
- `cwe_ids` → array from `**CWE:**` field (e.g. `["CWE-639"]`)
- `vulnerabilities` → **MANDATORY, non-empty** — `[{ "package": { "ecosystem": "<pip|npm|go|…|other>", "name": "<pkg>" } }]`. ⚠️ Omitting it makes the endpoint return **HTTP 500 (empty body)**, not a clean error (the docs wrongly mark it optional). This is the #1 PVR-submission failure; always include it. See `vuln-scanner` step 5b.
If the POST returns 201: mark draft as `status: submitted`, update `memory/vuln-scanned.json` channel to `pvr-submitted`, and note in the watchlist row `status: submitted`.
If the POST returns **500 (empty body)**: the `vulnerabilities` array is almost certainly missing/empty — add it and re-POST once before treating it as a real fai
…