Skip to content
Closed
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
95 changes: 95 additions & 0 deletions .github/workflows/dependency-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Dependency security

on:
pull_request:
paths:
- "**/pyproject.toml"
- "**/uv.lock"
- "**/package.json"
- "**/pnpm-lock.yaml"
- "renovate.json"
- "scripts/audit_requirements.py"
- "scripts/check_notebook_audit.py"
- ".github/workflows/dependency-security.yml"
push:
branches: [main]
schedule:
- cron: "0 8 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
python:
name: Python dependency audit (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12", "3.14"]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: ${{ matrix.python-version }}

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.24"

- name: Export locked dependencies
run: |
uv export --locked --no-emit-workspace --no-hashes --output-file "$RUNNER_TEMP/dependencies.txt" > /dev/null
uv export --project examples/local_video --locked --no-emit-local --no-hashes --output-file "$RUNNER_TEMP/example.txt" > /dev/null

- name: Audit workspace and example
run: |
uvx --from pip-audit==2.10.1 pip-audit --disable-pip --no-deps -r "$RUNNER_TEMP/dependencies.txt" --format json --output "$RUNNER_TEMP/python-audit.json"
uvx --from pip-audit==2.10.1 pip-audit --disable-pip --no-deps -r "$RUNNER_TEMP/example.txt" --format json --output "$RUNNER_TEMP/example-audit.json"
Comment on lines +49 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Example audit skipped after workspace findings

When the workspace scan finds an advisory, pip-audit exits nonzero and the step stops before scanning the example. The example receives no audit report for that run.

Learn more

GitHub runs the shell block with fail-fast behavior. pip-audit returns a nonzero status when it finds vulnerabilities, even if it writes the JSON report. Because the example audit follows the workspace audit in the same step, a workspace finding prevents the example audit and its artifact from being produced. The upload step still runs, but it can only upload reports that exist.

Example: A workspace dependency has one advisory and the local-video example has another. The workspace scan writes python-audit.json and exits 1; the example scan never writes example-audit.json, leaving its advisory unreported.

Recommended fix: Run both pip-audit commands independently, capture each exit status, then fail the job if either status is nonzero. Keep artifact upload under if: always() so both JSON reports remain available.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


- name: Report Python 3.9 findings
if: matrix.python-version == '3.12'
continue-on-error: true
run: |
uv run --no-project --with packaging==26.0 python scripts/audit_requirements.py "$RUNNER_TEMP/dependencies.txt" "$RUNNER_TEMP/python39.txt" --python-version 3.9
echo '::warning::Python 3.9 retains aiohttp/multidict advisories whose fixes require Python 3.10+. See the audit artifact.'
uvx --from pip-audit==2.10.1 pip-audit --disable-pip --no-deps -r "$RUNNER_TEMP/python39.txt" --format json --output "$RUNNER_TEMP/python39-audit.json"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: python-audit-${{ matrix.python-version }}
path: ${{ runner.temp }}/*audit.json

notebook:
name: Notebook dependency audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"

- name: Audit notebook dependencies
run: |
npm install --global pnpm@10.28.2
# The checker handles the exact known unpatched finding and blocks all others.
audit_status=0
pnpm --dir livekit-rtc/jupyter-html audit --json > "$RUNNER_TEMP/notebook-audit.json" || audit_status=$?
if [ "$audit_status" -gt 1 ]; then
echo "::error::Notebook audit failed with exit status $audit_status"
exit "$audit_status"
fi
if [ "$audit_status" -eq 0 ]; then
echo "Notebook audit has no findings"
fi
python scripts/check_notebook_audit.py "$RUNNER_TEMP/notebook-audit.json"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: notebook-audit
path: ${{ runner.temp }}/notebook-audit.json
11 changes: 10 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "helpers:pinGitHubActionDigests"],
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
"minimumReleaseAge": "2 weeks",
"commitBody": "Generated by renovateBot",
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 9am on monday"]
},
"git-submodules": {
"enabled": true
},
"packageRules": [
{
"matchManagers": ["github-actions"],
"matchPackageNames": ["python"],
"enabled": false
},
{
"matchManagers": ["github-actions"],
"groupName": "github workflows"
Expand Down
40 changes: 40 additions & 0 deletions scripts/audit_requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Select exported lockfile requirements for a Python version on this platform."""

import argparse
from pathlib import Path

from packaging.markers import default_environment
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path)
parser.add_argument("destination", type=Path)
parser.add_argument("--python-version", required=True)
args = parser.parse_args()

environment = default_environment()
environment["python_version"] = args.python_version
environment["python_full_version"] = args.python_version + ".99"
requirements = {}
for line in args.source.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
requirement = Requirement(line)
if requirement.marker and not requirement.marker.evaluate(environment):
continue
specifiers = list(requirement.specifier)
if requirement.url or len(specifiers) != 1 or specifiers[0].operator != "==":
parser.error(f"Expected an exact registry version: {line}")
name = canonicalize_name(requirement.name)
version = specifiers[0].version
if name in requirements and requirements[name] != version:
parser.error(f"Conflicting versions for {name} on Python {args.python_version}")
requirements[name] = version

if not requirements:
parser.error("No requirements selected")
args.destination.write_text(
"".join(f"{name}=={version}\n" for name, version in sorted(requirements.items()))
)
27 changes: 27 additions & 0 deletions scripts/check_notebook_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Fail notebook audits except for the explicitly tracked unpatched advisory."""

import argparse
import json
from pathlib import Path

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("report", type=Path)
args = parser.parse_args()
report = json.loads(args.report.read_text())
if "advisories" not in report or "metadata" not in report or "error" in report:
parser.error("Expected a successful pnpm audit JSON response")

blocking = []
for advisory in report["advisories"].values():
advisory_id = advisory.get("github_advisory_id")
if advisory_id == "GHSA-vfj7-8cjw-p6xm" and advisory.get("patched_versions") == "<0.0.0":
print(
"::warning::braces GHSA-vfj7-8cjw-p6xm remains unpatched in notebook "
"build tooling. The full finding is retained in the audit artifact."
)
else:
blocking.append(advisory_id or advisory["id"])

if blocking:
print(f"Blocking notebook advisories: {blocking}")
raise SystemExit(1)
Loading