Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
af5f4d9
Add PVR triage taskflow
anticomputer Mar 2, 2026
c233dac
Address PR review: add SPDX headers, pass GH_TOKEN in toolbox env
anticomputer Mar 2, 2026
6d97ef5
Add PVR triage batch scoring, write-back, and reporter reputation tra…
anticomputer Mar 3, 2026
9dd96c2
Add run_pvr_triage.sh: local test and demo script for pvr triage task…
anticomputer Mar 3, 2026
0d83c6f
pvr_triage_batch: skip already-triaged advisories by default
anticomputer Mar 3, 2026
0568973
Add SCORING.md: reference for batch priority, quality signals, fast-c…
anticomputer Mar 3, 2026
cbe2184
Address PR review feedback
anticomputer Mar 3, 2026
e0e29a8
Self-review: robustness and logic fixes
anticomputer Mar 3, 2026
7fd9074
fetch_file_at_ref: raise default length from 50 to 100 lines
anticomputer Mar 3, 2026
9436f3d
pvr-triage: add bulk respond taskflow, 3-path fast-close, reputation …
anticomputer Mar 3, 2026
57c1bbf
SCORING.md: update 3-path decision table and reputation thresholds
anticomputer Mar 3, 2026
fcb3654
Fix ruff linter errors in test_pvr_mcp
anticomputer Mar 3, 2026
388f145
Fix advisory state: incoming PVRs use triage state, not draft
anticomputer Mar 3, 2026
c84904f
Fix advisory state API: reject→closed, remove withdraw_pvr_advisory
anticomputer Mar 3, 2026
f5b9d26
Add accept_pvr_advisory: triage→draft state transition
anticomputer Mar 3, 2026
2c331cb
Update overview doc: accept/reject state transitions in diagram and o…
anticomputer Mar 3, 2026
c1b5dba
Remove comment posting: no GitHub REST API for advisory comments
anticomputer Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions docs/pvr_triage_overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# PVR Triage Taskflows — Overview

> 30-minute sync reference. Last updated: 2026-03-03.

---

## The Problem

OSS maintainers get flooded with low-quality vulnerability reports via GitHub's Private Vulnerability Reporting (PVR). Most are vague, duplicated, or AI-generated. Reviewing each one manually is expensive.

---

## The Solution: 4 Taskflows

```
┌─────────────────────────────────────────────────────────────┐
│ INBOX │
│ (GHSAs in triage state via GitHub PVR) │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────┐
│ pvr_triage_batch │ "What's in my inbox?"
│ │
│ • List triage GHSAs │
│ • Score each by │
│ severity + quality │
│ • Show Age (days) │
│ • Rank: highest first │
│ (oldest wins ties) │
└────────────┬────────────┘
│ ranked queue saved to REPORT_DIR
┌─────────────────────────┐
│ pvr_triage │ "Is this real?"
│ (one advisory) │
│ │
│ Task 1: init │
│ Task 2: fetch & parse │
│ Task 3: quality gate ──┼──► fast-close? ──► skip to Task 7
│ Task 4: verify code │
│ Task 5: write report │
│ Task 6: save report │
│ Task 7: draft response │
│ Task 8: save + record │
└────────────┬────────────┘
│ _triage.md + _response_triage.md saved
Maintainer reviews
(edits draft if needed)
┌────────┴────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ pvr_respond │ │ pvr_respond_batch │
│ (one at a time) │ │ (all at once) │
│ │ │ │
│ confirm-gated: │ │ • list_pending │
│ accept (→draft) │ │ • for each: │
│ reject (→closed)│ │ - confirm-gated │
│ │ │ state change │
│ mark as applied │ │ - mark as applied │
│ post draft │ │ • post drafts │
│ manually via UI │ │ manually via UI │
└──────────────────┘ └──────────────────────┘
```

---

## The Quality Gate (Task 3) — Key Logic

```
Reporter has history?
├── HIGH TRUST ──────────────────► Always full verification
│ (≥60% confirmed, ≤20% low)
├── SKEPTICISM ──────────────────► Fast-close if 0 quality signals
│ (≤20% confirmed OR ≥50% low) (no prior report needed)
└── NORMAL / NEW ────────────────► Fast-close only if:
0 quality signals
AND prior similar report exists
```

**Quality signals:** file paths cited · PoC provided · line numbers cited

**Fast-close effect:** skip code verification → use canned response template requesting specifics

---

## Scoring (batch)

```
priority_score = severity_weight + quality_weight

severity: critical=4 high=3 medium=2 low=1
quality: +1 per signal (files, PoC, lines) → max +3

≥5 Triage Immediately
≥3 Triage Soon
2 Triage
≤1 Likely Low Quality — Fast Close
```

---

## Output Files (all in REPORT_DIR)

| File | Written by | What it is |
|---|---|---|
| `GHSA-xxxx_triage.md` | pvr_triage | Full analysis report |
| `GHSA-xxxx_response_triage.md` | pvr_triage | Draft reply to reporter |
| `GHSA-xxxx_response_sent.md` | pvr_respond / batch | State-transition applied marker (idempotent) |
| `batch_queue_<repo>_<date>.md` | pvr_triage_batch | Ranked inbox table |

