Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ GH_AUTH_TOKEN="ghp_123456"
CVE_USERNAME="user@example.org"
CVE_API_KEY="123456"
CVE_ENV="testproddev"
CVE_ENABLED_REPOS="python/cpython"
SENTRY_DSN="{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID}"
1 change: 1 addition & 0 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ jobs:
CVE_USERNAME: ${{ vars.CVE_USERNAME }}
CVE_API_KEY: ${{ secrets.CVE_API_KEY }}
CVE_ENV: ${{ vars.CVE_ENV }}
CVE_ENABLED_REPOS: ${{ vars.CVE_ENABLED_REPOS }}
Comment thread
sethmlarson marked this conversation as resolved.
SENTRY_DSN: ${{ github.event_name == 'schedule' && secrets.SENTRY_DSN || '' }}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ PSRT GHSA Bot is a GitHub App that automates the [Python Security Response Team
handling of GitHub Security Advisories. It runs hourly (or by manual dispatch)
and, for every advisory it closes ones marked as completed, promotes accepted ones
from triage to draft, reserves CVE IDs, creates private forks, and adds the
PSRT members as collaborators.
PSRT team as collaborators.

It processes every repository the GitHub App is installed on.
The process is identical across repositories except that CVE IDs are only
reserved for repositories listed in the `CVE_ENABLED_REPOS` environment variable.

```mermaid
flowchart TD
Expand Down
19 changes: 15 additions & 4 deletions src/psrt_ghsa_bot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def reserve_one_cve(cve_api: CveApi) -> str:
return cve_ids[0]


def apply_to_repo(github: GitHub, owner: str, repo: str, cve_api: CveApi) -> None:
def apply_to_repo(github: GitHub, owner: str, repo: str, cve_api: CveApi, *, reserve_cves: bool = True) -> None:
"""Applies the PSRT GitHub Security Advisory process to the repository."""
security_advisories = get_repository_advisories(github, owner, repo)
advisory_count = 0
Expand Down Expand Up @@ -173,7 +173,7 @@ def apply_to_repo(github: GitHub, owner: str, repo: str, cve_api: CveApi) -> Non

# Advisories that are in the 'draft' state without a CVE ID
# should have one allocated by the PSF CVE Numbering Authority.
if state == "draft" and security_advisory.get("cve_id") is None:
if reserve_cves and state == "draft" and security_advisory.get("cve_id") is None:
cve_id = reserve_one_cve(cve_api)
patch_data["cve_id"] = cve_id
print(f" ✅ Will reserve CVE ID: {cve_id}")
Expand Down Expand Up @@ -219,6 +219,10 @@ def run() -> None:
env=os.environ.get("CVE_ENV", "prod"),
)

cve_enabled_repos = frozenset(
name.strip() for name in (os.environ.get("CVE_ENABLED_REPOS") or "python/cpython").split(",") if name.strip()
)

print("Fetching installations...")
# Apply to all repositories for each installation.
installations = github.rest.paginate(
Expand All @@ -238,8 +242,15 @@ def run() -> None:
map_func=lambda r: r.parsed_data.repositories,
)
for repo in repos:
print(f" Checking repo: {repo.owner.login}/{repo.name}")
apply_to_repo(installation_github, repo.owner.login, repo.name, cve_api)
slug = f"{repo.owner.login}/{repo.name}"
print(f"Processing repo: {slug}")
apply_to_repo(
installation_github,
repo.owner.login,
repo.name,
cve_api,
reserve_cves=slug in cve_enabled_repos,
)

print(f"\nDone! Processed {installation_count} installation(s).")

Expand Down
15 changes: 15 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ def test_does_not_reserve_cve_id_for_triage_security_advisories(state) -> None:
github.rest.security_advisories.update_repository_advisory.assert_not_called()


def test_does_not_reserve_cve_id_when_reserve_cves_disabled() -> None:
security_advisory = _create_advisory_dict("draft", None, ["psrt"])

github = mock.Mock()
cve_api = mock.Mock()

with mock.patch("psrt_ghsa_bot.app.get_repository_advisories") as get_repo_advs:
get_repo_advs.return_value = [security_advisory]

app.apply_to_repo(github, "owner", "repo", cve_api, reserve_cves=False)

cve_api.reserve.assert_not_called()
github.rest.security_advisories.update_repository_advisory.assert_not_called()


def test_create_private_fork() -> None:
github = mock.Mock()
cve_api = mock.Mock()
Expand Down