From 124649feaff74ccf336d844ef788fd9b4ee75da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Houpert?= <10154151+lhoupert@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:25:49 +0000 Subject: [PATCH 1/3] feat: add bandit action instead of custom scripts --- README.md | 4 +- action.yml | 14 +++-- pyproject.toml | 1 - src/python_security_auditing/__main__.py | 5 +- src/python_security_auditing/report.py | 2 +- src/python_security_auditing/runners.py | 52 ++++++++++++----- src/python_security_auditing/settings.py | 13 ++--- tests/fixtures/bandit_clean.sarif | 13 +++++ tests/fixtures/bandit_issues.sarif | 62 ++++++++++++++++++++ tests/test_report.py | 2 +- tests/test_runners.py | 74 ++++++++++++++---------- tests/test_settings.py | 19 +++--- 12 files changed, 185 insertions(+), 76 deletions(-) create mode 100644 tests/fixtures/bandit_clean.sarif create mode 100644 tests/fixtures/bandit_issues.sarif diff --git a/README.md b/README.md index f7c860f..112ad8c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [p - uses: developmentseed/python-security-auditing@v1 with: package_manager: poetry - bandit_severity_threshold: MEDIUM + bandit_severity_threshold: medium pip_audit_block_on: all ``` @@ -44,7 +44,7 @@ Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [p |---|---|---| | `tools` | `bandit,pip-audit` | Comma-separated tools to run | | `bandit_scan_dirs` | `.` | Comma-separated directories for bandit to scan | -| `bandit_severity_threshold` | `HIGH` | Minimum severity that blocks the job: `HIGH`, `MEDIUM`, or `LOW` | +| `bandit_severity_threshold` | `high` | Minimum severity that blocks the job: `high`, `medium`, or `low` | | `pip_audit_block_on` | `fixable` | Block on: `fixable` (has a fix), `all`, or `none` | | `package_manager` | `requirements` | How to resolve deps: `uv`, `pip`, `poetry`, `pipenv`, `requirements` | | `requirements_file` | `requirements.txt` | Path when `package_manager=requirements` | diff --git a/action.yml b/action.yml index 3c96b90..7138bd7 100644 --- a/action.yml +++ b/action.yml @@ -10,8 +10,8 @@ inputs: description: Comma-separated directories for bandit to scan default: . bandit_severity_threshold: - description: Minimum bandit severity that blocks the job (HIGH, MEDIUM, LOW) - default: HIGH + description: Minimum bandit severity that blocks the job (high, medium, low) + default: high pip_audit_block_on: description: When to block on pip-audit findings — fixable, all, or none default: fixable @@ -34,6 +34,13 @@ inputs: runs: using: composite steps: + - name: Run Bandit (static security analysis) + if: contains(inputs.tools, 'bandit') + uses: lhoupert/bandit-action@18022d5292d04b21fae1bfa44597b94402ba7365 + with: + targets: ${{ inputs.bandit_scan_dirs }} + level: ${{ inputs.bandit_severity_threshold }} + - name: Set up Python uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: @@ -50,8 +57,8 @@ runs: working-directory: ${{ inputs.working_directory }} env: TOOLS: ${{ inputs.tools }} - BANDIT_SCAN_DIRS: ${{ inputs.bandit_scan_dirs }} BANDIT_SEVERITY_THRESHOLD: ${{ inputs.bandit_severity_threshold }} + BANDIT_SARIF_PATH: ${{ github.workspace }}/results.sarif PIP_AUDIT_BLOCK_ON: ${{ inputs.pip_audit_block_on }} PACKAGE_MANAGER: ${{ inputs.package_manager }} REQUIREMENTS_FILE: ${{ inputs.requirements_file }} @@ -66,7 +73,6 @@ runs: with: name: security-audit-reports path: | - ${{ inputs.working_directory }}/bandit-report.json ${{ inputs.working_directory }}/pip-audit-report.json if-no-files-found: ignore diff --git a/pyproject.toml b/pyproject.toml index 55b39df..f4806c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ license = { text = "MIT" } requires-python = ">=3.13" dependencies = [ "pydantic-settings>=2.0", - "bandit>=1.8", "pip-audit>=2.7", ] diff --git a/src/python_security_auditing/__main__.py b/src/python_security_auditing/__main__.py index f9043e0..0e822c1 100644 --- a/src/python_security_auditing/__main__.py +++ b/src/python_security_auditing/__main__.py @@ -3,11 +3,12 @@ from __future__ import annotations import sys +from pathlib import Path from typing import Any from .pr_comment import upsert_pr_comment from .report import build_markdown, check_thresholds, write_step_summary -from .runners import generate_requirements, run_bandit, run_pip_audit +from .runners import generate_requirements, read_bandit_sarif, run_pip_audit from .settings import Settings @@ -18,7 +19,7 @@ def main() -> None: pip_audit_report: list[dict[str, Any]] = [] if "bandit" in settings.enabled_tools: - bandit_report = run_bandit(settings.scan_directories) + bandit_report = read_bandit_sarif(Path(settings.bandit_sarif_path)) if "pip-audit" in settings.enabled_tools: requirements_path = generate_requirements(settings) diff --git a/src/python_security_auditing/report.py b/src/python_security_auditing/report.py index 059331a..915dfe4 100644 --- a/src/python_security_auditing/report.py +++ b/src/python_security_auditing/report.py @@ -65,7 +65,7 @@ def _bandit_section(report: dict[str, Any], settings: Settings) -> str: ] lines.append( f"\n_{len(results)} issue(s) found, " - f"{len(blocking_results)} at or above {settings.bandit_severity_threshold} threshold._\n" + f"{len(blocking_results)} at or above {settings.bandit_severity_threshold.upper()} threshold._\n" ) return "\n".join(lines) diff --git a/src/python_security_auditing/runners.py b/src/python_security_auditing/runners.py index f6417a8..44110ee 100644 --- a/src/python_security_auditing/runners.py +++ b/src/python_security_auditing/runners.py @@ -62,22 +62,46 @@ def generate_requirements(settings: Settings) -> Path: return out_path -def run_bandit(scan_dirs: list[str]) -> dict[str, Any]: - """Run bandit, write bandit-report.json, return parsed report.""" - output_file = Path("bandit-report.json") - cmd = ["bandit", "-r", *scan_dirs, "-f", "json", "-o", str(output_file)] - - result = subprocess.run(cmd, capture_output=True, text=True) - # bandit exits 1 when issues are found — that is expected, not an error - if result.returncode not in (0, 1): - print( - f"bandit exited with unexpected code {result.returncode}:\n{result.stderr}", - file=sys.stderr, +_SARIF_LEVEL_TO_SEVERITY: dict[str, str] = { + "error": "HIGH", + "warning": "MEDIUM", + "note": "LOW", + "none": "LOW", +} + + +def read_bandit_sarif(sarif_path: Path) -> dict[str, Any]: + """Read results.sarif produced by lhoupert/bandit-action, return bandit-style report dict.""" + if not sarif_path.exists(): + return {"results": [], "errors": []} + + sarif: dict[str, Any] = json.loads(sarif_path.read_text()) + sarif_results: list[dict[str, Any]] = sarif.get("runs", [{}])[0].get("results", []) + results: list[dict[str, Any]] = [] + for sarif_result in sarif_results: + props: dict[str, Any] = sarif_result.get("properties", {}) + severity = props.get("issue_severity") or _SARIF_LEVEL_TO_SEVERITY.get( + sarif_result.get("level", "none"), "LOW" + ) + locations: list[dict[str, Any]] = sarif_result.get("locations", []) + filename = "" + line_number = 0 + if locations: + phys = locations[0].get("physicalLocation", {}) + filename = phys.get("artifactLocation", {}).get("uri", "") + line_number = phys.get("region", {}).get("startLine", 0) + results.append( + { + "issue_severity": severity, + "issue_confidence": props.get("issue_confidence", ""), + "issue_text": sarif_result.get("message", {}).get("text", ""), + "filename": filename, + "line_number": line_number, + "test_id": sarif_result.get("ruleId", ""), + } ) - if output_file.exists(): - return dict(json.loads(output_file.read_text())) - return {"results": [], "errors": []} + return {"results": results, "errors": []} def run_pip_audit(requirements_path: Path) -> list[dict[str, Any]]: diff --git a/src/python_security_auditing/settings.py b/src/python_security_auditing/settings.py index 9d7591b..c2aefeb 100644 --- a/src/python_security_auditing/settings.py +++ b/src/python_security_auditing/settings.py @@ -15,9 +15,10 @@ class Settings(BaseSettings): # Tool selection tools: str = "bandit,pip-audit" - # Bandit config - bandit_scan_dirs: str = "." - bandit_severity_threshold: Literal["HIGH", "MEDIUM", "LOW"] = "HIGH" + # Bandit config — scan dirs and threshold are passed directly to lhoupert/bandit-action; + # the Python module only reads the SARIF output and uses the threshold for reporting. + bandit_severity_threshold: Literal["high", "medium", "low"] = "high" + bandit_sarif_path: str = "results.sarif" # pip-audit config pip_audit_block_on: Literal["fixable", "all", "none"] = "fixable" @@ -42,13 +43,9 @@ class Settings(BaseSettings): def enabled_tools(self) -> list[str]: return [t.strip() for t in self.tools.split(",") if t.strip()] - @property - def scan_directories(self) -> list[str]: - return [d.strip() for d in self.bandit_scan_dirs.split(",") if d.strip()] - @property def blocking_severities(self) -> list[str]: """All severities at or above the configured threshold.""" all_severities = ["LOW", "MEDIUM", "HIGH"] - threshold_idx = all_severities.index(self.bandit_severity_threshold) + threshold_idx = all_severities.index(self.bandit_severity_threshold.upper()) return all_severities[threshold_idx:] diff --git a/tests/fixtures/bandit_clean.sarif b/tests/fixtures/bandit_clean.sarif new file mode 100644 index 0000000..daa825c --- /dev/null +++ b/tests/fixtures/bandit_clean.sarif @@ -0,0 +1,13 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Bandit" + } + }, + "results": [] + } + ] +} diff --git a/tests/fixtures/bandit_issues.sarif b/tests/fixtures/bandit_issues.sarif new file mode 100644 index 0000000..a0aaa9e --- /dev/null +++ b/tests/fixtures/bandit_issues.sarif @@ -0,0 +1,62 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Bandit" + } + }, + "results": [ + { + "ruleId": "B404", + "level": "error", + "message": { + "text": "Consider possible security implications associated with subprocess module." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/app.py" + }, + "region": { + "startLine": 2 + } + } + } + ], + "properties": { + "issue_confidence": "HIGH", + "issue_severity": "HIGH", + "issue_text": "Consider possible security implications associated with subprocess module." + } + }, + { + "ruleId": "B602", + "level": "warning", + "message": { + "text": "subprocess call with shell=True identified, security issue." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/app.py" + }, + "region": { + "startLine": 5 + } + } + } + ], + "properties": { + "issue_confidence": "MEDIUM", + "issue_severity": "MEDIUM", + "issue_text": "subprocess call with shell=True identified, security issue." + } + } + ] + } + ] +} diff --git a/tests/test_report.py b/tests/test_report.py index d1fdf88..7bf36a4 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -73,7 +73,7 @@ def test_bandit_medium_does_not_block_at_high_threshold( def test_bandit_medium_blocks_at_medium_threshold( bandit_issues: dict[str, Any], pip_clean: list[Any], monkeypatch: pytest.MonkeyPatch ) -> None: - monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "MEDIUM") + monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "medium") s = Settings() medium_only = { **bandit_issues, diff --git a/tests/test_runners.py b/tests/test_runners.py index 6e8d568..4accb3e 100644 --- a/tests/test_runners.py +++ b/tests/test_runners.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch import pytest -from python_security_auditing.runners import generate_requirements, run_bandit, run_pip_audit +from python_security_auditing.runners import generate_requirements, read_bandit_sarif, run_pip_audit from python_security_auditing.settings import Settings FIXTURES = Path(__file__).parent / "fixtures" @@ -84,49 +84,61 @@ def test_pipenv_mode_calls_pipenv_requirements(monkeypatch: pytest.MonkeyPatch) # --------------------------------------------------------------------------- -# run_bandit +# read_bandit_sarif # --------------------------------------------------------------------------- -def test_run_bandit_parses_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.chdir(tmp_path) - fixture = json.loads((FIXTURES / "bandit_issues.json").read_text()) - - with patch("python_security_auditing.runners.subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=1, stderr="") - # Simulate bandit writing the output file - (tmp_path / "bandit-report.json").write_text(json.dumps(fixture)) - report = run_bandit(["src"]) +def test_read_bandit_sarif_parses_findings(tmp_path: Path) -> None: + sarif_path = tmp_path / "results.sarif" + sarif_path.write_text((FIXTURES / "bandit_issues.sarif").read_text()) + report = read_bandit_sarif(sarif_path) assert len(report["results"]) == 2 assert report["results"][0]["issue_severity"] == "HIGH" + assert report["results"][0]["issue_confidence"] == "HIGH" + assert report["results"][0]["test_id"] == "B404" + assert report["results"][0]["filename"] == "src/app.py" + assert report["results"][0]["line_number"] == 2 + assert report["results"][1]["issue_severity"] == "MEDIUM" -def test_run_bandit_returns_empty_on_missing_file( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - with patch("python_security_auditing.runners.subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0, stderr="") - report = run_bandit(["."]) - +def test_read_bandit_sarif_returns_empty_on_missing_file(tmp_path: Path) -> None: + report = read_bandit_sarif(tmp_path / "results.sarif") assert report["results"] == [] + assert report["errors"] == [] -def test_run_bandit_passes_dirs_to_cmd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.chdir(tmp_path) - (tmp_path / "bandit-report.json").write_text('{"results": [], "errors": []}') +def test_read_bandit_sarif_returns_empty_on_clean_sarif(tmp_path: Path) -> None: + sarif_path = tmp_path / "results.sarif" + sarif_path.write_text((FIXTURES / "bandit_clean.sarif").read_text()) + report = read_bandit_sarif(sarif_path) + assert report["results"] == [] - with patch("python_security_auditing.runners.subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0, stderr="") - run_bandit(["src/", "scripts/"]) - cmd = mock_run.call_args[0][0] - assert "src/" in cmd - assert "scripts/" in cmd - assert "-f" in cmd - assert "json" in cmd +def test_read_bandit_sarif_falls_back_to_level_mapping(tmp_path: Path) -> None: + sarif_path = tmp_path / "results.sarif" + sarif_path.write_text( + json.dumps( + { + "version": "2.1.0", + "runs": [ + { + "results": [ + { + "ruleId": "B999", + "level": "warning", + "message": {"text": "test issue"}, + "locations": [], + "properties": {}, + } + ] + } + ], + } + ) + ) + report = read_bandit_sarif(sarif_path) + assert report["results"][0]["issue_severity"] == "MEDIUM" # --------------------------------------------------------------------------- diff --git a/tests/test_settings.py b/tests/test_settings.py index 5d31edf..00ea19e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -7,8 +7,8 @@ def test_defaults() -> None: s = Settings() assert s.tools == "bandit,pip-audit" - assert s.bandit_scan_dirs == "." - assert s.bandit_severity_threshold == "HIGH" + assert s.bandit_severity_threshold == "high" + assert s.bandit_sarif_path == "results.sarif" assert s.pip_audit_block_on == "fixable" assert s.package_manager == "requirements" assert s.requirements_file == "requirements.txt" @@ -39,15 +39,10 @@ def test_enabled_tools_whitespace(monkeypatch: pytest.MonkeyPatch) -> None: assert s.enabled_tools == ["bandit", "pip-audit"] -def test_scan_directories_default() -> None: +def test_bandit_sarif_path_custom(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("BANDIT_SARIF_PATH", "/workspace/results.sarif") s = Settings() - assert s.scan_directories == ["."] - - -def test_scan_directories_multiple(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("BANDIT_SCAN_DIRS", "src/,scripts/") - s = Settings() - assert s.scan_directories == ["src/", "scripts/"] + assert s.bandit_sarif_path == "/workspace/results.sarif" def test_blocking_severities_high() -> None: @@ -56,13 +51,13 @@ def test_blocking_severities_high() -> None: def test_blocking_severities_medium(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "MEDIUM") + monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "medium") s = Settings() assert s.blocking_severities == ["MEDIUM", "HIGH"] def test_blocking_severities_low(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "LOW") + monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "low") s = Settings() assert s.blocking_severities == ["LOW", "MEDIUM", "HIGH"] From 02c5fc4865c5324f8a0d6a46a66b5f13fac8e749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Houpert?= <10154151+lhoupert@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:27:43 +0000 Subject: [PATCH 2/3] doc: update readme --- README.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 112ad8c..14f456f 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,100 @@ # python-security-auditing -Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [pip-audit](https://pypi.org/project/pip-audit/) on any Python repository. Posts findings as a PR comment and fails the job when blocking issues are found. +A reusable GitHub Action that runs **[bandit](https://bandit.readthedocs.io/)** (static code analysis) and **[pip-audit](https://pypi.org/project/pip-audit/)** (dependency vulnerability scanning) on any Python repository, then consolidates the results into a single PR comment, a workflow step summary, and a downloadable artifact. -## Usage +## Why this action instead of using bandit or pip-audit directly? -### Minimal (requirements.txt project) +| | `lhoupert/bandit-action` alone | `pypa/gh-action-pip-audit` alone | **this action** | +|---|---|---|---| +| Static code analysis (bandit) | ✅ | — | ✅ | +| Dependency vulnerability scan (pip-audit) | — | ✅ | ✅ | +| Unified PR comment | — | — | ✅ | +| Configurable blocking thresholds | partial | partial | ✅ | +| Multi-package-manager support | — | ✅ | ✅ | +| Workflow step summary | — | — | ✅ | +| Downloadable audit artifact | — | — | ✅ | + +The core value is the **reporting layer**: instead of two separate actions producing separate outputs you have to check individually, you get one PR comment that is created on first run and updated in place on every subsequent run. + +## What the PR comment looks like + +When issues are found, the comment posted to the PR looks like this: + +``` +# Security Audit Report + +## Bandit — Static Security Analysis + +| Severity | Confidence | File | Line | Issue | +|---|---|---|---|---| +| 🔴 HIGH | HIGH | `src/app.py` | 2 | [B404] Consider possible security implications associated with subprocess module. | +| 🟡 MEDIUM | MEDIUM | `src/app.py` | 5 | [B602] subprocess call with shell=True identified, security issue. | + +_2 issue(s) found, 1 at or above HIGH threshold._ + +## pip-audit — Dependency Vulnerabilities + +| Package | Version | ID | Fix Versions | Description | +|---|---|---|---|---| +| requests | 2.25.0 | GHSA-j8r2-6x86-q33q | 2.31.0 | Unintended leak of Proxy-Authorization header ... | + +_1 vulnerability/vulnerabilities found (1 fixable) across 1 package(s)._ + +--- +**Result: ❌ Blocking issues found — see details above.** +``` + +When everything is clean: + +``` +## Bandit — Static Security Analysis +✅ No issues found. + +## pip-audit — Dependency Vulnerabilities +✅ No vulnerabilities found. + +--- +**Result: ✅ No blocking issues found.** +``` + +The comment is idempotent — it is created once and updated in place on every push, so the PR thread stays clean. + +## Quickstart + +Add this to your workflow (e.g. `.github/workflows/security.yml`): ```yaml -- uses: developmentseed/python-security-auditing@v1 +name: Security Audit + +on: + pull_request: + push: + branches: [main] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: developmentseed/python-security-auditing@v1 ``` -### uv-based project +This runs both bandit and pip-audit with sensible defaults: blocks the job on HIGH-severity code issues and on dependency vulnerabilities that have a fix available. + +## Usage examples + +### uv project ```yaml - uses: developmentseed/python-security-auditing@v1 with: package_manager: uv - bandit_scan_dirs: 'src/,scripts/' + bandit_scan_dirs: 'src/' ``` -### Poetry project, stricter thresholds +### Poetry project with stricter thresholds + +Block on any bandit finding at MEDIUM or above, and on all known vulnerabilities regardless of whether a fix exists: ```yaml - uses: developmentseed/python-security-auditing@v1 @@ -29,7 +104,9 @@ Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [p pip_audit_block_on: all ``` -### Bandit only (no dependency audit) +### Bandit only (skip dependency audit) + +Useful when you manage dependencies externally or run pip-audit in a separate job: ```yaml - uses: developmentseed/python-security-auditing@v1 @@ -38,6 +115,47 @@ Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [p bandit_scan_dirs: 'src/' ``` +### Project in a subdirectory (monorepo) + +```yaml +- uses: developmentseed/python-security-auditing@v1 + with: + working_directory: services/api + package_manager: uv + bandit_scan_dirs: 'services/api/src/' +``` + +### Audit-only mode (never block the job) + +Run the audit and post the comment for visibility, but don't fail CI: + +```yaml +- uses: developmentseed/python-security-auditing@v1 + with: + bandit_severity_threshold: low # report everything + pip_audit_block_on: none # never block +``` + +## How blocking works + +The job fails (non-zero exit) when **either** tool finds issues above its configured threshold. + +**Bandit threshold** (`bandit_severity_threshold`): findings at or above the threshold block the job. + +| `bandit_severity_threshold` | Blocks on | +|---|---| +| `high` (default) | 🔴 HIGH only | +| `medium` | 🟡 MEDIUM and 🔴 HIGH | +| `low` | 🟢 LOW, 🟡 MEDIUM, and 🔴 HIGH | + +**pip-audit threshold** (`pip_audit_block_on`): + +| `pip_audit_block_on` | Blocks on | +|---|---| +| `fixable` (default) | Vulnerabilities with a fix available — you can act on these immediately | +| `all` | All known vulnerabilities, including those with no fix yet | +| `none` | Never blocks — audit runs but CI stays green | + ## Inputs | Input | Default | Description | @@ -45,18 +163,19 @@ Reusable GitHub Action that runs [bandit](https://bandit.readthedocs.io/) and [p | `tools` | `bandit,pip-audit` | Comma-separated tools to run | | `bandit_scan_dirs` | `.` | Comma-separated directories for bandit to scan | | `bandit_severity_threshold` | `high` | Minimum severity that blocks the job: `high`, `medium`, or `low` | -| `pip_audit_block_on` | `fixable` | Block on: `fixable` (has a fix), `all`, or `none` | -| `package_manager` | `requirements` | How to resolve deps: `uv`, `pip`, `poetry`, `pipenv`, `requirements` | -| `requirements_file` | `requirements.txt` | Path when `package_manager=requirements` | +| `pip_audit_block_on` | `fixable` | When pip-audit findings block the job: `fixable`, `all`, or `none` | +| `package_manager` | `requirements` | How to resolve deps for pip-audit: `uv`, `pip`, `poetry`, `pipenv`, `requirements` | +| `requirements_file` | `requirements.txt` | Path to requirements file when `package_manager=requirements` | +| `working_directory` | `.` | Directory to run the audit from (useful for monorepos) | | `post_pr_comment` | `true` | Post/update a PR comment with scan results | -| `github_token` | `${{ github.token }}` | Token for PR comments | +| `github_token` | `${{ github.token }}` | Token used for posting PR comments | ## Outputs -- **Step summary** — written to the workflow run summary. -- **PR comment** — upserted on every run (idempotent via `` marker). -- **Artifacts** — `bandit-report.json` and `pip-audit-report.json` uploaded as `security-audit-reports`. -- **Exit code** — non-zero when blocking issues are found. +- **PR comment** — created on first run, updated in place on every subsequent run (keyed on a hidden `` marker). +- **Step summary** — the same report is written to the workflow run summary, visible under the "Summary" tab. +- **Artifact** — `pip-audit-report.json` uploaded as `security-audit-reports` for download or downstream steps. +- **Exit code** — non-zero when blocking issues are found, so the job fails and branch protections can enforce it. ## Development From 65de97454ae2c351b6f9998d029075d9e011fc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Houpert?= <10154151+lhoupert@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:33:32 +0000 Subject: [PATCH 3/3] chore: fix formatting --- src/python_security_auditing/report.py | 3 +- uv.lock | 62 -------------------------- 2 files changed, 2 insertions(+), 63 deletions(-) diff --git a/src/python_security_auditing/report.py b/src/python_security_auditing/report.py index 915dfe4..c46f8dd 100644 --- a/src/python_security_auditing/report.py +++ b/src/python_security_auditing/report.py @@ -65,7 +65,8 @@ def _bandit_section(report: dict[str, Any], settings: Settings) -> str: ] lines.append( f"\n_{len(results)} issue(s) found, " - f"{len(blocking_results)} at or above {settings.bandit_severity_threshold.upper()} threshold._\n" + f"{len(blocking_results)} at or above " + f"{settings.bandit_severity_threshold.upper()} threshold._\n" ) return "\n".join(lines) diff --git a/uv.lock b/uv.lock index dc735eb..243d792 100644 --- a/uv.lock +++ b/uv.lock @@ -11,21 +11,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "bandit" -version = "1.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "stevedore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, -] - [[package]] name = "boolean-py" version = "5.0" @@ -584,7 +569,6 @@ name = "python-security-auditing" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "bandit" }, { name = "pip-audit" }, { name = "pydantic-settings" }, ] @@ -600,7 +584,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "bandit", specifier = ">=1.8" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, { name = "pip-audit", specifier = ">=2.7" }, { name = "pydantic-settings", specifier = ">=2.0" }, @@ -611,42 +594,6 @@ requires-dist = [ ] provides-extras = ["dev"] -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - [[package]] name = "requests" version = "2.33.0" @@ -709,15 +656,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] -[[package]] -name = "stevedore" -version = "5.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, -] - [[package]] name = "tomli" version = "2.4.1"