Created
June 2, 2026 18:52
-
-
Save minrk/8b63685611793498fb38934aacdb8006 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # (c) 2026 Min RK | |
| # License: BSD-3-Clause | |
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = [ | |
| # "click", | |
| # "rich", | |
| # ] | |
| # /// | |
| """ | |
| List security advisories for a github org | |
| Usage: | |
| ./list-security-advisories [org] | |
| Default org is jupyterhub because I wrote this. | |
| """ | |
| import json | |
| from datetime import datetime, timedelta, UTC | |
| import click | |
| from rich.console import Console | |
| from rich.table import Table, Column | |
| from subprocess import check_output | |
| def _fmt_age(age: timedelta) -> str: | |
| """format a timedelta as a simple age in days""" | |
| if age < timedelta(days=7): | |
| color = "[green]" | |
| elif age > timedelta(days=30): | |
| color = "[red]" | |
| else: | |
| color = "" | |
| if age.days == 1: | |
| s = "yesterday" | |
| elif age.days: | |
| s = f"{age.days} days ago" | |
| else: | |
| h = age.seconds // 3600 | |
| m = age.seconds // 60 | |
| s = f"{h:02}:{m:02} ago" | |
| return f"{color}{s}" | |
| def _sort_key(advisory) -> tuple[int, int, str]: | |
| """construct sort order for advisories | |
| first by state, then by severity, then by date | |
| """ | |
| state_order = [ | |
| "triage", | |
| "draft", | |
| "published", | |
| "closed", | |
| ] | |
| severity_order = [ | |
| None, | |
| "critical", | |
| "high", | |
| "medium", | |
| "low", | |
| ] | |
| state_key = state_order.index(advisory["state"]) | |
| severity_key = severity_order.index(advisory["severity"]) | |
| return ( | |
| state_key, | |
| severity_key, | |
| advisory["created_at"], | |
| ) | |
| def _color_word(word: str) -> str: | |
| """ | |
| Pick a color for keyword highlighting | |
| """ | |
| color = "" | |
| match word: | |
| case "critical": | |
| color = "[red]" | |
| case "high": | |
| color = "[magenta]" | |
| case "triage": | |
| color = "[green]" | |
| return f"{color}{word}" | |
| @click.command() | |
| @click.argument("orgs", nargs=-1, required=True) | |
| @click.option( | |
| "--all", | |
| default=False, | |
| type=bool, | |
| help="include all (default: just triage and draft)", | |
| ) | |
| def main(orgs: str, all: bool = False): | |
| # fetch advisories for a given org | |
| # (assumes gh cli is authenticated) | |
| advisories = [] | |
| for org in orgs: | |
| advisory_json = check_output( | |
| ["gh", "api", f"/orgs/{org}/security-advisories"], text=True | |
| ) | |
| org_advisories = json.loads(advisory_json) | |
| for advisory in org_advisories: | |
| advisory['org'] = org | |
| advisories.extend(org_advisories) | |
| # import pprint | |
| # pprint.pprint(advisories[-1]) | |
| # return | |
| advisories = sorted(advisories, key=_sort_key) | |
| table = Table( | |
| "State", | |
| "Severity", | |
| "Org", | |
| "Title", | |
| "Created", | |
| "Updated", | |
| Column("URL", no_wrap=True), | |
| title=f"{len(advisories)} advisories", | |
| ) | |
| now = datetime.now(UTC) | |
| for advisory in advisories: | |
| if all or advisory["state"] in {"triage", "draft"}: | |
| updated = datetime.fromisoformat(advisory["updated_at"]) | |
| created = datetime.fromisoformat(advisory["created_at"]) | |
| age = _fmt_age(now - created) | |
| updated_age = _fmt_age(now - updated) | |
| table.add_row( | |
| _color_word(advisory["state"]), | |
| # severity can be None | |
| _color_word(advisory["severity"] or ""), | |
| advisory["org"], | |
| advisory["summary"], | |
| age, | |
| updated_age, | |
| advisory["html_url"], | |
| ) | |
| console = Console() | |
| console.print(table) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment