Skip to content

Commit 49be479

Browse files
ci: maintain and audit dependency security weekly
1 parent b6ea360 commit 49be479

4 files changed

Lines changed: 166 additions & 1 deletion

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Dependency security
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "**/pyproject.toml"
7+
- "**/uv.lock"
8+
- "**/package.json"
9+
- "**/pnpm-lock.yaml"
10+
- "renovate.json"
11+
- "scripts/audit_requirements.py"
12+
- "scripts/check_notebook_audit.py"
13+
- ".github/workflows/dependency-security.yml"
14+
push:
15+
branches: [main]
16+
schedule:
17+
- cron: "0 8 * * 1"
18+
workflow_dispatch:
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
python:
25+
name: Python dependency audit (${{ matrix.python-version }})
26+
runs-on: ubuntu-latest
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
python-version: ["3.10", "3.12", "3.14"]
31+
steps:
32+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
33+
34+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
38+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
39+
with:
40+
version: "0.12.24"
41+
42+
- name: Export locked dependencies
43+
run: |
44+
uv export --locked --no-emit-workspace --no-hashes --output-file "$RUNNER_TEMP/dependencies.txt" > /dev/null
45+
uv export --project examples/local_video --locked --no-emit-local --no-hashes --output-file "$RUNNER_TEMP/example.txt" > /dev/null
46+
47+
- name: Audit workspace and example
48+
run: |
49+
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"
50+
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"
51+
52+
- name: Report Python 3.9 findings
53+
if: matrix.python-version == '3.12'
54+
continue-on-error: true
55+
run: |
56+
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
57+
echo '::warning::Python 3.9 retains aiohttp/multidict advisories whose fixes require Python 3.10+. See the audit artifact.'
58+
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"
59+
60+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
61+
if: always()
62+
with:
63+
name: python-audit-${{ matrix.python-version }}
64+
path: ${{ runner.temp }}/*audit.json
65+
66+
notebook:
67+
name: Notebook dependency audit
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
71+
72+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
73+
with:
74+
node-version: "22"
75+
76+
- name: Audit notebook dependencies
77+
run: |
78+
npm install --global pnpm@10.28.2
79+
# The checker handles the exact known unpatched finding and blocks all others.
80+
if pnpm --dir livekit-rtc/jupyter-html audit --json > "$RUNNER_TEMP/notebook-audit.json"; then
81+
echo "Notebook audit has no findings"
82+
fi
83+
python scripts/check_notebook_audit.py "$RUNNER_TEMP/notebook-audit.json"
84+
85+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
86+
if: always()
87+
with:
88+
name: notebook-audit
89+
path: ${{ runner.temp }}/notebook-audit.json

‎renovate.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
{
22
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3-
"extends": ["config:base", "helpers:pinGitHubActionDigests"],
3+
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
44
"minimumReleaseAge": "2 weeks",
55
"commitBody": "Generated by renovateBot",
6+
"lockFileMaintenance": {
7+
"enabled": true,
8+
"schedule": ["before 9am on monday"]
9+
},
610
"git-submodules": {
711
"enabled": true
812
},
913
"packageRules": [
14+
{
15+
"matchManagers": ["github-actions"],
16+
"matchPackageNames": ["python"],
17+
"enabled": false
18+
},
1019
{
1120
"matchManagers": ["github-actions"],
1221
"groupName": "github workflows"

‎scripts/audit_requirements.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Select exported lockfile requirements for a Python version on this platform."""
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
from packaging.markers import default_environment
7+
from packaging.requirements import Requirement
8+
from packaging.utils import canonicalize_name
9+
10+
parser = argparse.ArgumentParser(description=__doc__)
11+
parser.add_argument("source", type=Path)
12+
parser.add_argument("destination", type=Path)
13+
parser.add_argument("--python-version", required=True)
14+
args = parser.parse_args()
15+
16+
environment = default_environment()
17+
environment["python_version"] = args.python_version
18+
environment["python_full_version"] = args.python_version + ".99"
19+
requirements = {}
20+
for line in args.source.read_text().splitlines():
21+
line = line.strip()
22+
if not line or line.startswith("#"):
23+
continue
24+
requirement = Requirement(line)
25+
if requirement.marker and not requirement.marker.evaluate(environment):
26+
continue
27+
specifiers = list(requirement.specifier)
28+
if requirement.url or len(specifiers) != 1 or specifiers[0].operator != "==":
29+
parser.error(f"Expected an exact registry version: {line}")
30+
name = canonicalize_name(requirement.name)
31+
version = specifiers[0].version
32+
if name in requirements and requirements[name] != version:
33+
parser.error(f"Conflicting versions for {name} on Python {args.python_version}")
34+
requirements[name] = version
35+
36+
if not requirements:
37+
parser.error("No requirements selected")
38+
args.destination.write_text(
39+
"".join(f"{name}=={version}\n" for name, version in sorted(requirements.items()))
40+
)

‎scripts/check_notebook_audit.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Fail notebook audits except for the explicitly tracked unpatched advisory."""
2+
3+
import argparse
4+
import json
5+
from pathlib import Path
6+
7+
parser = argparse.ArgumentParser(description=__doc__)
8+
parser.add_argument("report", type=Path)
9+
args = parser.parse_args()
10+
report = json.loads(args.report.read_text())
11+
if "advisories" not in report or "metadata" not in report or "error" in report:
12+
parser.error("Expected a successful pnpm audit JSON response")
13+
14+
blocking = []
15+
for advisory in report["advisories"].values():
16+
advisory_id = advisory.get("github_advisory_id")
17+
if advisory_id == "GHSA-vfj7-8cjw-p6xm" and advisory.get("patched_versions") == "<0.0.0":
18+
print(
19+
"::warning::braces GHSA-vfj7-8cjw-p6xm remains unpatched in notebook "
20+
"build tooling. The full finding is retained in the audit artifact."
21+
)
22+
else:
23+
blocking.append(advisory_id or advisory["id"])
24+
25+
if blocking:
26+
print(f"Blocking notebook advisories: {blocking}")
27+
raise SystemExit(1)

0 commit comments

Comments
 (0)