Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
95f1e1e
use uv and githubkit
JacobCoffee Nov 5, 2025
f7bcbe3
add playwright basic
JacobCoffee Nov 5, 2025
7944e6b
remove stringent patch version
JacobCoffee Nov 5, 2025
4dc9eae
playwright initial base + otp
JacobCoffee Nov 6, 2025
92a4732
playwright comment grab
JacobCoffee Nov 6, 2025
a1e3a58
add post comments
JacobCoffee Nov 7, 2025
d301cfa
organize things into module, merge comments tests
JacobCoffee Nov 7, 2025
b9882f7
fix missing f
JacobCoffee Nov 19, 2025
c928c22
add helpers
JacobCoffee Nov 19, 2025
8061f44
add monitoring ci
JacobCoffee Nov 19, 2025
7fa9d3b
placeholder for playright things
JacobCoffee Nov 19, 2025
b9d1144
set username inside clien
JacobCoffee Nov 19, 2025
09b2a13
parse comments as commands
JacobCoffee Nov 19, 2025
a629854
idea file
JacobCoffee Nov 19, 2025
09210f5
run linting with rules actually enabled
JacobCoffee Nov 19, 2025
6a2f794
add some quick targets for local dev
JacobCoffee Nov 19, 2025
4534192
lint
JacobCoffee Nov 19, 2025
2789d28
add gha
JacobCoffee Nov 19, 2025
1cd9515
source state for exampl
JacobCoffee Nov 19, 2025
80ccdd6
lint
JacobCoffee Nov 19, 2025
f0c33b0
placeholder file
JacobCoffee Nov 19, 2025
7463115
dont process bot comments
JacobCoffee Nov 19, 2025
75119c9
dont print but log
JacobCoffee Nov 20, 2025
cc11440
swap to log instead of print, run lint/fmt
JacobCoffee Nov 20, 2025
a1a6575
update tests pasing, source tests for new files,
JacobCoffee Nov 20, 2025
05283d8
add some info to help people onboard
JacobCoffee Nov 20, 2025
03bac83
add words
JacobCoffee Nov 20, 2025
a6c498e
fix spacing
JacobCoffee Nov 20, 2025
2c5d40f
spelling...
JacobCoffee Nov 20, 2025
a772d26
we have cron-run that is more clear
JacobCoffee Nov 20, 2025
f820090
more docs for weirdness
JacobCoffee Nov 20, 2025
f568fae
EVEN MORE docs on clarifying weirdness and requirements
JacobCoffee Nov 20, 2025
8714967
update makefile section printouts
JacobCoffee Nov 25, 2025
f836ebd
address comments
JacobCoffee Nov 25, 2025
d20a309
address comments
JacobCoffee Nov 25, 2025
dcb77e2
address comments
JacobCoffee Nov 25, 2025
263c50e
simplify, remove unused call on enumerate
JacobCoffee Nov 25, 2025
182ddee
enable health check
JacobCoffee Nov 25, 2025
1a72dbd
remove unused noqa
JacobCoffee Nov 25, 2025
a6df710
configify slugs, change workflows
JacobCoffee Nov 25, 2025
2ea402a
make bootstrap org private
JacobCoffee Nov 25, 2025
4c57133
move commands into another branch
JacobCoffee Nov 25, 2025
b4238fb
wrap in run load in try/exc
JacobCoffee Nov 25, 2025
fa408e0
why be fancy? no names pls!
JacobCoffee Nov 25, 2025
7924a12
make fixture out of large pytest skipif
JacobCoffee Nov 25, 2025
cd82f98
refactor health check to use new status tracking
JacobCoffee Nov 25, 2025
f482e9b
piece out integration test
JacobCoffee Nov 25, 2025
881ca22
clean up unit tests
JacobCoffee Nov 25, 2025
a14dd6f
clean up unit tests
JacobCoffee Nov 25, 2025
4d68d43
move conf into one area,
JacobCoffee Nov 25, 2025
d135bd3
fmt
JacobCoffee Nov 25, 2025
14e3dc3
elevate to globals
JacobCoffee Nov 25, 2025
a5c7463
make tests pass
JacobCoffee Nov 25, 2025
16444ac
Add bot commands
JacobCoffee Nov 25, 2025
e3f9782
Add “Act” for local GitHub Actions debugging
JacobCoffee Nov 25, 2025
76ff3a0
fix bot author check
JacobCoffee Nov 25, 2025
b8c82b3
fix default for bot uname
JacobCoffee Nov 25, 2025
e510788
fix: raise error instead of returning fallback timestamp
JacobCoffee Nov 25, 2025
a3e50f0
fmt
JacobCoffee Nov 25, 2025
e4eca8c
fix lint, none checks on username for bot
JacobCoffee Nov 25, 2025
cd2a22d
move from parsed_data to json because githubkit but with pydnatic
JacobCoffee Nov 26, 2025
2b64e6c
move from parsed_data to json because githubkit but with pydnatic
JacobCoffee Nov 26, 2025
f76e513
update tests
JacobCoffee Nov 26, 2025
3cdb4d7
toggleable commenting
JacobCoffee Nov 26, 2025
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
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# GitHub App credentials (for API-capable operations)
GH_CLIENT_ID="123456"
GH_CLIENT_PRIVATE_KEY="base64...your...pem...keyfile"
GH_AUTH_TOKEN="ghp_123456"

# Playwright bot credentials (for browser automation)
GH_BOT_USERNAME=PSRT-GHSA-Automation
GH_BOT_PASSWORD=<secure-password>
GH_BOT_OTP_SECRET=<optional-2fa-secret> # Key used to generate OTP codes

# CVE API credentials
CVE_USERNAME="user@example.org"
CVE_API_KEY="123456"
CVE_ENV="testproddev"

# Sentry
SENTRY_DSN=

# Set to "true" to disable comment posting (staging/testing mode)
DONT_COMMENT=false
2 changes: 1 addition & 1 deletion .github/workflows/cron.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "PSRT GHSA Bot"
name: "PSRT GHSA Cron Bot"

on:
workflow_dispatch:
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/health-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: "Health Check"

on:
workflow_dispatch:
schedule:
- cron: "15 * * * *"

jobs:
monitor:
runs-on: ubuntu-latest
name: "Monitor Workflow Health"
steps:
- uses: actions/checkout@v5

- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"

- name: Install dependencies
run: uv sync --locked --no-editable --no-dev

- name: Check workflow status and report to Sentry
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: uv run python src/psrt_ghsa_bot/health_check.py
69 changes: 69 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: "PSRT GHSA Playwright Bot"

on:
workflow_dispatch:
schedule:
- cron: "*/5 * * * *"

jobs:
process-comments:
runs-on: ubuntu-latest
name: "Process GHSA Comments"
steps:
- uses: actions/checkout@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"

- name: Install dependencies
run: uv sync --locked --no-editable --no-dev

- name: Install Playwright browsers
run: uv run playwright install --with-deps chromium

- name: Restore state from cache
id: cache-state
uses: actions/cache/restore@v4
with:
path: state.json
key: playwright-state-${{ github.run_id }}
restore-keys: |
playwright-state-

- name: Process comments
run: uv run python -m psrt_ghsa_bot.comment_processor
env:
GH_CLIENT_ID: ${{ vars.GH_CLIENT_ID }}
GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
GH_CLIENT_PRIVATE_KEY: ${{ secrets.GH_CLIENT_PRIVATE_KEY }}
CVE_USERNAME: ${{ vars.CVE_USERNAME }}
CVE_API_KEY: ${{ secrets.CVE_API_KEY }}
CVE_ENV: ${{ vars.CVE_ENV }}
GH_BOT_USERNAME: ${{ vars.GH_BOT_USERNAME }}
GH_BOT_PASSWORD: ${{ secrets.GH_BOT_PASSWORD }}
GH_BOT_OTP_SECRET: ${{ secrets.GH_BOT_OTP_SECRET }}

- name: Save state to cache
if: always()
uses: actions/cache/save@v4
with:
path: state.json
key: playwright-state-${{ github.run_id }}

- name: Commit state file
if: always()
run: |
git config user.name "PSRT-GHSA-Automation[bot]"
git config user.email "bot@python.org"
git add state.json
git diff --quiet || git commit -m "Update comment processing state [skip ci]"
git push || true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just speculation, but might this introduce a race condition?

