mirror of
https://github.com/penpot/penpot.git
synced 2026-08-22 12:49:26 +00:00
✨ Add advisories access helper to gh tool
This commit is contained in:
parent
ddc98bdd47
commit
4d90fe9126
@ -9,6 +9,7 @@ repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
||||
- Finding issues with no milestone.
|
||||
- Fetching PR details by number or by milestone.
|
||||
- Comparing milestone issues against CHANGES.md to find missing entries.
|
||||
- Listing or inspecting GitHub Security Advisories (GHSA).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@ -72,6 +73,30 @@ python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||
|
||||
**Output**: JSON array to stdout; progress to stderr.
|
||||
|
||||
### `advisories`
|
||||
|
||||
List or inspect GitHub Security Advisories for the repository.
|
||||
|
||||
```bash
|
||||
# List all advisories (summary view)
|
||||
python3 scripts/gh.py advisories
|
||||
|
||||
# Filter by severity
|
||||
python3 scripts/gh.py advisories --severity critical
|
||||
|
||||
# Filter by state
|
||||
python3 scripts/gh.py advisories --state triage
|
||||
|
||||
# Get full detail for a single advisory
|
||||
python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7
|
||||
```
|
||||
|
||||
**Summary output fields**: ghsa_id, cve_id, severity, cvss_score, state, summary, cwes, published_at, closed_at, url.
|
||||
|
||||
**Detail output** (single advisory) adds: description, vulnerabilities (package, version ranges), credits, timestamps.
|
||||
|
||||
**Output**: JSON to stdout; progress to stderr.
|
||||
|
||||
## Key principles
|
||||
|
||||
- All output is JSON — pipe into `jq` or other tools for further processing.
|
||||
|
||||
121
scripts/gh.py
121
scripts/gh.py
@ -5,8 +5,9 @@ gh.py — Multi-purpose CLI helper for penpot/penpot GitHub operations.
|
||||
Uses GitHub GraphQL and REST APIs via the authenticated ``gh`` CLI.
|
||||
|
||||
Subcommands:
|
||||
issues List issues in a milestone (or unassigned with milestone=none)
|
||||
prs Fetch details for one or more PRs (by number or milestone)
|
||||
issues List issues in a milestone (or unassigned with milestone=none)
|
||||
prs Fetch details for one or more PRs (by number or milestone)
|
||||
advisories List or inspect GitHub security advisories
|
||||
|
||||
Usage:
|
||||
python3 scripts/gh.py issues <milestone-title> (default: state=closed)
|
||||
@ -23,6 +24,9 @@ Usage:
|
||||
cat prs.txt | python3 scripts/gh.py prs --stdin
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" (default: state=merged)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||
python3 scripts/gh.py advisories (list all advisories)
|
||||
python3 scripts/gh.py advisories --severity critical (filter by severity)
|
||||
python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7 (single advisory detail)
|
||||
|
||||
Prerequisites:
|
||||
- gh CLI authenticated (gh auth status)
|
||||
@ -63,6 +67,16 @@ def run_gh_graphql(query: str, variables: dict) -> Any:
|
||||
return body["data"]
|
||||
|
||||
|
||||
def run_gh_rest(path: str) -> Any:
|
||||
"""Run a REST API call via ``gh api``."""
|
||||
cmd = ["gh", "api", path]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"gh error: {result.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Shared: milestone lookup
|
||||
# ─────────────────────────────────────────────
|
||||
@ -581,6 +595,93 @@ def cmd_prs(args: argparse.Namespace) -> None:
|
||||
print(json.dumps(all_results, indent=2))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Subcommand: advisories
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def fetch_advisories() -> list[dict]:
|
||||
"""Fetch all security advisories for the repository via REST API."""
|
||||
return run_gh_rest(f"repos/{REPO}/security-advisories")
|
||||
|
||||
|
||||
def fetch_advisory(ghsa_id: str) -> dict:
|
||||
"""Fetch a single security advisory by GHSA ID."""
|
||||
return run_gh_rest(f"repos/{REPO}/security-advisories/{ghsa_id}")
|
||||
|
||||
|
||||
def format_advisory_summary(adv: dict) -> dict:
|
||||
"""Extract a summary view of an advisory."""
|
||||
return {
|
||||
"ghsa_id": adv["ghsa_id"],
|
||||
"cve_id": adv.get("cve_id"),
|
||||
"severity": adv.get("severity"),
|
||||
"cvss_score": (adv.get("cvss") or {}).get("score"),
|
||||
"state": adv.get("state"),
|
||||
"summary": adv.get("summary"),
|
||||
"cwes": [c["cwe_id"] for c in adv.get("cwes", [])],
|
||||
"published_at": adv.get("published_at"),
|
||||
"closed_at": adv.get("closed_at"),
|
||||
"url": adv.get("html_url"),
|
||||
}
|
||||
|
||||
|
||||
def format_advisory_detail(adv: dict) -> dict:
|
||||
"""Extract full detail view of an advisory."""
|
||||
summary = format_advisory_summary(adv)
|
||||
summary["description"] = adv.get("description")
|
||||
summary["vulnerabilities"] = [
|
||||
{
|
||||
"package": v.get("package", {}).get("name"),
|
||||
"vulnerable_version_range": v.get("vulnerable_version_range"),
|
||||
"patched_versions": v.get("patched_versions"),
|
||||
}
|
||||
for v in adv.get("vulnerabilities", [])
|
||||
]
|
||||
summary["credits"] = [
|
||||
{"login": c.get("user", {}).get("login"), "type": c.get("type")}
|
||||
for c in adv.get("credits_detailed", [])
|
||||
]
|
||||
summary["created_at"] = adv.get("created_at")
|
||||
summary["updated_at"] = adv.get("updated_at")
|
||||
summary["withdrawn_at"] = adv.get("withdrawn_at")
|
||||
return summary
|
||||
|
||||
|
||||
def cmd_advisories(args: argparse.Namespace) -> None:
|
||||
"""Handle the ``advisories`` subcommand."""
|
||||
|
||||
# ── Single advisory detail ──────────────────────────────
|
||||
if args.ghsa_id:
|
||||
ghsa_id = args.ghsa_id.upper()
|
||||
if not ghsa_id.startswith("GHSA-"):
|
||||
ghsa_id = f"GHSA-{ghsa_id}"
|
||||
print(f"Fetching advisory {ghsa_id}...", file=sys.stderr)
|
||||
adv = fetch_advisory(ghsa_id)
|
||||
print(json.dumps(format_advisory_detail(adv), indent=2))
|
||||
return
|
||||
|
||||
# ── List all advisories ─────────────────────────────────
|
||||
print("Fetching security advisories...", file=sys.stderr)
|
||||
advisories = fetch_advisories()
|
||||
print(f"Fetched {len(advisories)} advisories", file=sys.stderr)
|
||||
|
||||
results = [format_advisory_summary(adv) for adv in advisories]
|
||||
|
||||
# Apply filters
|
||||
if args.severity:
|
||||
sev = args.severity.lower()
|
||||
results = [r for r in results if (r.get("severity") or "").lower() == sev]
|
||||
print(f"After severity filter ({sev}): {len(results)} advisories", file=sys.stderr)
|
||||
|
||||
if args.state:
|
||||
st = args.state.lower()
|
||||
results = [r for r in results if (r.get("state") or "").lower() == st]
|
||||
print(f"After state filter ({st}): {len(results)} advisories", file=sys.stderr)
|
||||
|
||||
print(json.dumps(results, indent=2))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# CLI entrypoint
|
||||
# ─────────────────────────────────────────────
|
||||
@ -645,6 +746,22 @@ def main() -> None:
|
||||
)
|
||||
p_prs.set_defaults(func=cmd_prs)
|
||||
|
||||
# --- advisories ---
|
||||
p_adv = sub.add_parser("advisories", help="List or inspect GitHub security advisories")
|
||||
p_adv.add_argument(
|
||||
"ghsa_id", nargs="?",
|
||||
help="GHSA ID to fetch (e.g. 'GHSA-xvj6-fh9w-gjw7'); omit to list all"
|
||||
)
|
||||
p_adv.add_argument(
|
||||
"--severity", choices=["critical", "high", "medium", "low"],
|
||||
help="Filter by severity level"
|
||||
)
|
||||
p_adv.add_argument(
|
||||
"--state", choices=["triage", "draft", "published", "closed", "withdrawn"],
|
||||
help="Filter by advisory state"
|
||||
)
|
||||
p_adv.set_defaults(func=cmd_advisories)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user