---

## Reporter Reputation (background)

Every completed triage records **verdict + quality** against the reporter's GitHub login in a local SQLite DB. Score feeds back into the next triage's quality gate automatically. No manual configuration.

---

## One-liner workflow

```bash
./scripts/run_pvr_triage.sh batch owner/repo # see inbox
./scripts/run_pvr_triage.sh triage owner/repo GHSA-xxx # analyse one
./scripts/run_pvr_triage.sh respond owner/repo GHSA-xxx accept # accept one (triage→draft)
./scripts/run_pvr_triage.sh respond owner/repo GHSA-xxx reject # reject one (triage→closed)
./scripts/run_pvr_triage.sh respond_batch owner/repo reject # bulk state transition
# Then post each *_response_triage.md manually via the advisory URL
```

---

## Further reading

- [`taskflows/pvr_triage/README.md`](../src/seclab_taskflows/taskflows/pvr_triage/README.md) — full usage docs for all four taskflows
- [`taskflows/pvr_triage/SCORING.md`](../src/seclab_taskflows/taskflows/pvr_triage/SCORING.md) — authoritative scoring reference and fast-close decision tables
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ ignore = [
"PLR1730", # Replace `if` statement with `min()`
"PLR2004", # Magic value used in comparison
"PLW0602", # Using global for variable but no assignment is done
"PLW0603", # Using the global statement to update a variable is discouraged
"PLW1508", # Invalid type for environment variable default
"PLW1510", # `subprocess.run` without explicit `check` argument
"RET504", # Unnecessary assignment before `return` statement
Expand All @@ -101,6 +102,7 @@ ignore = [
"RUF015", # Prefer `next(iter())` over single element slice
"S607", # Starting a process with a partial executable path
"SIM101", # Use a ternary expression instead of if-else-block
"SIM105", # Use contextlib.suppress (false positive: try block contains assignment)
"SIM114", # Combine `if` branches using logical `or` operator
"SIM117", # Use a single `with` statement with multiple contexts
"SIM118", # Use `key in dict` instead of `key in dict.keys()`
Expand All @@ -113,3 +115,12 @@ ignore = [
"W291", # Trailing whitespace
"W293", # Blank line contains whitespace
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = [
"PLC0415", # Import not at top of file (deliberate in setUp/test methods for patching)
"PT009", # Use assert instead of unittest-style assertEqual (TestCase subclass)
"PT027", # Use pytest.raises instead of assertRaises (TestCase subclass)
"S101", # Use of assert (standard in pytest)
"SLF001", # Private member accessed (tests legitimately access module internals)
]
208 changes: 208 additions & 0 deletions scripts/run_pvr_triage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/bin/bash
# SPDX-FileCopyrightText: GitHub, Inc.
# SPDX-License-Identifier: MIT
#
# Local test / demo script for the PVR triage taskflows.
#
# Usage:
# ./scripts/run_pvr_triage.sh batch <owner/repo>
# ./scripts/run_pvr_triage.sh triage <owner/repo> <GHSA-xxxx-xxxx-xxxx>
# ./scripts/run_pvr_triage.sh respond <owner/repo> <GHSA-xxxx-xxxx-xxxx> <accept|comment|reject>
# ./scripts/run_pvr_triage.sh respond_batch <owner/repo> <accept|comment|reject>
# ./scripts/run_pvr_triage.sh demo <owner/repo>
#
# Environment (any already-set values are respected):
# GH_TOKEN — GitHub token; falls back to: gh auth token
# AI_API_TOKEN — AI API key (required, must be set before running)
# AI_API_ENDPOINT — defaults to https://api.githubcopilot.com
# REPORT_DIR — defaults to ./reports
# LOG_DIR — defaults to ./logs

set -euo pipefail

__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__root="$(cd "${__dir}/.." && pwd)"

# ---------------------------------------------------------------------------
# Usage (defined early so --help can fire before env validation)
# ---------------------------------------------------------------------------

usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args]

Commands:
batch <owner/repo>
Score unprocessed triage advisories and save a ranked queue table to REPORT_DIR.
Advisories already present in REPORT_DIR are skipped.

triage <owner/repo> <GHSA-xxxx-xxxx-xxxx>
Run full triage on one advisory: verify code, generate report + response draft.

respond <owner/repo> <GHSA-xxxx-xxxx-xxxx> <action>
Apply a state transition to a GitHub advisory. action = accept | reject
Requires pvr_triage to have been run first for the given GHSA.
Post the response draft manually via the advisory URL after running.

respond_batch <owner/repo> <action>
Scan REPORT_DIR and apply state transitions to all pending advisories.
action = accept | reject

demo <owner/repo>
Full pipeline on the given repo (batch → triage on first triage advisory → report preview).
Does not post anything to GitHub.

Environment:
GH_TOKEN — GitHub token; falls back to: gh auth token
AI_API_TOKEN — AI API key (required, must be set before running)
AI_API_ENDPOINT — defaults to https://api.githubcopilot.com
REPORT_DIR — defaults to ./reports
LOG_DIR — defaults to ./logs
EOF
}

case "${1:-}" in
-h|--help|help|"") usage; exit 0 ;;
esac

