Skip to content

Commit 9f90cfb

Browse files
committed
Add CVE number and private fork pull request link
1 parent a14b91d commit 9f90cfb

1 file changed

Lines changed: 89 additions & 35 deletions

File tree

security-overview

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
2+
import argparse
23
import os
34
import sys
4-
import argparse
55
from datetime import datetime, timezone
66

77
import httpx
@@ -11,21 +11,36 @@ ALL_STATES = ["published", "draft", "triage", "closed"]
1111

1212
RESET = "\033[0m"
1313
BOLD = "\033[1m"
14+
GREY = "\033[2m"
1415

1516
STATE_COLORS = {
16-
"draft": "\033[36m", # cyan
17-
"triage": "\033[33m", # yellow
17+
"draft": "\033[36m", # cyan
18+
"triage": "\033[33m", # yellow
1819
"published": "\033[32m", # green
19-
"closed": "\033[2m", # grey
20+
"closed": GREY,
2021
}
2122

23+
PR_STATE_COLORS = {
24+
"open": "\033[32m", # green
25+
"merged": "\033[35m", # purple
26+
"closed": "\033[31m", # red
27+
}
28+
29+
30+
def pr_state(pull):
31+
if pull is None:
32+
return None
33+
if pull.get("merged_at"):
34+
return "merged"
35+
return pull.get("state", "?") # "open" or "closed"
2236

23-
def plasma(t):
37+
38+
def plasma_rgb(t):
2439
"""Plasma color: t=0 → purple (old), t=1 → yellow (recent)."""
2540
r = int(13 + (253 - 13) * t)
2641
g = int(8 + (231 - 8) * t)
2742
b = int(135 + (37 - 135) * t)
28-
return f"\033[38;2;{r};{g};{b}m"
43+
return r, g, b
2944

3045

3146
def age_t(date_str):
@@ -34,9 +49,14 @@ def age_t(date_str):
3449
return max(0.0, 1.0 - age / 365)
3550

3651

37-
def block(date_str):
38-
color = plasma(age_t(date_str)) if date_str else ""
39-
return f"{color}{RESET}"
52+
def age_badge(date_str, text):
53+
"""Render text on a plasma background; foreground chosen for legibility."""
54+
t = age_t(date_str) if date_str else 0.0
55+
r, g, b = plasma_rgb(t)
56+
luminance = 0.299 * r + 0.587 * g + 0.114 * b
57+
fg = "\033[30m" if luminance > 128 else "\033[97m"
58+
bg = f"\033[48;2;{r};{g};{b}m"
59+
return f"{bg}{fg}{text}{RESET}"
4060

4161

4262
def redact_str(s):
@@ -55,6 +75,23 @@ def check_token():
5575
return token
5676

5777

78+
async def fetch_first_pull(client, fork_html_url, results):
79+
"""Fetch the first pull request (by creation date) for a private fork repo."""
80+
path = fork_html_url.split("github.com/", 1)[-1].rstrip("/")
81+
url = f"{GITHUB_API}/repos/{path}/pulls"
82+
params = {"per_page": 1, "state": "all", "sort": "created", "direction": "asc"}
83+
try:
84+
req = client.build_request("GET", url, params=params)
85+
resp = await client.send(req)
86+
if resp.status_code == 200:
87+
pulls = resp.json()
88+
results[fork_html_url] = pulls[0] if pulls else None
89+
else:
90+
results[fork_html_url] = None
91+
except httpx.HTTPError:
92+
results[fork_html_url] = None
93+
94+
5895
async def fetch(client, org, state, results):
5996
"""Fetch all advisories for (org, state), following pagination, storing in results[(org, state)]."""
6097
url = f"{GITHUB_API}/orgs/{org}/security-advisories"
@@ -85,12 +122,19 @@ async def fetch(client, org, state, results):
85122
results[(org, state)] = advisories
86123

87124

88-
def print_org(org, states, results_by_key, redact=False):
125+
def link(text, target):
126+
return f"\033[4m\033]8;;{target}\033\\{text}\033]8;;\033\\\033[24m"
127+
128+
129+
def color(text, color):
130+
return f"{color}{text}{RESET}"
131+
132+
133+
def print_org(org, states, results_by_key, pull_results=None, redact=False):
89134
advisories = []
90135
for state in states:
91136
advisories.extend(results_by_key.get((org, state), []))
92-
if not redact:
93-
print(f"\n{BOLD}=== {org} ==={RESET}")
137+
print(f"\n{BOLD}=== {org if not redact else 'REDACTED ORG'} ==={RESET}")
94138
if not advisories:
95139
if not redact:
96140
print(" (no advisories)")
@@ -109,29 +153,27 @@ def print_org(org, states, results_by_key, redact=False):
109153

