From 7f1037c5d4cadff0a8a4d1386342c61016b37fe5 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 16 Aug 2026 00:13:02 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/link-foundation/python-ai-driven-development-pipeline-template/issues/58 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..b9f2fa6 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-08-16T00:13:02.696Z for PR creation at branch issue-58-f8d359692214 for issue https://github.com/link-foundation/python-ai-driven-development-pipeline-template/issues/58 \ No newline at end of file From 0acffaf28f4a35019672b9211d13e7a521734c47 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 16 Aug 2026 00:14:44 +0000 Subject: [PATCH 2/4] test: require continuous dependency audit --- tests/test_workflows.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 78cf5b8..3ddd51b 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -112,6 +112,7 @@ def test_security_workflow_scans_code_actions_and_dependencies() -> None: workflow = read_workflow("security.yml") codeql_job = workflow_job_block(workflow, "codeql") dependency_job = workflow_job_block(workflow, "dependency-review") + audit_job = workflow_job_block(workflow, "dependency-audit") assert "branches: [main]" in workflow assert "pull_request:" in workflow @@ -135,6 +136,26 @@ def test_security_workflow_scans_code_actions_and_dependencies() -> None: assert "fail-on-severity: high" in dependency_job assert "comment-summary-in-pr: on-failure" in dependency_job + assert "timeout-minutes: 15" in audit_job + assert "uses: actions/checkout@v6" in audit_job + assert "uses: actions/setup-python@v6" in audit_job + assert "python scripts/audit_dependencies.py" in audit_job + assert "pip-audit==2.10.1" in audit_job + assert "if: github.event_name == 'pull_request'" not in audit_job + + +def test_dependency_audit_maps_every_declared_surface() -> None: + """Every dependency declaration in the template must be audited.""" + script = (ROOT / "scripts" / "audit_dependencies.py").read_text(encoding="utf-8") + dependency_surfaces = [ROOT / "pyproject.toml", *ROOT.rglob("requirements*.txt")] + + assert dependency_surfaces + for surface in dependency_surfaces: + relative_surface = surface.relative_to(ROOT).as_posix() + assert relative_surface in script, ( + f"Dependency surface {relative_surface!r} has no audit mapping" + ) + def test_links_workflow_fails_for_every_broken_live_link() -> None: """Archived snapshots must not make broken live links pass validation.""" From f85317f86f352a12f392ca8581a1c6df4fc25c1e Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 16 Aug 2026 00:17:33 +0000 Subject: [PATCH 3/4] feat: audit resolved Python dependencies --- .github/workflows/security.yml | 18 +++ .../20260816_issue_58_dependency_audit.md | 4 + scripts/audit_dependencies.py | 120 ++++++++++++++++++ tests/test_workflows.py | 12 +- 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 changelog.d/20260816_issue_58_dependency_audit.md create mode 100644 scripts/audit_dependencies.py diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9b9c3ba..a252a4a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -11,6 +11,24 @@ permissions: contents: read jobs: + dependency-audit: + name: Audit Resolved Python Dependencies + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-dependency-audit + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Audit pyproject.toml and docs/requirements.txt + run: python scripts/audit_dependencies.py + codeql: name: CodeQL (${{ matrix.language }}) runs-on: ubuntu-latest diff --git a/changelog.d/20260816_issue_58_dependency_audit.md b/changelog.d/20260816_issue_58_dependency_audit.md new file mode 100644 index 0000000..cce3886 --- /dev/null +++ b/changelog.d/20260816_issue_58_dependency_audit.md @@ -0,0 +1,4 @@ +### Security + +- Audit the complete resolved Python dependency set on pushes, pull requests, + and a weekly schedule with pinned `pip-audit` tooling. diff --git a/scripts/audit_dependencies.py b/scripts/audit_dependencies.py new file mode 100644 index 0000000..1f26ba1 --- /dev/null +++ b/scripts/audit_dependencies.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Resolve and audit every Python dependency surface supported by the template.""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +import tomllib +from pathlib import Path + +PIP_AUDIT_VERSION = "2.10.1" +DEPENDENCY_SURFACES = ("pyproject.toml", "docs/requirements.txt") + + +def run(command: list[str], *, cwd: Path) -> str: + """Run a command, failing the audit when dependency resolution fails.""" + completed = subprocess.run( + command, + cwd=cwd, + check=True, + text=True, + stdout=subprocess.PIPE, + ) + output = completed.stdout.strip() + if output: + print(output) + return output + + +def python_executable(venv: Path) -> Path: + """Return the Python executable for a virtual environment.""" + scripts_directory = "Scripts" if sys.platform == "win32" else "bin" + executable = "python.exe" if sys.platform == "win32" else "python" + return venv / scripts_directory / executable + + +def project_install_target(project_root: Path) -> str: + """Build an install target that resolves every declared optional extra.""" + pyproject_path = project_root / "pyproject.toml" + with pyproject_path.open("rb") as pyproject_file: + pyproject = tomllib.load(pyproject_file) + extras = sorted(pyproject.get("project", {}).get("optional-dependencies", {})) + return f".[{','.join(extras)}]" if extras else "." + + +def audit_dependencies(project_root: Path) -> None: + """Resolve application dependencies in isolation and audit the result.""" + missing = [ + surface + for surface in DEPENDENCY_SURFACES + if not (project_root / surface).is_file() + ] + if missing: + message = f"Unmapped or missing dependency surfaces: {', '.join(missing)}" + raise FileNotFoundError(message) + + with tempfile.TemporaryDirectory(prefix="dependency-audit-") as temporary: + temporary_root = Path(temporary) + target_venv = temporary_root / "target" + audit_venv = temporary_root / "audit" + run([sys.executable, "-m", "venv", str(target_venv)], cwd=project_root) + run([sys.executable, "-m", "venv", str(audit_venv)], cwd=project_root) + + target_python = python_executable(target_venv) + audit_python = python_executable(audit_venv) + run( + [ + str(target_python), + "-m", + "pip", + "install", + project_install_target(project_root), + ], + cwd=project_root, + ) + run( + [ + str(target_python), + "-m", + "pip", + "install", + "-r", + "docs/requirements.txt", + ], + cwd=project_root, + ) + run( + [ + str(audit_python), + "-m", + "pip", + "install", + f"pip-audit=={PIP_AUDIT_VERSION}", + ], + cwd=project_root, + ) + site_packages = run( + [ + str(target_python), + "-c", + "import sysconfig; print(sysconfig.get_paths()['purelib'])", + ], + cwd=project_root, + ) + run( + [ + str(audit_python), + "-m", + "pip_audit", + "--path", + site_packages, + "--skip-editable", + ], + cwd=project_root, + ) + + +if __name__ == "__main__": + audit_dependencies(Path(__file__).resolve().parents[1]) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 3ddd51b..c528a20 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -140,9 +140,13 @@ def test_security_workflow_scans_code_actions_and_dependencies() -> None: assert "uses: actions/checkout@v6" in audit_job assert "uses: actions/setup-python@v6" in audit_job assert "python scripts/audit_dependencies.py" in audit_job - assert "pip-audit==2.10.1" in audit_job assert "if: github.event_name == 'pull_request'" not in audit_job + audit_script = (ROOT / "scripts" / "audit_dependencies.py").read_text( + encoding="utf-8" + ) + assert 'PIP_AUDIT_VERSION = "2.10.1"' in audit_script + def test_dependency_audit_maps_every_declared_surface() -> None: """Every dependency declaration in the template must be audited.""" @@ -152,9 +156,9 @@ def test_dependency_audit_maps_every_declared_surface() -> None: assert dependency_surfaces for surface in dependency_surfaces: relative_surface = surface.relative_to(ROOT).as_posix() - assert relative_surface in script, ( - f"Dependency surface {relative_surface!r} has no audit mapping" - ) + assert ( + relative_surface in script + ), f"Dependency surface {relative_surface!r} has no audit mapping" def test_links_workflow_fails_for_every_broken_live_link() -> None: From 546d073098d9495a4f4d0dfa4b83739e4d65ea69 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 16 Aug 2026 00:17:44 +0000 Subject: [PATCH 4/4] chore: remove pull request placeholder --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index b9f2fa6..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-08-16T00:13:02.696Z for PR creation at branch issue-58-f8d359692214 for issue https://github.com/link-foundation/python-ai-driven-development-pipeline-template/issues/58 \ No newline at end of file