Skip to content

Commit d0446da

Browse files
authored
Merge pull request #79 from Carreau/priv-sec
private reporting vuln
2 parents 01e98c8 + 9bef48b commit d0446da

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

psc_ignore.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# list of non private/archive {org}/{repo} it is ok to not have private security reporting.
2+
jupyter/governance

tools/private-sec-reporting.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import os
2+
import asyncio
3+
import aiohttp
4+
from rich import print
5+
from datetime import datetime
6+
from pathlib import Path
7+
import humanize
8+
from itertools import count
9+
10+
orgs = [
11+
"binder-examples",
12+
"binderhub-ci-repos",
13+
"ipython",
14+
"jupyter",
15+
"jupyter-book",
16+
"jupyter-governance",
17+
"jupyter-incubator",
18+
"jupyter-server",
19+
"jupyter-standards",
20+
"jupyter-widgets",
21+
"jupyterhub",
22+
"jupyterlab",
23+
"jupyter-xeus",
24+
"jupytercon",
25+
"voila-dashboards",
26+
"voila-gallery",
27+
]
28+
token = os.getenv("GH_TOKEN")
29+
if not token:
30+
print("[red]Error: GH_TOKEN environment variable not set[/red]")
31+
exit(1)
32+
33+
headers = {
34+
"Authorization": f"token {token}",
35+
"Accept": "application/vnd.github.v3+json",
36+
}
37+
38+
39+
async def check_private_vulnerability_reporting(
40+
session: aiohttp.ClientSession, org: str, repo_name: str
41+
) -> bool:
42+
"""Check if private vulnerability reporting is enabled for a repository
43+
44+
Parameters
45+
----------
46+
session: aiohttp.ClientSession
47+
The aiohttp client session
48+
org: str
49+
The organization name
50+
repo_name: str
51+
The repository name
52+
53+
Returns
54+
-------
55+
bool: True if enabled, False otherwise
56+
"""
57+
url = f"https://api.github.com/repos/{org}/{repo_name}/private-vulnerability-reporting"
58+
59+
async with session.get(url, headers=headers) as response:
60+
if response.status == 200:
61+
data = await response.json()
62+
return data.get("enabled", False)
63+
return False
64+
65+
66+
async def get_org_repos(session: aiohttp.ClientSession, org: str) -> list[dict]:
67+
"""Get all repositories for an organization
68+
69+
Parameters
70+
----------
71+
session: aiohttp.ClientSession
72+
The aiohttp client session
73+
org: str
74+
The organization name
75+
76+
Returns
77+
-------
78+
list[dict]: The list of repositories
79+
"""
80+
repos = []
81+
82+
for page in count(1): # starts at 1 and counts up infinitely
83+
url = f"https://api.github.com/orgs/{org}/repos?page={page}&per_page=100"
84+
async with session.get(url, headers=headers) as response:
85+
if response.status != 200:
86+
print(f"[red]Error fetching repos: {response.status}[/red]")
87+
break
88+
89+
page_repos = await response.json()
90+
if not page_repos: # empty page means we've reached the end
91+
break
92+
93+
repos.extend(page_repos)
94+
95+
return repos
96+
97+
98+
async def main():
99+
ignores = Path("psc_ignore.txt").read_text().splitlines()
100+
async with aiohttp.ClientSession() as session:
101+
# Check rate limit before making requests
102+
async with session.get(
103+
"https://api.github.com/rate_limit", headers=headers
104+
) as response:
105+
if response.status == 200:
106+
rate_data = await response.json()
107+
remaining = rate_data["resources"]["core"]["remaining"]
108+
reset_time = datetime.fromtimestamp(
109+
rate_data["resources"]["core"]["reset"]
110+
)
111+
reset_in = humanize.naturaltime(reset_time)
112+
if remaining < 100:
113+
print(
114+
f"[yellow]Warning: Rate limit is low! ({remaining} remaining, full in {reset_in})[/yellow]"
115+
)
116+
if remaining < 10:
117+
print("[red]Aborting due to very low rate limit[/red]")
118+
return
119+
else:
120+
print(f"Rate limit remaining: {remaining}")
121+
print(f"Rate limit resets {reset_in}")
122+
else:
123+
print(f"[red]Error checking rate limit: {response.status}[/red]")
124+
tasks = []
125+
org_tasks = [(org, get_org_repos(session, org)) for org in orgs]
126+
org_results = await asyncio.gather(*(task for _, task in org_tasks))
127+
repos = []
128+
for (org, _), org_repos in zip(org_tasks, org_results):
129+
for repo in org_repos:
130+
repos.append((org, repo))
131+
132+
for org, repo in sorted(repos, key=lambda x: x[1]["name"]):
133+
if f"{org}/{repo['name']}" in ignores:
134+
print(
135+
f"[yellow]Ignoring {org}/{repo['name']} from ignore file[/yellow]"
136+
)
137+
continue
138+
repo_name = repo["name"]
139+
140+
task = check_private_vulnerability_reporting(session, org, repo_name)
141+
tasks.append((repo, org, repo_name, task))
142+
143+
results = await asyncio.gather(*[task for _, _, _, task in tasks])
144+
145+
for (repo, org, repo_name, _), has_vuln_reporting in sorted(
146+
zip(tasks, results), key=lambda x: x[0][0]["pushed_at"], reverse=True
147+
):
148+
last_activity = repo["pushed_at"]
149+
last_activity_date = datetime.fromisoformat(last_activity).strftime(
150+
"%Y-%m-%d"
151+
)
152+
last_activity_ago_human = humanize.naturaltime(
153+
datetime.now(datetime.fromisoformat(last_activity).tzinfo)
154+
- datetime.fromisoformat(last_activity)
155+
)
156+
157+
if repo["archived"]:
158+
print(
159+
f"{org+'/'+repo_name:<55}: [yellow]Archived {'Enabled' if has_vuln_reporting else 'Disabled[/yellow]'}"
160+
)
161+
elif repo["private"]:
162+
print(
163+
f"{org+'/'+repo_name:<55}: [yellow]Private {'Enabled' if has_vuln_reporting else 'Disabled[/yellow]'} –– last activity: {last_activity_date} ({last_activity_ago_human})"
164+
)
165+
166+
else:
167+
print(
168+
f"{org+'/'+repo_name:<55}: {'[green]Enabled[/green]' if has_vuln_reporting else '[red]Disabled[/red]'} –– last activity: {last_activity_date} ({last_activity_ago_human})"
169+
)
170+
171+
172+
if __name__ == "__main__":
173+
asyncio.run(main())

0 commit comments

Comments
 (0)