8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,11 @@ cython_debug/
playwright/.auth/
playwright-state/
**/playwright-state/

# Playwright videos and traces
playwright-videos/
playwright-traces/
*.webm
trace.zip
debug_*.png
tests/PLAYWRIGHT_FULL.test
50 changes: 47 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
.DEFAULT_GOAL:=help
.ONESHELL:
ACT_INSTALLED := $(shell command -v act 2> /dev/null)

.PHONY: help upgrade lint fmt fmt-check type-check ty check test ci
.PHONY: act-check act-list act-ci act-health-check act-playwright act-cron
.PHONY: cron playwright health-check

help: ## Display this help text for Makefile
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

upgrade: ## Upgrade all dependencies to the latest stable versions
@uv lock --upgrade
@echo "=> Dependencies Updated"
Expand All @@ -14,16 +21,53 @@ lint: ## Lint the code
fmt: ## Format the code
@uv run ruff format .

mt-check: ## Runs Ruff format in check mode (no changes)
fmt-check: ## Runs Ruff format in check mode (no changes)
@uv run --no-sync ruff format --check .

type-check: ## Run type-checking
@uv run ty check

ty: type-check ## Alias for type-check

check: lint fmt type-check ## Run all checks except tests
Comment thread
sethmlarson marked this conversation as resolved.

test: ## Run tests
@test -f tests/PLAYWRIGHT_FULL.test && uv run playwright install --with-deps chromium 2>/dev/null || true
@uv run pytest

ci: lint fmt type-check test ## Run everything

app: ## Run the app
@uv run python app.py
##@ GitHub Actions (Local Testing)

act-check: ## Check if act is installed
ifndef ACT_INSTALLED
@echo "act is not installed. Install it from: https://nektosact.com/installation/index.html"
@exit 1
endif

act-list: act-check ## List all available GitHub Actions workflows
@act -l

act-ci: act-check ## Test CI workflow locally using act
@act -W .github/workflows/ci.yml

act-health-check: act-check ## Test health-check workflow locally using act
@act -W .github/workflows/health-check.yml

act-playwright: act-check ## Test playwright workflow locally using act
@act -W .github/workflows/playwright.yml

act-cron: act-check ## Test cron workflow locally using act
@act -W .github/workflows/cron.yml

##@ Live Bot Commands
### These all require .env file with the vars set based on .env.example!

cron: ## Run the cron bot (app.py)
@uv run python -m psrt_ghsa_bot.app

playwright: ## Run playwright bot
@uv run python -m psrt_ghsa_bot.comment_processor

health-check: ## Run health check
@uv run python -m psrt_ghsa_bot.health_check
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,83 @@
# PSRT GHSA Bot

Bot which adds the PSRT GitHub team (`python/psrt`) and CVE IDs to GitHub Security Advisories.

## Architecture

### GitHub Actions

- The cron bot runs off of [`.github/workflows/cron.yml`](.github/workflows/cron.yml) and runs at the top of each hour.
- Calls [src/psrt_ghsa_bot/app.py](src/psrt_ghsa_bot/app.py)
- Fetches open GHSAs from all installed orgs/repos
- Adds PSRT team to GHSAs without it
- Assigns CVE IDs to draft GHSAs without one
- The Playwright bot runs off of [`.github/workflows/playwright.yml`](.github/workflows/playwright.yml) and runs every 5 minutes.
- Calls [src/psrt_ghsa_bot/comment_processor.py](src/psrt_ghsa_bot/comment_processor.py)
- Reads GHSA comments via Playwright (no API available)
- Parses `@<bot-username>` commands, executes if authorized
- Posts responses, tracks state in `state.json`
- Health checks are done via [`.github/workflows/health-check.yml`](.github/workflows/health-check.yml) and runs every 15 minutes.
- It checks the status using the `gh` CLI and reports to Sentry if the bot is not healthy via Sentry cron monitors.

### Why Playwright?

The GHSA API is limited and has not really been developed in awhile. As such, it is missing a lot of features
like:
- Commenting on GHSAs
- Adding teams to GHSAs
- Assigning CVE IDs to GHSAs
- Removing temporary forks generated inside a GHSA that collaborators use fro remediation
- No webhooks to respond to things.. so we do the GHA polling thing...