# ---------------------------------------------------------------------------
# Environment setup
# ---------------------------------------------------------------------------

# Prepend local venv to PATH if present (resolves 'python' for MCP servers)
if [ -d "${__root}/.venv/bin" ]; then
export PATH="${__root}/.venv/bin:${PATH}"
fi

# GitHub token
if [ -z "${GH_TOKEN:-}" ]; then
if command -v gh &>/dev/null; then
GH_TOKEN="$(gh auth token 2>/dev/null)" || true
fi
if [ -z "${GH_TOKEN:-}" ]; then
echo "ERROR: GH_TOKEN not set and 'gh auth token' failed." >&2
exit 1
fi
export GH_TOKEN
fi

# AI API token
if [ -z "${AI_API_TOKEN:-}" ]; then
echo "ERROR: AI_API_TOKEN is not set." >&2
exit 1
fi

export AI_API_ENDPOINT="${AI_API_ENDPOINT:-https://api.githubcopilot.com}"

export REPORT_DIR="${REPORT_DIR:-${__root}/reports}"
mkdir -p "${REPORT_DIR}"

export LOG_DIR="${LOG_DIR:-${__root}/logs}"
mkdir -p "${LOG_DIR}"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

run_agent() {
python -m seclab_taskflow_agent "$@"
}

# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

cmd_batch() {
local repo="${1:?Usage: $0 batch <owner/repo>}"
echo "==> Scoring inbox for ${repo} ..."
run_agent \
-t seclab_taskflows.taskflows.pvr_triage.pvr_triage_batch \
-g "repo=${repo}"
}

cmd_triage() {
local repo="${1:?Usage: $0 triage <owner/repo> <GHSA>}"
local ghsa="${2:?Usage: $0 triage <owner/repo> <GHSA>}"
echo "==> Triaging ${ghsa} in ${repo} ..."
run_agent \
-t seclab_taskflows.taskflows.pvr_triage.pvr_triage \
-g "repo=${repo}" \
-g "ghsa=${ghsa}"
}

cmd_respond() {
local repo="${1:?Usage: $0 respond <owner/repo> <GHSA> <action>}"
local ghsa="${2:?Usage: $0 respond <owner/repo> <GHSA> <action>}"
local action="${3:?Usage: $0 respond <owner/repo> <GHSA> <action>}"
case "${action}" in
accept|reject) ;;
*) echo "ERROR: action must be accept or reject" >&2; exit 1 ;;
esac
echo "==> Responding to ${ghsa} in ${repo} (action=${action}) ..."
run_agent \
-t seclab_taskflows.taskflows.pvr_triage.pvr_respond \
-g "repo=${repo}" \
-g "ghsa=${ghsa}" \
-g "action=${action}"
}

cmd_respond_batch() {
local repo="${1:?Usage: $0 respond_batch <owner/repo> <action>}"
local action="${2:?Usage: $0 respond_batch <owner/repo> <action>}"
case "${action}" in
accept|reject) ;;
*) echo "ERROR: action must be accept or reject" >&2; exit 1 ;;
esac
echo "==> Bulk respond for ${repo} (action=${action}) ..."
run_agent \
-t seclab_taskflows.taskflows.pvr_triage.pvr_respond_batch \
-g "repo=${repo}" \
-g "action=${action}"
}

cmd_demo() {
local repo="${1:?Usage: $0 demo <owner/repo>}"

# Pick the first triage advisory, or bail if none
local ghsa
ghsa="$(gh api "/repos/${repo}/security-advisories?state=triage&per_page=1" \
--jq '.[0].ghsa_id // empty' 2>/dev/null)" || true

if [ -z "${ghsa}" ]; then
echo "No triage advisories found in ${repo}. Create one at:" >&2
echo " https://github.com/${repo}/security/advisories/new" >&2
exit 1
fi

echo "==> Demo: ${repo} advisory: ${ghsa}"
echo

echo "--- Step 1: batch inbox score ---"
cmd_batch "${repo}"
echo

echo "--- Step 2: full triage ---"
cmd_triage "${repo}" "${ghsa}"
echo

echo "--- Reports written to ${REPORT_DIR} ---"
ls -1 "${REPORT_DIR}"/*.md 2>/dev/null || true
echo
echo "To accept (triage → draft) or reject (triage → closed):"
echo " $0 respond ${repo} ${ghsa} accept"
echo " $0 respond ${repo} ${ghsa} reject"
echo "Then post the response draft manually via the advisory URL."
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

case "${1:-}" in
batch) shift; cmd_batch "$@" ;;
triage) shift; cmd_triage "$@" ;;
respond) shift; cmd_respond "$@" ;;
respond_batch) shift; cmd_respond_batch "$@" ;;
demo) shift; cmd_demo "$@" ;;
*) echo "ERROR: unknown command '${1}'" >&2; usage; exit 1 ;;
esac
Loading