110154
for repo, items in advisories_by_repo:
111155
items = sorted(items, key=lambda a: a.get("updated_at", ""), reverse=True)
112-
if not redact:
113-
print(f"{repo}")
156+
print(f"{BOLD}{repo if not redact else 'REDACTED REPO'}{RESET}")
114157
for advisory in items:
158+
id = advisory.get("ghsa_id", "") if not redact else "GHSA-xxxx-yyyy-zzzz"
159+
url = advisory.get("html_url", "")
115160
date = advisory.get("updated_at", "")
116-
title = (advisory.get("summary") or "")[:50].ljust(50)
161+
title = (advisory.get("summary") or "")[:40].ljust(40)
117162
state_val = advisory.get("state") or "?"
118-
state_col = STATE_COLORS.get(state_val, "")
119-
state_str = f"{state_col}{state_val.ljust(10)}{RESET}"
120-
url = advisory.get("html_url", "")
121-
url_base, _, url_id = url.rpartition("/")
122-
if redact:
123-
# replace org/repo slugs in the URL with literal ORG/REPO
124-
parts = url_base.split("/")
125-
if len(parts) >= 5:
126-
parts[3] = "ORG"
127-
parts[4] = "REPO"
128-
url_base = "/".join(parts)
129-
# GHSA-xxxx-yyyy-zzzz → keep "GHSA-" prefix, hide the random bits
130-
if url_id.startswith("GHSA-"):
131-
url_id = "GHSA-" + redact_str(url_id[5:])
132-
else:
133-
url_id = redact_str(url_id)
134-
url_fmt = f"{state_col}\033[2m{url_base}/\033[22m{url_id}{RESET}"
163+
state_str = color(state_val.ljust(9), STATE_COLORS.get(state_val, ""))
164+
cve = advisory.get("cve_id") or ""
165+
fork = advisory.get("private_fork")
166+
fork_html_url = fork.get("html_url") if fork else None
167+
cve_str = cve[:15].ljust(15)
168+
cve_str = color(cve_str, GREY)
169+
first_pull = (
170+
(pull_results or {}).get(fork_html_url) if fork_html_url else None
171+
)
172+
ps = pr_state(first_pull)
173+
pull_str = (
174+
color(link("PR " + ps.ljust(6), first_pull["html_url"]), PR_STATE_COLORS.get(ps, GREY))
175+
if first_pull else " " * 9
176+
)
135177
days_ago = (
136178
(
137179
datetime.now(timezone.utc)
@@ -140,13 +182,14 @@ def print_org(org, states, results_by_key, redact=False):
140182
if date
141183
else "?"
142184
)
185+
badge = age_badge(date, (str(days_ago) + 'd').rjust(5))
143186
if redact:
144187
print(
145-
f" {block(date)} {str(days_ago).rjust(4)}d {state_str} {url_fmt}"
188+
f" {badge}\t{link(id, url)} {state_str}"
146189
)
147190
else:
148191
print(
149-
f" {block(date)} {str(days_ago).rjust(4)}d {title} {state_str} {url_fmt}"
192+
f" {badge}\t{state_str} {color(link(id, url), GREY)} {title} {cve_str} {pull_str}"
150193
)
151194

152195

@@ -188,14 +231,25 @@ async def main():
188231
}
189232

190233
results = {}
234+
pull_results = {}
191235
async with httpx.AsyncClient(headers=headers, timeout=30.0) as client:
192236
async with trio.open_nursery() as nursery:
193237
for org in args.orgs:
194238
for state in states:
195239
nursery.start_soon(fetch, client, org, state, results)
196240

241+
fork_urls = {
242+
advisory["private_fork"]["html_url"]
243+
for advisories in results.values()
244+
for advisory in advisories
245+
if advisory.get("private_fork") and advisory["private_fork"].get("html_url")
246+
}
247+
async with trio.open_nursery() as nursery:
248+
for fork_url in fork_urls:
249+
nursery.start_soon(fetch_first_pull, client, fork_url, pull_results)
250+
197251
for org in args.orgs:
198-
print_org(org, states, results, redact=args.redact)
252+
print_org(org, states, results, pull_results=pull_results, redact=args.redact)
199253
print()
200254

201255

0 commit comments

Comments
 (0)