That's why there is this weird split between the cron.yml and playwright.yml. As API things
are added, we can move more into app.py/cron.yml and remove the playwright stuff (gladly!)

## Installation

The GitHub app (API-related activities) **MUST** be installed in all GitHub organizations
you want scanned. The GitHub user (`GH_BOT_USERNAME`, used by Playwright) **MUST** have access to the repos that
you want to interact with.

**Important:** The GitHub App installation and GitHub user require the same permissions on repositories.
The Playwright bot uses the GitHub App installation to generate a list of repos
(~[comment_processor.py:130-140](src/psrt_ghsa_bot/comment_processor.py#L130-L140)), so any permission
mismatch will cause failures


## Development

Uses `uv` for dependency management and `pytest` for testing.

### Setup

Make sure you have `uv` installed at https://docs.astral.sh/uv/getting-started/installation/
Quickly, for Linux/macOS:
```shell
curl -LsSf https://astral.sh/uv/install.sh | sh
```
or (not recommended):
```shell
pipx install uv
```

Afterwards, you can use `make` to run the commands in the [`Makefile`](Makefile).
- `make upgrade` - Upgrade all dependencies to the latest stable versions.

Every time you run `uv run` or anything it automatically installs/syncs the dependencies
and it's near-instant so there is no `make install` or anything.

### Tests

Only unique things here are `PLAYWRIGHT_FULL.test`, which can can see more about in [`PLAYWRIGHT_FULL.test.example`](tests/PLAYWRIGHT_FULL.test.example).
This just tells the [`Makefile`](Makefile) target `make test` to run `playwright install` before running the tests
and then enables some of the skipped tests. These tests assume a test organization and all the setup behind that
because it is an integration test and will comment on and read a GHSA advisory.

### Scripts

There is a [`scripts/`](scripts/) directory with some local dev scripts, namely one that will
set up an organization with all the things needed to develop (TODO: it doesn't actually do anything yet.)

The idea behind the bootstrap_org.py is that it will:
- Take your test org, set up a `psrt` (or whatever) team, create a repo with some GHSA, then comment, read the comments, etc.
This helps more from the integration testing side of things without doing it all in some public, busy repo like `python/CPython` :)
62 changes: 61 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ requires-python = ">=3.14.0"
dependencies = [
"cvelib>=1.4.0",
"githubkit[auth-app]>=0.13.5",
# we might could put this into a dep group to not load it every time we install

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# we might could put this into a dep group to not load it every time we install
# We could put this into a dep group to not load it every time we install

# inside gh actions for cron.yml or the sentry check in
"playwright>=1.55.0",
"pyotp>=2.9.0",
"python-dotenv>=1.0.0",
"sentry-sdk>=2.22.0",
]

[dependency-groups]
Expand All @@ -32,11 +36,67 @@ line-length = 120
indent-width = 4

[tool.ruff.lint]
ignore = ["D203", "D213", "COM812"]
select = ["ALL"]
ignore = [
"D203",
"D213",
"COM812",
"T201",
"TD", # todo without author
"FIX002", # Line contains TODO
"PLR0913", # Too many arguments
"PLR0911", # Too many returns
"C901", # Too complex
"ARG001", # unused arg
"BLE001", # blind except
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.ruff.lint.per-file-ignores]
"tests/**/*.*" = [
"A",
"ARG",
"B",
"BLE",
"C901",
"D",
"DTZ",
"EM",
"FBT",
"G",
"N",
"PGH",
"PIE",
"PLR",
"PLW",
"PTH",
"RSE",
"S",
"S101",
"SIM",
"TC",
"TRY",
"SLF",
"ANN",
"FIX",
"TD",
"ERA",
]
"scripts/**/*.*" = [
"INP001", # Implicit namespace package
"EXE001", # Shebang not executable
"RUF001", # Ambiguous unicode
"DTZ", # Timezone issues
"S", # Security issues
"T201", # Print statements
]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.pytest.ini_options]
addopts = "-p no:anyio"
testpaths = ["tests/unit"]
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Scripts for PSRT GHSA bot."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Scripts for PSRT GHSA bot."""
"""Scripts for the PSRT GHSA bot."""

Loading
Loading