This is the runbook for security-relevant maintenance: dependency upgrades, fast-patch response, and how the supply-chain protections (spec #25) are wired together.
| Layer | Mechanism | Why |
|---|---|---|
| Python deps | uv.lock — every package pinned with sha256 |
A compromised PyPI publish can't ambush a fresh install: hash mismatch → install fails. |
| Python install | CI: uv sync --frozen. Docker: pip wheel --require-hashes |
Both honour the lockfile; nothing in CI or in the shipped image floats. |
| Docker base images | All FROM lines digest-pinned (@sha256:…) in Dockerfile |
Tags (:slim, :latest) are mutable. Without a digest, every build pulls "whatever Docker Hub points at right now". |
| Frontend deps | package-lock.json committed; npm ci in CI |
npm-ecosystem equivalent of the Python lockfile. |
| Upgrade cadence | Dependabot — weekly Mondays for pip / npm / docker | Bot opens grouped minor/patch PRs; humans review and merge. Avoids drift while keeping a paper trail. |
| Secret scanning | gitleaks pre-commit + CI | Catches accidentally-committed API keys, JWTs, etc. on push. |
Every Monday Dependabot opens grouped PRs for:
- pip minor + patch — backend Python deps.
- npm minor + patch — frontend deps.
- docker — base-image digest bumps.
Workflow:
- Inspect each PR's diff. Minor/patch should be small.
- Wait for CI green (Python tests + Frontend tests + Docker build + Secret scan).
- Merge. Dependabot rebases the others automatically.
- Major bumps come as separate PRs and need a closer read — they're allowed to fail; close and ignore if a major upgrade isn't worth the migration cost yet.
yt-dlp is the highest-risk dep in this codebase: it parses untrusted
server responses and runs FFmpeg subprocesses on user-supplied URLs.
Any RCE or path-traversal CVE in yt-dlp is patch within 48 h.
Same response shape applies to any other Critical CVE in the dep tree.
- GitHub Security Advisories on the repo (auto-enabled).
- Dependabot security alerts (separate from the routine weekly bumps).
- The
yt-dlpGitHub Releases page — major CVEs typically have a same-day patch release.
-
Confirm severity. Read the advisory. Is it RCE? Affecting our call site?
yt-dlpis invoked fromthestill/core/youtube_downloader.pywith user-supplied URLs — assume yes unless the advisory explicitly excludes the YouTube/audio-extraction path we use. -
Bump the lockfile:
uv lock --upgrade-package yt-dlp
Eyeball
uv.lockto confirm onlyyt-dlpand its direct dependencies moved. -
Run the suite:
./venv/bin/python -m pytest -q --ignore=tests/integration/pipeline
Then
npx tsc --noEmit && npx vitest runfromthestill/web/frontend/. -
Open a PR titled
fix(security): bump yt-dlp to <version> for <CVE-id>. Reference the advisory in the body. Skip the routine-review queue — merge as soon as CI is green. -
Rebuild the Docker image if running in production. The deploy process is owner-defined; the slim image build is a one-line
docker build -t thestill:slim --target slim .. -
Annotate the spec. If the CVE was particularly nasty, add a one-line entry under "Notable patched CVEs" below.
| Step | Target time |
|---|---|
| Detection → assessed | < 6 h (during business hours) |
| PR open | < 24 h |
| Merged + deploy | < 48 h |
These targets assume normal staffing. Genuine all-hands-on-deck CVEs (actively exploited, no workaround) get the full 48 h window if they land overnight; otherwise faster is better.
None to date. Add a one-line entry per fast-patch event with date + CVE id + commit ref so we can reconstruct the supply-chain history later.
- Add to
pyproject.toml(Python) orpackage.json(frontend). - Run
uv lockornpm install— the lockfile rebuilds with the new dep + its transitive deps + their hashes. - Commit both the manifest and the lockfile in the same PR. Never commit one without the other; CI will fail when the lockfile is out of sync with the manifest.
- If the dep is high-risk (parses untrusted input, runs subprocesses, does network IO), make sure it's on the radar for fast-patch policy — this is a judgement call, not a checklist.