diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..c3dee2a --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,328 @@ +name: Integration Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: integration-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + + # ── Run each test scenario against the local composite action ── + integration-test: + name: "Test ${{ matrix.id }}" + runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - id: "01" + working_directory: integration-tests/cases/01-requirements-flat + package_manager: requirements + requirements_file: requirements.txt + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: "" + + - id: "02" + working_directory: integration-tests/cases/02-requirements-src-bandit + package_manager: requirements + requirements_file: requirements.txt + bandit_scan_dirs: src/ + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: "" + + - id: "03" + working_directory: integration-tests/cases/03-requirements-multi-both + package_manager: requirements + requirements_file: requirements.txt + bandit_scan_dirs: "src/,scripts/" + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: "" + + - id: "04" + working_directory: integration-tests/cases/04-uv-flat + package_manager: uv + requirements_file: "" + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: uv + + - id: "05" + working_directory: integration-tests/cases/05-uv-src-vuln + package_manager: uv + requirements_file: "" + bandit_scan_dirs: src/ + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: uv + + - id: "06" + working_directory: integration-tests/cases/06-uv-multi-bandit + package_manager: uv + requirements_file: "" + bandit_scan_dirs: "src/,scripts/" + bandit_severity_threshold: medium + pip_audit_block_on: none + tools: "bandit,pip-audit" + setup: uv + + - id: "07" + working_directory: integration-tests/cases/07-poetry-flat + package_manager: poetry + requirements_file: "" + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: poetry + + - id: "08" + working_directory: integration-tests/cases/08-poetry-src-both + package_manager: poetry + requirements_file: "" + bandit_scan_dirs: src/ + bandit_severity_threshold: medium + pip_audit_block_on: all + tools: "bandit,pip-audit" + setup: poetry + + - id: "09" + working_directory: integration-tests/cases/09-pipenv-flat + package_manager: pipenv + requirements_file: "" + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: pipenv + + - id: "10" + working_directory: integration-tests/cases/10-pipenv-multi-bandit + package_manager: pipenv + requirements_file: "" + bandit_scan_dirs: "src/,scripts/" + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: pipenv + + # Test 11: working_directory is repo root; paths are prefixed with integration-tests/cases/ + - id: "11" + working_directory: "." + package_manager: requirements + requirements_file: integration-tests/cases/11-requirements-root/requirements.txt + bandit_scan_dirs: integration-tests/cases/11-requirements-root + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: "" + + # Tests 12 & 14: bandit-only — no lockfile setup needed + - id: "12" + working_directory: integration-tests/cases/12-uv-flat-bandit-only + package_manager: uv + requirements_file: "" + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: bandit + setup: "" + + - id: "13" + working_directory: integration-tests/cases/13-requirements-unfixable + package_manager: requirements + requirements_file: requirements.txt + bandit_scan_dirs: "." + bandit_severity_threshold: high + pip_audit_block_on: fixable + tools: "bandit,pip-audit" + setup: "" + + - id: "14" + working_directory: integration-tests/cases/14-uv-low-threshold + package_manager: uv + requirements_file: "" + bandit_scan_dirs: "." + bandit_severity_threshold: low + pip_audit_block_on: fixable + tools: bandit + setup: "" + + permissions: + contents: read + + steps: + - name: Checkout action repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + # --- Package-manager-specific lockfile setup --- + + - name: Set up uv + if: matrix.setup == 'uv' + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.3.1 + + - name: Generate uv.lock + if: matrix.setup == 'uv' + working-directory: ${{ matrix.working_directory }} + run: uv lock + + - name: Set up Poetry + if: matrix.setup == 'poetry' + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 + + - name: Generate poetry.lock + if: matrix.setup == 'poetry' + working-directory: ${{ matrix.working_directory }} + run: poetry lock + + - name: Set up Python (for pipenv) + if: matrix.setup == 'pipenv' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - name: Install pipenv + if: matrix.setup == 'pipenv' + run: pip install pipenv + + - name: Generate Pipfile.lock + if: matrix.setup == 'pipenv' + working-directory: ${{ matrix.working_directory }} + run: pipenv install + + # --- Run the composite action at the current commit --- + + - name: Run security audit + id: audit + uses: ./ + continue-on-error: true + with: + working_directory: ${{ matrix.working_directory }} + package_manager: ${{ matrix.package_manager }} + tools: ${{ matrix.tools }} + requirements_file: ${{ matrix.requirements_file }} + bandit_scan_dirs: ${{ matrix.bandit_scan_dirs }} + bandit_severity_threshold: ${{ matrix.bandit_severity_threshold }} + pip_audit_block_on: ${{ matrix.pip_audit_block_on }} + post_pr_comment: 'false' + artifact_name: security-audit-${{ matrix.id }} + + # --- Record outcome so the validate job can reconstruct NEEDS_JSON --- + + - name: Record step outcome + if: always() + run: | + mkdir -p outcome + echo "${{ steps.audit.outcome }}" > outcome/outcome.txt + + - name: Upload outcome + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: test-outcome-${{ matrix.id }} + path: outcome/outcome.txt + + # ── Validate all results against expected_results.yml ────────── + validate: + name: Validate results + if: always() + needs: [integration-test] + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout action repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Download security-audit artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: security-audit-* + path: artifacts + + - name: Download outcome artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: test-outcome-* + path: artifacts + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install pyyaml + + # Build NEEDS_JSON from outcome files — validate_results.py expects + # {"test-01": {"result": "success"}, ...} (same shape as toJSON(needs)) + - name: Build NEEDS_JSON from outcome artifacts + run: | + python3 - <<'EOF' >> "$GITHUB_ENV" + import json + from pathlib import Path + needs = {} + for f in sorted(Path("artifacts").glob("test-outcome-*/outcome.txt")): + num = f.parent.name.replace("test-outcome-", "") + needs["test-" + num] = {"result": f.read_text().strip()} + print("NEEDS_JSON=" + json.dumps(needs)) + EOF + + - name: Validate test outcomes + env: + NEEDS_JSON: ${{ env.NEEDS_JSON }} + run: python integration-tests/validate_results.py + + - name: Post or update PR comment + if: always() && github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ ! -f validation-report.md ]; then + echo "No report generated" >&2 + exit 0 + fi + + MARKER="" + PR_NUMBER="${{ github.event.pull_request.number }}" + + # Find existing comment with our marker + COMMENT_ID=$( + gh api \ + "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + --paginate -q \ + ".[] | select(.body | contains(\"${MARKER}\")) | .id" \ + | head -n 1 + ) + + if [ -n "$COMMENT_ID" ]; then + gh api \ + "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \ + --method PATCH \ + -F "body=@validation-report.md" + echo "Updated existing comment ${COMMENT_ID}" + else + gh pr comment "${PR_NUMBER}" --body-file validation-report.md + echo "Created new comment" + fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a52573..b3f4fd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,10 +10,12 @@ repos: rev: v1.10.0 hooks: - id: mypy + exclude: ^integration-tests/cases/ additional_dependencies: - pydantic-settings>=2.0 - bandit>=1.8 - pytest>=8.0 + - types-PyYAML>=6.0 - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.23.1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1847755 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "uv": true + } +} \ No newline at end of file diff --git a/integration-tests/cases/01-requirements-flat/README.md b/integration-tests/cases/01-requirements-flat/README.md new file mode 100644 index 0000000..4209751 --- /dev/null +++ b/integration-tests/cases/01-requirements-flat/README.md @@ -0,0 +1,21 @@ +# 01 · requirements · flat layout · clean + +**Package manager**: `requirements` +**Source layout**: flat (Python files at project root) +**Expected outcome**: PASS + +## What this tests + +- Basic `requirements.txt` with pinned safe versions +- Bandit scans the flat project directory (no src/ subdirectory) +- pip-audit finds no vulnerabilities + +## Action settings + +| Setting | Value | +|---------|-------| +| `package_manager` | `requirements` | +| `requirements_file` | `requirements.txt` | +| `bandit_scan_dirs` | `.` | +| `bandit_severity_threshold` | `HIGH` | +| `pip_audit_block_on` | `fixable` | diff --git a/integration-tests/cases/01-requirements-flat/app.py b/integration-tests/cases/01-requirements-flat/app.py new file mode 100644 index 0000000..1de3c42 --- /dev/null +++ b/integration-tests/cases/01-requirements-flat/app.py @@ -0,0 +1,15 @@ +"""Simple HTTP client — clean code for security audit baseline testing.""" +import requests +import click + + +@click.command() +@click.argument("url") +def fetch(url: str) -> None: + """Fetch a URL and print the response status.""" + response = requests.get(url, timeout=10) + click.echo(f"Status: {response.status_code}") + + +if __name__ == "__main__": + fetch() diff --git a/integration-tests/cases/01-requirements-flat/requirements.txt b/integration-tests/cases/01-requirements-flat/requirements.txt new file mode 100644 index 0000000..8ed067f --- /dev/null +++ b/integration-tests/cases/01-requirements-flat/requirements.txt @@ -0,0 +1,2 @@ +requests==2.33.0 +click==8.1.7 diff --git a/integration-tests/cases/02-requirements-src-bandit/README.md b/integration-tests/cases/02-requirements-src-bandit/README.md new file mode 100644 index 0000000..9beec43 --- /dev/null +++ b/integration-tests/cases/02-requirements-src-bandit/README.md @@ -0,0 +1,19 @@ +# 02 · requirements · src/ layout · bandit HIGH + +**Package manager**: `requirements` +**Source layout**: `src/` subdirectory +**Expected outcome**: FAIL (bandit) + +## What this tests + +- Bandit correctly scans `src/` subdirectory only +- B602 (`subprocess.call(shell=True)`) is detected as HIGH severity +- B105 (hardcoded password) is detected as MEDIUM severity +- Workflow fails because B602 meets the HIGH threshold + +## Intentional issues + +| File | Issue | Severity | +|------|-------|---------| +| `src/app.py` | B602: `subprocess.call(cmd, shell=True)` | HIGH | +| `src/app.py` | B105: hardcoded password string | MEDIUM | diff --git a/integration-tests/cases/02-requirements-src-bandit/requirements.txt b/integration-tests/cases/02-requirements-src-bandit/requirements.txt new file mode 100644 index 0000000..9324823 --- /dev/null +++ b/integration-tests/cases/02-requirements-src-bandit/requirements.txt @@ -0,0 +1,2 @@ +httpx==0.27.0 +click==8.1.7 diff --git a/integration-tests/cases/02-requirements-src-bandit/src/app.py b/integration-tests/cases/02-requirements-src-bandit/src/app.py new file mode 100644 index 0000000..ece2f07 --- /dev/null +++ b/integration-tests/cases/02-requirements-src-bandit/src/app.py @@ -0,0 +1,15 @@ +"""Command runner — intentionally insecure for bandit testing.""" +import subprocess + + +def run_command(cmd: str) -> int: + """Run a shell command.""" + # B602: subprocess call with shell=True — HIGH severity + return subprocess.call(cmd, shell=True) # noqa: S602 + + +def get_config() -> dict: + """Return config dict.""" + # B105: hardcoded password string — MEDIUM severity + password = "supersecret123" # noqa: S105 + return {"password": password} diff --git a/integration-tests/cases/03-requirements-multi-both/README.md b/integration-tests/cases/03-requirements-multi-both/README.md new file mode 100644 index 0000000..08db23f --- /dev/null +++ b/integration-tests/cases/03-requirements-multi-both/README.md @@ -0,0 +1,21 @@ +# 03 · requirements · src/+scripts/ · bandit HIGH + pip-audit + +**Package manager**: `requirements` +**Source layout**: `src/` + `scripts/` directories +**Expected outcome**: FAIL (bandit AND pip-audit) + +## What this tests + +- Bandit scans multiple directories (`src/,scripts/`) in one run +- B602 in `src/` triggers at HIGH threshold +- pip-audit detects fixable CVEs in pinned vulnerable versions +- Both tools report issues simultaneously + +## Intentional issues + +| File | Issue | Severity | +|------|-------|---------| +| `src/processor.py` | B602: `subprocess.call(shell=True)` | HIGH | +| `scripts/run.py` | B105: hardcoded password | MEDIUM | +| `requirements.txt` | `requests==2.25.0` — CVE-2023-32681 | fixable | +| `requirements.txt` | `Pillow==9.0.0` — multiple CVEs | fixable | diff --git a/integration-tests/cases/03-requirements-multi-both/requirements.txt b/integration-tests/cases/03-requirements-multi-both/requirements.txt new file mode 100644 index 0000000..3ae48db --- /dev/null +++ b/integration-tests/cases/03-requirements-multi-both/requirements.txt @@ -0,0 +1,3 @@ +requests==2.25.0 +cryptography==38.0.0 +click==8.1.7 diff --git a/integration-tests/cases/03-requirements-multi-both/scripts/run.py b/integration-tests/cases/03-requirements-multi-both/scripts/run.py new file mode 100644 index 0000000..6f8a6f3 --- /dev/null +++ b/integration-tests/cases/03-requirements-multi-both/scripts/run.py @@ -0,0 +1,8 @@ +"""Runner script — intentionally insecure for bandit testing.""" + + +def authenticate(user: str) -> dict: + """Return auth config for a user.""" + # B105: hardcoded password string — MEDIUM severity + db_password = "hardcoded_db_pass" # noqa: S105 + return {"user": user, "db_password": db_password} diff --git a/integration-tests/cases/03-requirements-multi-both/src/processor.py b/integration-tests/cases/03-requirements-multi-both/src/processor.py new file mode 100644 index 0000000..ece1508 --- /dev/null +++ b/integration-tests/cases/03-requirements-multi-both/src/processor.py @@ -0,0 +1,8 @@ +"""Data processor — intentionally insecure for bandit testing.""" +import subprocess + + +def process(data: str) -> None: + """Process data via shell command.""" + # B602: subprocess with shell=True — HIGH severity + subprocess.call(f"process.sh {data}", shell=True) # noqa: S602 diff --git a/integration-tests/cases/04-uv-flat/README.md b/integration-tests/cases/04-uv-flat/README.md new file mode 100644 index 0000000..a104b82 --- /dev/null +++ b/integration-tests/cases/04-uv-flat/README.md @@ -0,0 +1,11 @@ +# 04 · uv · flat layout · clean + +**Package manager**: `uv` +**Source layout**: flat (Python files at project root) +**Expected outcome**: PASS + +## What this tests + +- Bandit scans the flat project directory +- No bandit issues, no vulnerable deps +- `uv.lock` is **not committed** — tests the action's behaviour when no lockfile is present diff --git a/integration-tests/cases/04-uv-flat/app.py b/integration-tests/cases/04-uv-flat/app.py new file mode 100644 index 0000000..b0e83d8 --- /dev/null +++ b/integration-tests/cases/04-uv-flat/app.py @@ -0,0 +1,12 @@ +"""Simple app — clean code for uv/flat security audit testing.""" +import httpx +from rich.console import Console + +console = Console() + + +def fetch(url: str) -> None: + """Fetch a URL and display the response status.""" + with httpx.Client() as client: + response = client.get(url) + console.print(f"[green]Status:[/green] {response.status_code}") diff --git a/integration-tests/cases/04-uv-flat/pyproject.toml b/integration-tests/cases/04-uv-flat/pyproject.toml new file mode 100644 index 0000000..d73c075 --- /dev/null +++ b/integration-tests/cases/04-uv-flat/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "uv-flat-clean" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "httpx>=0.27.0", + "rich>=13.9.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/integration-tests/cases/05-uv-src-vuln/README.md b/integration-tests/cases/05-uv-src-vuln/README.md new file mode 100644 index 0000000..1b4ee25 --- /dev/null +++ b/integration-tests/cases/05-uv-src-vuln/README.md @@ -0,0 +1,19 @@ +# 05 · uv · src/ layout · pip-audit vuln + +**Package manager**: `uv` +**Source layout**: `src/` subdirectory +**Expected outcome**: FAIL (pip-audit) + +## What this tests + +- `uv export` resolves and exports `requests==2.25.0` from `uv.lock` +- pip-audit detects the fixable CVE-2023-32681 in `requests==2.25.0` +- Bandit finds no issues in clean code +- Workflow fails due to pip-audit finding a fixable vulnerability +- `uv.lock` is **committed** — required so pip-audit reliably resolves the pinned vulnerable version + +## Intentional issues + +| Dependency | Version | CVE | +|-----------|---------|-----| +| `requests` | `2.25.0` | CVE-2023-32681 (fixable — upgrade to ≥2.31.0) | diff --git a/integration-tests/cases/05-uv-src-vuln/pyproject.toml b/integration-tests/cases/05-uv-src-vuln/pyproject.toml new file mode 100644 index 0000000..95a31eb --- /dev/null +++ b/integration-tests/cases/05-uv-src-vuln/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "uv-src-vuln" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "requests==2.25.0", + "click>=8.1.7", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/uv_src_vuln"] diff --git a/integration-tests/cases/05-uv-src-vuln/src/fetcher.py b/integration-tests/cases/05-uv-src-vuln/src/fetcher.py new file mode 100644 index 0000000..239f8b7 --- /dev/null +++ b/integration-tests/cases/05-uv-src-vuln/src/fetcher.py @@ -0,0 +1,9 @@ +"""HTTP fetcher — clean code, but depends on vulnerable requests version.""" +import requests + + +def get_resource(url: str) -> bytes: + """Fetch a remote resource and return its content.""" + response = requests.get(url, timeout=30) + response.raise_for_status() + return response.content diff --git a/integration-tests/cases/05-uv-src-vuln/src/uv_src_vuln/__init__.py b/integration-tests/cases/05-uv-src-vuln/src/uv_src_vuln/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration-tests/cases/05-uv-src-vuln/uv.lock b/integration-tests/cases/05-uv-src-vuln/uv.lock new file mode 100644 index 0000000..b298e40 --- /dev/null +++ b/integration-tests/cases/05-uv-src-vuln/uv.lock @@ -0,0 +1,90 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "chardet" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", size = 1868453, upload-time = "2017-06-08T14:34:35.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", size = 133356, upload-time = "2017-06-08T14:34:33.552Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "idna" +version = "2.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", size = 175616, upload-time = "2020-06-27T23:45:05.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/38/928ddce2273eaa564f6f50de919327bf3a00f091b5baba8dfa9460f3a8a8/idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0", size = 58811, upload-time = "2020-06-27T23:45:03.457Z" }, +] + +[[package]] +name = "requests" +version = "2.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "chardet" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/14/4a6542a078773957aa83101336375c9597e6fe5889d20abda9c38f9f3ff2/requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", size = 101897, upload-time = "2020-11-11T20:05:17.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/fc/f91eac5a39a65f75a7adb58eac7fa78871ea9872283fb9c44e6545998134/requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998", size = 61132, upload-time = "2020-11-11T20:05:15.87Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "uv-src-vuln" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1.7" }, + { name = "requests", specifier = "==2.25.0" }, +] diff --git a/integration-tests/cases/06-uv-multi-bandit/README.md b/integration-tests/cases/06-uv-multi-bandit/README.md new file mode 100644 index 0000000..dd0f520 --- /dev/null +++ b/integration-tests/cases/06-uv-multi-bandit/README.md @@ -0,0 +1,20 @@ +# 06 · uv · src/+scripts/ · bandit MEDIUM + +**Package manager**: `uv` +**Source layout**: `src/` + `scripts/` directories +**Expected outcome**: FAIL (bandit) + +## What this tests + +- Bandit scans two directories via `bandit_scan_dirs: src/,scripts/` +- MEDIUM severity threshold (lower than default HIGH) catches B324 and B506 +- `uv export` produces clean deps — no pip-audit failures +- `pip_audit_block_on: none` ensures only bandit can fail this job +- `uv.lock` is committed — tests the action with a pre-existing lockfile + +## Intentional issues + +| File | Issue | Severity | +|------|-------|----------| +| `src/parser.py` | B506: `yaml.load()` without Loader | MEDIUM | +| `scripts/digest.py` | B324: `hashlib.md5()` | MEDIUM | diff --git a/integration-tests/cases/06-uv-multi-bandit/pyproject.toml b/integration-tests/cases/06-uv-multi-bandit/pyproject.toml new file mode 100644 index 0000000..8c46392 --- /dev/null +++ b/integration-tests/cases/06-uv-multi-bandit/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "uv-multi-bandit" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "pyyaml>=6.0.2", + "httpx>=0.27.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/integration-tests/cases/06-uv-multi-bandit/scripts/digest.py b/integration-tests/cases/06-uv-multi-bandit/scripts/digest.py new file mode 100644 index 0000000..ef5fd54 --- /dev/null +++ b/integration-tests/cases/06-uv-multi-bandit/scripts/digest.py @@ -0,0 +1,8 @@ +"""Digest utility — intentionally insecure for bandit testing.""" +import hashlib + + +def compute_hash(data: bytes) -> str: + """Compute a digest of the given data.""" + # B303: use of MD5 — MEDIUM severity + return hashlib.md5(data).hexdigest() # noqa: S324 diff --git a/integration-tests/cases/06-uv-multi-bandit/src/parser.py b/integration-tests/cases/06-uv-multi-bandit/src/parser.py new file mode 100644 index 0000000..bac5f02 --- /dev/null +++ b/integration-tests/cases/06-uv-multi-bandit/src/parser.py @@ -0,0 +1,8 @@ +"""YAML parser — intentionally insecure for bandit testing.""" +import yaml + + +def parse_config(data: str) -> dict: + """Parse YAML config string.""" + # B506: yaml.load without Loader — MEDIUM severity + return yaml.load(data) # noqa: S506 diff --git a/integration-tests/cases/06-uv-multi-bandit/uv.lock b/integration-tests/cases/06-uv-multi-bandit/uv.lock new file mode 100644 index 0000000..626775a --- /dev/null +++ b/integration-tests/cases/06-uv-multi-bandit/uv.lock @@ -0,0 +1,150 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { 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 = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "uv-multi-bandit" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, +] diff --git a/integration-tests/cases/07-poetry-flat/README.md b/integration-tests/cases/07-poetry-flat/README.md new file mode 100644 index 0000000..37049a2 --- /dev/null +++ b/integration-tests/cases/07-poetry-flat/README.md @@ -0,0 +1,11 @@ +# 07 · poetry · flat layout · clean + +**Package manager**: `poetry` +**Source layout**: flat (Python files at project root) +**Expected outcome**: PASS + +## What this tests + +- Bandit scans the flat project directory +- No bandit issues, no vulnerable deps +- `poetry.lock` is **not committed** — tests the action's behaviour when no lockfile is present diff --git a/integration-tests/cases/07-poetry-flat/app.py b/integration-tests/cases/07-poetry-flat/app.py new file mode 100644 index 0000000..df69d2d --- /dev/null +++ b/integration-tests/cases/07-poetry-flat/app.py @@ -0,0 +1,15 @@ +"""Simple CLI app — clean code for poetry/flat security audit testing.""" +import click +import httpx + +@click.command() +@click.option("--url", default="https://example.com", help="URL to fetch.") +def fetch(url: str) -> None: + """Fetch a URL and print the HTTP status code.""" + with httpx.Client() as client: + response = client.get(url) + click.echo(f"Status: {response.status_code}") + + +if __name__ == "__main__": + fetch() diff --git a/integration-tests/cases/07-poetry-flat/pyproject.toml b/integration-tests/cases/07-poetry-flat/pyproject.toml new file mode 100644 index 0000000..03f1cf9 --- /dev/null +++ b/integration-tests/cases/07-poetry-flat/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "poetry-flat-clean" +version = "0.1.0" +description = "Clean poetry project for security audit testing" +authors = ["Test "] + +[tool.poetry.dependencies] +python = ">=3.11" +httpx = ">=0.27.0" +click = ">=8.1.7" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/integration-tests/cases/08-poetry-src-both/README.md b/integration-tests/cases/08-poetry-src-both/README.md new file mode 100644 index 0000000..27e0cb0 --- /dev/null +++ b/integration-tests/cases/08-poetry-src-both/README.md @@ -0,0 +1,22 @@ +# 08 · poetry · src/ layout · bandit MEDIUM + pip-audit + +**Package manager**: `poetry` +**Source layout**: `src/` subdirectory +**Expected outcome**: FAIL (bandit AND pip-audit) + +## What this tests + +- `poetry export` resolves and exports vulnerable pinned versions +- MEDIUM bandit threshold catches B324 and B105 in `src/` +- `pip_audit_block_on: all` blocks on any vulnerability (fixable or not) +- Both tools report issues simultaneously +- `poetry.lock` is **committed** — tests the action with a pre-existing lockfile + +## Intentional issues + +| File/Dep | Issue | Severity | +|---------|-------|---------| +| `src/auth.py` | B324: `hashlib.md5()` | MEDIUM | +| `src/auth.py` | B105: hardcoded token string | MEDIUM | +| `cryptography==38.0.0` | CVE-2023-49083 | fixable | +| `requests==2.25.0` | CVE-2023-32681 | fixable | diff --git a/integration-tests/cases/08-poetry-src-both/poetry.lock b/integration-tests/cases/08-poetry-src-both/poetry.lock new file mode 100644 index 0000000..3701c91 --- /dev/null +++ b/integration-tests/cases/08-poetry-src-both/poetry.lock @@ -0,0 +1,238 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] + +[[package]] +name = "cryptography" +version = "38.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "cryptography-38.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:c27e541b532d13a15fe969977c55753d0f187696280ba073273efb0c733e522b"}, + {file = "cryptography-38.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:b224d7e73676a3ea27cd1a726591c7984db32ceb63a801e11d80669eb9964f37"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8fe3f1df20286d50dc5ab201cce4bf5c326e813d7fa39a62ed41bc01d46cd3ab"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:684134063c108b6a8af59c1869e09400a1c078336e79fa8cc1cbfb82e67d992a"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:634f38529aac216844df4df8632cdc1d7fff11c1ba2440c7299c38c1aa7a3e19"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b69f3f316711567f03eab907713a2c076f43fc84f92a6d7ce5ffc8c26e6709bc"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7208cf3fa37e4c2d3ec9b49ac7721c457b793c64d548c4f7f2989ded0405f700"}, + {file = "cryptography-38.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ebe9e75f9a759e1019bdd2a823b51c8d4002bed006d60c025e3d6b9dff33ebaa"}, + {file = "cryptography-38.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7ac685fe7c1103f5802e52a49f90f010dd768fd1cffd797fe01df2c674f3862a"}, + {file = "cryptography-38.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:144c9b09135caf6580fb8bf03594760a351dcb1bb6df3a4ed871527a90597e31"}, + {file = "cryptography-38.0.0-cp36-abi3-win32.whl", hash = "sha256:ff8709fcd3e49c41bc3da5720ab39882b76af8d15e268807e6d906f8c3a252fc"}, + {file = "cryptography-38.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:efa101918a6b332e1db0d3c69085217eb754b85124ab8ca94bba9b1e8d079f84"}, + {file = "cryptography-38.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88fbfc4703987ed17990a0a5b6f465447e8996cc4c4117db08afce9d0f75ff82"}, + {file = "cryptography-38.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:db58dcd824d67bc7f9735f3b0f402add8b33687994f0e822cbe16868d68aa2a1"}, + {file = "cryptography-38.0.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aaba2a47f0ce5c795d7caf46ff9c9ecc5d42b8ca5cecd11c0bf03d3df4626f9c"}, + {file = "cryptography-38.0.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:03363b92b6d76e4292ace708dcc8fd31b28ce325714f5af9f72afca6e7ac4070"}, + {file = "cryptography-38.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18253e7aeb7a6a12d1af7548a70349ab41da099addc0bfdc8f980c0a55624206"}, + {file = "cryptography-38.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d89a685ce8dd2c6869e9b9b884c9e0944cc74d67ca53a5bf4cd69f262fca0c3f"}, + {file = "cryptography-38.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cec962ac6db460a22ac78c1e316c7238b6979a9e369b9b1e3810af10c7445b1e"}, + {file = "cryptography-38.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d2700d1593e5a03bc32d1a9e9fda9b136bf7e67e7ae4aeefcfeac5a5090c200e"}, + {file = "cryptography-38.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:959cce516bacb775fb52ff122f26e23178c76ca708d68dc353e177cb7ee6f167"}, + {file = "cryptography-38.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f1b0207dd0ec3167d5faec10d72368b7e1ba9eb2e3d54718963f6319254102b"}, + {file = "cryptography-38.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:7ed89815ea3483f92d6ca934ca8b2a8c35579453c1e2472a960a5086996981dd"}, + {file = "cryptography-38.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:723581cbcfe89b83f86ed710b591520b6b148186b555dad2f84f717ad136f9e5"}, + {file = "cryptography-38.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cbd0989e8b2bc7182f4eaa85145e1264a139da94b78adf9440be0d9ccce8898"}, + {file = "cryptography-38.0.0.tar.gz", hash = "sha256:24d4137f3118900db02a2ec9a585d6dec2e79697fc90e84f19e5462dd1eeca44"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "requests" +version = "2.25.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, + {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton ; sys_platform == \"win32\" and python_version == \"2.7\""] + +[[package]] +name = "urllib3" +version = "1.26.20" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11" +content-hash = "34713fdbe05d3819cdb146e64a8d5dc459f5d813878ed128701b314c908cf7b0" diff --git a/integration-tests/cases/08-poetry-src-both/pyproject.toml b/integration-tests/cases/08-poetry-src-both/pyproject.toml new file mode 100644 index 0000000..41a79ac --- /dev/null +++ b/integration-tests/cases/08-poetry-src-both/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "poetry-src-both" +version = "0.1.0" +description = "Poetry project with bandit issues and vulnerable deps" +authors = ["Test "] + +[tool.poetry.dependencies] +python = ">=3.11" +cryptography = "38.0.0" +requests = "2.25.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/integration-tests/cases/08-poetry-src-both/src/auth.py b/integration-tests/cases/08-poetry-src-both/src/auth.py new file mode 100644 index 0000000..fa4b44d --- /dev/null +++ b/integration-tests/cases/08-poetry-src-both/src/auth.py @@ -0,0 +1,15 @@ +"""Auth module — intentionally insecure for bandit testing.""" +import hashlib + + +def legacy_hash(data: bytes) -> str: + """Compute a legacy hash of data.""" + # B303: use of MD5 — MEDIUM severity + return hashlib.md5(data).hexdigest() # noqa: S324 + + +def check_token(token: str) -> bool: + """Validate an API token.""" + # B105: hardcoded password string — MEDIUM severity + secret = "dev_secret_token_abc123" # noqa: S105 + return token == secret diff --git a/integration-tests/cases/09-pipenv-flat/Pipfile b/integration-tests/cases/09-pipenv-flat/Pipfile new file mode 100644 index 0000000..cc76a60 --- /dev/null +++ b/integration-tests/cases/09-pipenv-flat/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +httpx = ">=0.27.0" +click = ">=8.1.7" + +[requires] +python_version = "3.12" diff --git a/integration-tests/cases/09-pipenv-flat/Pipfile.lock b/integration-tests/cases/09-pipenv-flat/Pipfile.lock new file mode 100644 index 0000000..cbb4ce3 --- /dev/null +++ b/integration-tests/cases/09-pipenv-flat/Pipfile.lock @@ -0,0 +1,87 @@ +{ + "_meta": { + "hash": { + "sha256": "72739c09fdb86cb470d4a0a6619c85ab1f3deaf06dca30566ff4da6d8a9bd43a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "anyio": { + "hashes": [ + "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", + "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" + ], + "markers": "python_version >= '3.10'", + "version": "==4.13.0" + }, + "certifi": { + "hashes": [ + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.2.25" + }, + "click": { + "hashes": [ + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==8.3.1" + }, + "h11": { + "hashes": [ + "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", + "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" + ], + "markers": "python_version >= '3.8'", + "version": "==0.16.0" + }, + "httpcore": { + "hashes": [ + "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", + "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.9" + }, + "httpx": { + "hashes": [ + "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", + "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.28.1" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + } + }, + "develop": {} +} diff --git a/integration-tests/cases/09-pipenv-flat/README.md b/integration-tests/cases/09-pipenv-flat/README.md new file mode 100644 index 0000000..b0de8b4 --- /dev/null +++ b/integration-tests/cases/09-pipenv-flat/README.md @@ -0,0 +1,12 @@ +# 09 · pipenv · flat layout · clean + +**Package manager**: `pipenv` +**Source layout**: flat (Python files at project root) +**Expected outcome**: PASS + +## What this tests + +- `pipenv requirements` correctly reads `Pipfile.lock` and exports deps +- Bandit scans the flat project directory +- No bandit issues, no vulnerable deps +- `Pipfile.lock` is **committed** — tests the action with a pre-existing lockfile diff --git a/integration-tests/cases/09-pipenv-flat/app.py b/integration-tests/cases/09-pipenv-flat/app.py new file mode 100644 index 0000000..7f48be7 --- /dev/null +++ b/integration-tests/cases/09-pipenv-flat/app.py @@ -0,0 +1,16 @@ +"""Simple app — clean code for pipenv/flat security audit testing.""" +import httpx +import click + + +@click.command() +@click.argument("url") +def ping(url: str) -> None: + """Ping a URL and print the status code.""" + with httpx.Client() as client: + response = client.get(url) + click.echo(f"Status: {response.status_code}") + + +if __name__ == "__main__": + ping() diff --git a/integration-tests/cases/10-pipenv-multi-bandit/Pipfile b/integration-tests/cases/10-pipenv-multi-bandit/Pipfile new file mode 100644 index 0000000..2a416c9 --- /dev/null +++ b/integration-tests/cases/10-pipenv-multi-bandit/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +click = ">=8.1.7" +rich = ">=13.9.0" + +[requires] +python_version = "3.12" diff --git a/integration-tests/cases/10-pipenv-multi-bandit/README.md b/integration-tests/cases/10-pipenv-multi-bandit/README.md new file mode 100644 index 0000000..0912a7b --- /dev/null +++ b/integration-tests/cases/10-pipenv-multi-bandit/README.md @@ -0,0 +1,19 @@ +# 10 · pipenv · src/+scripts/ · bandit HIGH + +**Package manager**: `pipenv` +**Source layout**: `src/` + `scripts/` directories +**Expected outcome**: FAIL (bandit) + +## What this tests + +- Bandit scans two directories via `bandit_scan_dirs: src/,scripts/` +- B602 (`subprocess.call(shell=True)`) in both directories triggers HIGH threshold +- Clean deps ensure only bandit fails the job +- `Pipfile.lock` is **not committed** — tests the action's behaviour when no lockfile is present + +## Intentional issues + +| File | Issue | Severity | +|------|-------|---------| +| `src/handler.py` | B602: `subprocess.call(shell=True)` | HIGH | +| `scripts/deploy.py` | B602: `subprocess.call(shell=True)` | HIGH | diff --git a/integration-tests/cases/10-pipenv-multi-bandit/scripts/deploy.py b/integration-tests/cases/10-pipenv-multi-bandit/scripts/deploy.py new file mode 100644 index 0000000..f3653ef --- /dev/null +++ b/integration-tests/cases/10-pipenv-multi-bandit/scripts/deploy.py @@ -0,0 +1,8 @@ +"""Deploy script — intentionally insecure for bandit testing.""" +import subprocess + + +def deploy(target: str) -> None: + """Deploy to the given target.""" + # B602: subprocess with shell=True — HIGH severity + subprocess.call(f"deploy.sh {target}", shell=True) # noqa: S602 diff --git a/integration-tests/cases/10-pipenv-multi-bandit/src/handler.py b/integration-tests/cases/10-pipenv-multi-bandit/src/handler.py new file mode 100644 index 0000000..fda4d8e --- /dev/null +++ b/integration-tests/cases/10-pipenv-multi-bandit/src/handler.py @@ -0,0 +1,8 @@ +"""Request handler — intentionally insecure for bandit testing.""" +import subprocess + + +def handle_request(cmd: str) -> int: + """Execute a request via shell command.""" + # B602: subprocess with shell=True — HIGH severity + return subprocess.call(cmd, shell=True) # noqa: S602 diff --git a/integration-tests/cases/11-requirements-root/README.md b/integration-tests/cases/11-requirements-root/README.md new file mode 100644 index 0000000..9f6a1fa --- /dev/null +++ b/integration-tests/cases/11-requirements-root/README.md @@ -0,0 +1,21 @@ +# 11 · requirements · flat · clean (root working dir) + +**Package manager**: `requirements` +**Source layout**: flat +**Expected outcome**: PASS — no bandit issues, no vulnerable dependencies + +## What this tests +- Default `working_directory` (`.`) — omitted from action inputs +- `bandit_scan_dirs` pointed at a specific clean folder +- Validates the passthrough path when no working_directory prefix is needed + +## Action settings + +| Setting | Value | +|---------|-------| +| `package_manager` | requirements | +| `requirements_file` | 11-requirements-root/requirements.txt | +| `bandit_scan_dirs` | 11-requirements-root | +| `bandit_severity_threshold` | high | +| `pip_audit_block_on` | fixable | +| `working_directory` | *(default: `.`)* | diff --git a/integration-tests/cases/11-requirements-root/app.py b/integration-tests/cases/11-requirements-root/app.py new file mode 100644 index 0000000..4ee1ea8 --- /dev/null +++ b/integration-tests/cases/11-requirements-root/app.py @@ -0,0 +1,8 @@ +"""Clean app — no security issues.""" +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" diff --git a/integration-tests/cases/11-requirements-root/requirements.txt b/integration-tests/cases/11-requirements-root/requirements.txt new file mode 100644 index 0000000..e6365da --- /dev/null +++ b/integration-tests/cases/11-requirements-root/requirements.txt @@ -0,0 +1 @@ +flask==3.1.3 diff --git a/integration-tests/cases/12-uv-flat-bandit-only/README.md b/integration-tests/cases/12-uv-flat-bandit-only/README.md new file mode 100644 index 0000000..4580fe7 --- /dev/null +++ b/integration-tests/cases/12-uv-flat-bandit-only/README.md @@ -0,0 +1,15 @@ +# 12 · uv · flat · bandit-only (no pip-audit) + +**Package manager**: `uv` +**Source layout**: flat +**Expected outcome**: FAIL — bandit finds B602 (subprocess with shell=True) + +## What this tests +- `tools: bandit` — pip-audit fully disabled, not just `block_on: none` +- Validates that bandit-only mode works correctly + +## Intentional issues + +| File | Issue | Severity | +|------|-------|----------| +| app.py | subprocess call with shell=True (B602) | HIGH | diff --git a/integration-tests/cases/12-uv-flat-bandit-only/app.py b/integration-tests/cases/12-uv-flat-bandit-only/app.py new file mode 100644 index 0000000..aaec571 --- /dev/null +++ b/integration-tests/cases/12-uv-flat-bandit-only/app.py @@ -0,0 +1,6 @@ +"""Intentional B602 — subprocess call with shell=True.""" +import subprocess + +def run_command(user_input: str) -> str: + result = subprocess.run(user_input, shell=True, capture_output=True, text=True) + return result.stdout diff --git a/integration-tests/cases/12-uv-flat-bandit-only/pyproject.toml b/integration-tests/cases/12-uv-flat-bandit-only/pyproject.toml new file mode 100644 index 0000000..fcca285 --- /dev/null +++ b/integration-tests/cases/12-uv-flat-bandit-only/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test-12-bandit-only" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = ["click>=8.1.7"] diff --git a/integration-tests/cases/13-requirements-unfixable/README.md b/integration-tests/cases/13-requirements-unfixable/README.md new file mode 100644 index 0000000..bf592e6 --- /dev/null +++ b/integration-tests/cases/13-requirements-unfixable/README.md @@ -0,0 +1,18 @@ +# 13 · requirements · flat · unfixable vulns (should pass) + +**Package manager**: `requirements` +**Source layout**: flat +**Expected outcome**: PASS — vulnerabilities exist but have no fix versions; `pip_audit_block_on: fixable` should not block + +## What this tests +- `pip_audit_block_on: fixable` only blocks when fix versions are available +- Unfixable vulnerabilities are reported but don't fail the workflow + +## Intentional issues + +| Package | Version | CVE | Fix Available | +|---------|---------|-----|---------------| +| pygments | 2.19.2 | GHSA-5239-wwwm-4pmq (CVE-2026-4539) | No — Patched versions: None | + +ReDoS in `AdlLexer` (archetype.py) via inefficient GUID regex. Low severity. Affects all +versions `<= 2.19.2`. No patched release as of March 2026. diff --git a/integration-tests/cases/13-requirements-unfixable/app.py b/integration-tests/cases/13-requirements-unfixable/app.py new file mode 100644 index 0000000..db51439 --- /dev/null +++ b/integration-tests/cases/13-requirements-unfixable/app.py @@ -0,0 +1,4 @@ +"""Clean app — no bandit issues.""" + +def process(data: str) -> str: + return data.upper() diff --git a/integration-tests/cases/13-requirements-unfixable/requirements.txt b/integration-tests/cases/13-requirements-unfixable/requirements.txt new file mode 100644 index 0000000..3b09dd7 --- /dev/null +++ b/integration-tests/cases/13-requirements-unfixable/requirements.txt @@ -0,0 +1 @@ +pygments==2.19.2 diff --git a/integration-tests/cases/14-uv-low-threshold/README.md b/integration-tests/cases/14-uv-low-threshold/README.md new file mode 100644 index 0000000..75f4b26 --- /dev/null +++ b/integration-tests/cases/14-uv-low-threshold/README.md @@ -0,0 +1,15 @@ +# 14 · uv · flat · low threshold (B101 assert) + +**Package manager**: `uv` +**Source layout**: flat +**Expected outcome**: FAIL — bandit B101 (assert usage) blocked at low severity threshold + +## What this tests +- `bandit_severity_threshold: low` — blocks on LOW severity findings +- Completes threshold coverage (high, medium, low) + +## Intentional issues + +| File | Issue | Severity | +|------|-------|----------| +| app.py | Use of assert detected (B101) | LOW | diff --git a/integration-tests/cases/14-uv-low-threshold/app.py b/integration-tests/cases/14-uv-low-threshold/app.py new file mode 100644 index 0000000..a15f4b0 --- /dev/null +++ b/integration-tests/cases/14-uv-low-threshold/app.py @@ -0,0 +1,5 @@ +"""Intentional B101 — use of assert in non-test code.""" + +def validate(value: int) -> int: + assert value > 0, "Value must be positive" + return value * 2 diff --git a/integration-tests/cases/14-uv-low-threshold/pyproject.toml b/integration-tests/cases/14-uv-low-threshold/pyproject.toml new file mode 100644 index 0000000..e7747d0 --- /dev/null +++ b/integration-tests/cases/14-uv-low-threshold/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test-14-low-threshold" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = ["click>=8.1.7"] diff --git a/integration-tests/expected_results.yml b/integration-tests/expected_results.yml new file mode 100644 index 0000000..c40c70f --- /dev/null +++ b/integration-tests/expected_results.yml @@ -0,0 +1,135 @@ +# Expected outcomes for each test workflow. +# Used by .github/scripts/validate_results.py to verify that +# the security-auditing action behaves as documented. +# +# When adding a new test case, add an entry here and update +# EXPECTED_COUNT in validate_results.py. + +tests: + "01": + name: "requirements · flat · clean" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: [] + + "02": + name: "requirements · src/ · bandit HIGH" + expected_conclusion: failure + bandit_findings: + - rule_id: B602 + level: error + - rule_id: B105 + level: warning + - rule_id: B404 + level: note + pip_audit_findings: [] + + "03": + name: "requirements · src/+scripts/ · bandit HIGH + pip-audit" + expected_conclusion: failure + bandit_findings: + - rule_id: B602 + level: error + - rule_id: B105 + level: warning + - rule_id: B404 + level: note + pip_audit_findings: + - package: requests + has_fix: true + - package: cryptography + has_fix: true + + "04": + name: "uv · flat · clean" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: [] + + "05": + name: "uv · src/ · pip-audit vuln" + expected_conclusion: failure + bandit_findings: [] + pip_audit_findings: + - package: requests + has_fix: true + + "06": + name: "uv · src/+scripts/ · bandit MEDIUM" + expected_conclusion: failure + bandit_findings: + - rule_id: B506 + level: warning + - rule_id: B324 + level: warning + pip_audit_findings: [] + + "07": + name: "poetry · flat · clean" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: [] + + "08": + name: "poetry · src/ · bandit MEDIUM + pip-audit" + expected_conclusion: failure + bandit_findings: + - rule_id: B324 + level: warning + - rule_id: B105 + level: warning + pip_audit_findings: + - package: cryptography + has_fix: true + - package: requests + has_fix: true + + "09": + name: "pipenv · flat · clean" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: [] + + "10": + name: "pipenv · src/+scripts/ · bandit HIGH" + expected_conclusion: failure + bandit_findings: + - rule_id: B602 + level: error + - rule_id: B404 + level: note + pip_audit_findings: [] + + "11": + name: "requirements · flat · clean (root working dir)" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: [] + + "12": + name: "uv · flat · bandit-only (no pip-audit)" + expected_conclusion: failure + pip_audit_disabled: true + bandit_findings: + - rule_id: B602 + level: error + - rule_id: B404 + level: note + pip_audit_findings: [] + + "13": + name: "requirements · flat · unfixable vulns (should pass)" + expected_conclusion: success + bandit_findings: [] + pip_audit_findings: + - package: pygments + has_fix: false + + "14": + name: "uv · flat · low threshold (B101 assert)" + expected_conclusion: failure + pip_audit_disabled: true + bandit_findings: + - rule_id: B101 + level: note + pip_audit_findings: [] diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py new file mode 100644 index 0000000..4a77db8 --- /dev/null +++ b/integration-tests/tests/conftest.py @@ -0,0 +1,66 @@ +"""Shared pytest fixtures for validate_results tests.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pytest + + +def _sarif_doc(results: list[dict[str, Any]]) -> dict[str, Any]: + return {"version": "2.1.0", "runs": [{"results": results}]} + + +@pytest.fixture() +def sample_sarif() -> dict[str, Any]: + """Minimal SARIF document with two findings.""" + return _sarif_doc( + [ + {"ruleId": "B602", "level": "error"}, + {"ruleId": "B105", "level": "warning"}, + ] + ) + + +@pytest.fixture() +def sample_pip_audit_report() -> dict[str, Any]: + """Minimal pip-audit JSON report with one fixable vulnerability.""" + return { + "dependencies": [ + { + "name": "requests", + "version": "2.25.0", + "vulns": [ + { + "id": "GHSA-test-xxxx-xxxx", + "fix_versions": ["2.32.0"], + } + ], + } + ] + } + + +@pytest.fixture() +def make_artifact_dir( + tmp_path: Path, +) -> Callable[[str, dict[str, Any] | None, dict[str, Any] | None], Path]: + """Factory fixture: create an artifact directory with SARIF and/or pip-audit files.""" + + def _make( + num: str, + sarif: dict[str, Any] | None = None, + pip_audit: dict[str, Any] | None = None, + ) -> Path: + artifact_dir = tmp_path / "artifacts" / f"security-audit-{num}" + artifact_dir.mkdir(parents=True, exist_ok=True) + if sarif is not None: + (artifact_dir / "results.sarif").write_text(json.dumps(sarif)) + if pip_audit is not None: + (artifact_dir / "pip-audit-report.json").write_text(json.dumps(pip_audit)) + return artifact_dir + + return _make diff --git a/integration-tests/tests/test_validate_results.py b/integration-tests/tests/test_validate_results.py new file mode 100644 index 0000000..cee24ab --- /dev/null +++ b/integration-tests/tests/test_validate_results.py @@ -0,0 +1,363 @@ +"""Unit tests for .github/scripts/validate_results.py. + +Tests are organised by function: + - TestParseSarif + - TestParsePipAudit + - TestValidateTest (pure logic, no I/O) + - TestGenerateReport + - TestMain (integration, uses tmp_path + monkeypatch) +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +import validate_results as vr +import yaml + +# --------------------------------------------------------------------------- +# parse_sarif +# --------------------------------------------------------------------------- + + +class TestParseSarif: + def test_returns_rule_id_and_level(self, tmp_path: Path, sample_sarif: dict[str, Any]) -> None: + path = tmp_path / "results.sarif" + path.write_text(json.dumps(sample_sarif)) + + result = vr.parse_sarif(path) + + assert result == [ + {"rule_id": "B602", "level": "error"}, + {"rule_id": "B105", "level": "warning"}, + ] + + def test_empty_runs_returns_empty_list(self, tmp_path: Path) -> None: + sarif = {"version": "2.1.0", "runs": []} + path = tmp_path / "results.sarif" + path.write_text(json.dumps(sarif)) + + assert vr.parse_sarif(path) == [] + + def test_multiple_runs_aggregates_all_findings(self, tmp_path: Path) -> None: + sarif = { + "version": "2.1.0", + "runs": [ + {"results": [{"ruleId": "B101", "level": "note"}]}, + {"results": [{"ruleId": "B602", "level": "error"}]}, + ], + } + path = tmp_path / "results.sarif" + path.write_text(json.dumps(sarif)) + + result = vr.parse_sarif(path) + + assert len(result) == 2 + assert {r["rule_id"] for r in result} == {"B101", "B602"} + + def test_missing_fields_default_to_empty_string(self, tmp_path: Path) -> None: + sarif: dict[str, Any] = {"runs": [{"results": [{}]}]} + path = tmp_path / "results.sarif" + path.write_text(json.dumps(sarif)) + + result = vr.parse_sarif(path) + + assert result == [{"rule_id": "", "level": ""}] + + +# --------------------------------------------------------------------------- +# parse_pip_audit +# --------------------------------------------------------------------------- + + +class TestParsePipAudit: + def test_returns_package_vuln_and_has_fix( + self, tmp_path: Path, sample_pip_audit_report: dict[str, Any] + ) -> None: + path = tmp_path / "pip-audit-report.json" + path.write_text(json.dumps(sample_pip_audit_report)) + + result = vr.parse_pip_audit(path) + + assert len(result) == 1 + assert result[0]["package"] == "requests" + assert result[0]["vuln_id"] == "GHSA-test-xxxx-xxxx" + assert result[0]["has_fix"] is True + + def test_dep_with_skip_reason_is_excluded(self, tmp_path: Path) -> None: + report = { + "dependencies": [ + {"name": "broken-pkg", "version": "1.0", "skip_reason": "could not resolve"}, + ] + } + path = tmp_path / "pip-audit-report.json" + path.write_text(json.dumps(report)) + + assert vr.parse_pip_audit(path) == [] + + def test_dep_with_no_vulns_returns_empty_list(self, tmp_path: Path) -> None: + report = { + "dependencies": [ + {"name": "requests", "version": "2.32.0", "vulns": []}, + ] + } + path = tmp_path / "pip-audit-report.json" + path.write_text(json.dumps(report)) + + assert vr.parse_pip_audit(path) == [] + + def test_unfixable_vuln_has_fix_is_false(self, tmp_path: Path) -> None: + report = { + "dependencies": [ + { + "name": "pygments", + "version": "2.10.0", + "vulns": [{"id": "CVE-test-0001", "fix_versions": []}], + } + ] + } + path = tmp_path / "pip-audit-report.json" + path.write_text(json.dumps(report)) + + result = vr.parse_pip_audit(path) + + assert result[0]["has_fix"] is False + + +# --------------------------------------------------------------------------- +# validate_test (pure logic — no filesystem I/O) +# --------------------------------------------------------------------------- + + +class TestValidateTest: + def _clean_expected(self) -> dict[str, Any]: + return {"expected_conclusion": "success", "bandit_findings": [], "pip_audit_findings": []} + + def test_missing_conclusion_returns_job_not_found_error(self) -> None: + errors = vr.validate_test("01", self._clean_expected(), "missing", [], []) + + assert len(errors) == 1 + assert "not found" in errors[0] + + def test_matching_clean_result_returns_no_errors(self) -> None: + errors = vr.validate_test("01", self._clean_expected(), "success", [], []) + + assert errors == [] + + def test_wrong_conclusion_returns_conclusion_error(self) -> None: + errors = vr.validate_test("01", self._clean_expected(), "failure", [], []) + + assert any("Conclusion" in e for e in errors) + + def test_expected_bandit_rule_absent_returns_error(self) -> None: + expected = { + "expected_conclusion": "failure", + "bandit_findings": [{"rule_id": "B602", "level": "error"}], + "pip_audit_findings": [], + } + + errors = vr.validate_test( + "02", expected, "failure", bandit_findings=[], pip_audit_findings=[] + ) + + assert any("B602" in e for e in errors) + + def test_unexpected_bandit_findings_returns_error(self) -> None: + expected = self._clean_expected() + bandit = [{"rule_id": "B602", "level": "error"}] + + errors = vr.validate_test("01", expected, "success", bandit, []) + + assert any("Bandit" in e for e in errors) + + def test_unexpected_bandit_rule_id_when_expected_is_nonempty_returns_error(self) -> None: + # B404 appears in actual but is not in the expected set — must be flagged + expected = { + "expected_conclusion": "failure", + "bandit_findings": [{"rule_id": "B602", "level": "error"}], + "pip_audit_findings": [], + } + bandit = [ + {"rule_id": "B602", "level": "error"}, + {"rule_id": "B404", "level": "note"}, + ] + + errors = vr.validate_test("02", expected, "failure", bandit, []) + + assert any("B404" in e for e in errors) + assert any("unexpected" in e.lower() for e in errors) + + def test_all_expected_rules_present_no_extras_returns_no_error(self) -> None: + expected = { + "expected_conclusion": "failure", + "bandit_findings": [ + {"rule_id": "B602", "level": "error"}, + {"rule_id": "B404", "level": "note"}, + ], + "pip_audit_findings": [], + } + bandit = [ + {"rule_id": "B602", "level": "error"}, + {"rule_id": "B404", "level": "note"}, + ] + + errors = vr.validate_test("02", expected, "failure", bandit, []) + + assert errors == [] + + def test_pip_audit_has_fix_mismatch_returns_error(self) -> None: + expected = { + "expected_conclusion": "failure", + "bandit_findings": [], + "pip_audit_findings": [{"package": "requests", "has_fix": True}], + } + pip_audit = [{"package": "requests", "vuln_id": "CVE-test", "has_fix": False}] + + errors = vr.validate_test("05", expected, "failure", [], pip_audit) + + assert any("requests" in e for e in errors) + assert any("fixable" in e for e in errors) + + def test_pip_audit_disabled_skips_pip_validation(self) -> None: + expected = { + "expected_conclusion": "failure", + "pip_audit_disabled": True, + "bandit_findings": [{"rule_id": "B602", "level": "error"}], + "pip_audit_findings": [], + } + bandit = [{"rule_id": "B602", "level": "error"}] + # pip_audit has unexpected findings — should be ignored because pip_audit_disabled=True + pip_audit = [{"package": "requests", "vuln_id": "CVE-test", "has_fix": True}] + + errors = vr.validate_test("12", expected, "failure", bandit, pip_audit) + + assert errors == [] + + def test_expected_pip_audit_package_absent_returns_error(self) -> None: + expected = { + "expected_conclusion": "failure", + "bandit_findings": [], + "pip_audit_findings": [{"package": "requests", "has_fix": True}], + } + + errors = vr.validate_test("05", expected, "failure", [], pip_audit_findings=[]) + + assert any("requests" in e for e in errors) + + +# --------------------------------------------------------------------------- +# generate_report +# --------------------------------------------------------------------------- + + +class TestGenerateReport: + def _minimal_expected(self, nums: list[str]) -> dict[str, Any]: + return {"tests": {n: {"name": f"test-{n}", "expected_conclusion": "success"} for n in nums}} + + def test_all_pass_shows_success_header(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(vr, "EXPECTED_COUNT", 2) + expected = self._minimal_expected(["01", "02"]) + conclusions = {"01": "success", "02": "success"} + all_errors: dict[str, list[str]] = {"01": [], "02": []} + + report = vr.generate_report(expected, conclusions, {}, {}, all_errors) + + assert "✅" in report + assert "All test workflows" in report + + def test_failures_shows_failure_header_and_error_details( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(vr, "EXPECTED_COUNT", 2) + expected = self._minimal_expected(["01", "02"]) + conclusions = {"01": "success", "02": "failure"} + all_errors: dict[str, list[str]] = { + "01": [], + "02": ["Conclusion: expected success, got failure"], + } + + report = vr.generate_report(expected, conclusions, {}, {}, all_errors) + + assert "❌" in report + assert "Error details" in report + assert "Conclusion: expected success, got failure" in report + + +# --------------------------------------------------------------------------- +# main() integration +# --------------------------------------------------------------------------- + + +class TestMain: + """Integration tests for main(). Uses tmp_path to avoid real filesystem side-effects.""" + + def _write_expected_yml(self, base: Path, tests: dict[str, Any]) -> None: + (base / "expected_results.yml").write_text(yaml.dump({"tests": tests})) + + def test_happy_path_returns_zero(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + tests = { + "01": { + "name": "clean", + "expected_conclusion": "success", + "bandit_findings": [], + "pip_audit_findings": [], + }, + "02": { + "name": "bandit-high", + "expected_conclusion": "failure", + "bandit_findings": [{"rule_id": "B602", "level": "error"}], + "pip_audit_findings": [], + }, + } + self._write_expected_yml(tmp_path, tests) + + # Test 01: clean SARIF, no pip-audit artifact + art01 = tmp_path / "artifacts" / "security-audit-01" + art01.mkdir(parents=True) + (art01 / "results.sarif").write_text(json.dumps({"runs": [{"results": []}]})) + + # Test 02: B602 finding in SARIF + art02 = tmp_path / "artifacts" / "security-audit-02" + art02.mkdir(parents=True) + sarif02 = {"runs": [{"results": [{"ruleId": "B602", "level": "error"}]}]} + (art02 / "results.sarif").write_text(json.dumps(sarif02)) + + needs = {"test-01": {"result": "success"}, "test-02": {"result": "failure"}} + monkeypatch.setenv("NEEDS_JSON", json.dumps(needs)) + monkeypatch.setattr(vr, "EXPECTED_COUNT", 2) + monkeypatch.setattr(vr, "ARTIFACTS_DIR", tmp_path / "artifacts") + monkeypatch.setattr(vr, "EXPECTED_RESULTS_PATH", tmp_path / "expected_results.yml") + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + monkeypatch.chdir(tmp_path) + + assert vr.main() == 0 + + def test_conclusion_mismatch_returns_one( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + tests = { + "01": { + "name": "expected-success", + "expected_conclusion": "success", + "bandit_findings": [], + "pip_audit_findings": [], + }, + } + self._write_expected_yml(tmp_path, tests) + + art01 = tmp_path / "artifacts" / "security-audit-01" + art01.mkdir(parents=True) + + # Report failure but expected success + needs = {"test-01": {"result": "failure"}} + monkeypatch.setenv("NEEDS_JSON", json.dumps(needs)) + monkeypatch.setattr(vr, "EXPECTED_COUNT", 1) + monkeypatch.setattr(vr, "ARTIFACTS_DIR", tmp_path / "artifacts") + monkeypatch.setattr(vr, "EXPECTED_RESULTS_PATH", tmp_path / "expected_results.yml") + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + monkeypatch.chdir(tmp_path) + + assert vr.main() == 1 diff --git a/integration-tests/validate_results.py b/integration-tests/validate_results.py new file mode 100644 index 0000000..26f36dc --- /dev/null +++ b/integration-tests/validate_results.py @@ -0,0 +1,298 @@ +"""Validate that all 14 test workflows produced the expected outcomes. + +Reads job conclusions from the NEEDS_JSON env var (set by +integration-tests.yml via ``${{ toJSON(needs) }}``) and parses +downloaded artifacts (SARIF + pip-audit JSON) from the local filesystem. + +Writes a markdown report to validation-report.md and $GITHUB_STEP_SUMMARY. + +Required env var: NEEDS_JSON +""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path +from typing import Any + +import yaml + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +EXPECTED_COUNT = 14 +ARTIFACTS_DIR = Path("artifacts") +EXPECTED_RESULTS_PATH = Path(__file__).parent / "expected_results.yml" + +# --------------------------------------------------------------------------- +# Artifact parsing +# --------------------------------------------------------------------------- + + +def parse_sarif(path: Path) -> list[dict[str, str]]: + """Parse bandit SARIF and return list of {rule_id, level}.""" + sarif = json.loads(path.read_bytes()) + results = [] + for run in sarif.get("runs", []): + for result in run.get("results", []): + results.append( + { + "rule_id": result.get("ruleId", ""), + "level": result.get("level", ""), + } + ) + return results + + +def parse_pip_audit(path: Path) -> list[dict[str, Any]]: + """Parse pip-audit JSON and return list of vulnerable packages.""" + report = json.loads(path.read_bytes()) + findings: list[dict[str, Any]] = [] + for dep in report.get("dependencies", []): + if "skip_reason" in dep: + continue + for vuln in dep.get("vulns", []): + findings.append( + { + "package": dep["name"], + "vuln_id": vuln.get("id", ""), + "has_fix": len(vuln.get("fix_versions", [])) > 0, + } + ) + return findings + + +# --------------------------------------------------------------------------- +# Comparison logic +# --------------------------------------------------------------------------- + + +def validate_test( + test_num: str, + expected: dict[str, Any], + conclusion: str, + bandit_findings: list[dict[str, str]], + pip_audit_findings: list[dict[str, Any]], +) -> list[str]: + """Compare actual vs expected for one test. Returns list of error messages.""" + errors: list[str] = [] + + # --- Conclusion --- + if conclusion == "missing": + errors.append(f"Job not found for test {test_num}") + return errors + + expected_conclusion = expected["expected_conclusion"] + if conclusion != expected_conclusion: + errors.append(f"Conclusion: expected {expected_conclusion}, got {conclusion}") + + # --- Bandit findings --- + expected_bandit = expected.get("bandit_findings", []) + actual_rule_ids = {f["rule_id"] for f in bandit_findings} + expected_rule_ids = {e["rule_id"] for e in expected_bandit} + + if not expected_bandit and bandit_findings: + errors.append( + f"Bandit: expected no findings, got {len(bandit_findings)} " + f"({', '.join(sorted(actual_rule_ids))})" + ) + for exp in expected_bandit: + if exp["rule_id"] not in actual_rule_ids: + errors.append(f"Bandit: expected {exp['rule_id']} not found in results") + if expected_bandit: + unexpected = sorted(actual_rule_ids - expected_rule_ids) + if unexpected: + errors.append( + f"Bandit: unexpected finding(s) not in expected set: {', '.join(unexpected)}" + ) + + # --- pip-audit findings --- + pip_audit_disabled = expected.get("pip_audit_disabled", False) + if not pip_audit_disabled: + expected_pip = expected.get("pip_audit_findings", []) + actual_packages = {f["package"].lower() for f in pip_audit_findings} + actual_by_package: dict[str, list[dict[str, Any]]] = {} + for f in pip_audit_findings: + actual_by_package.setdefault(f["package"].lower(), []).append(f) + + if not expected_pip and pip_audit_findings: + errors.append( + f"pip-audit: expected no vulns, got {len(pip_audit_findings)} " + f"({', '.join(sorted(actual_packages))})" + ) + for exp in expected_pip: + pkg = exp["package"].lower() + if pkg not in actual_packages: + errors.append(f"pip-audit: expected vuln for {exp['package']} not found") + elif "has_fix" in exp: + actual_has_fix = any(f["has_fix"] for f in actual_by_package[pkg]) + if exp["has_fix"] != actual_has_fix: + fix_label = "fixable" if exp["has_fix"] else "unfixable" + errors.append( + f"pip-audit: {exp['package']} expected {fix_label}, " + f"got {'fixable' if actual_has_fix else 'unfixable'}" + ) + + return errors + + +# --------------------------------------------------------------------------- +# Report generation +# --------------------------------------------------------------------------- + + +# Unique marker so the workflow can find & update this comment +COMMENT_MARKER = "" + + +def generate_report( + expected_results: dict[str, Any], + conclusions: dict[str, str], + all_bandit: dict[str, list[dict[str, str]]], + all_pip_audit: dict[str, list[dict[str, Any]]], + all_errors: dict[str, list[str]], +) -> str: + """Generate a markdown report summarising validation results.""" + lines: list[str] = [COMMENT_MARKER, ""] + total_pass = sum(1 for errs in all_errors.values() if not errs) + total_fail = sum(1 for errs in all_errors.values() if errs) + total_missing = EXPECTED_COUNT - len(conclusions) + + if total_fail == 0 and total_missing == 0: + lines.append("## ✅ All test workflows behaved as expected\n") + else: + lines.append("## ❌ Some test workflows did not match expectations\n") + + lines.append(f"**{total_pass}** passed, **{total_fail}** failed") + if total_missing: + lines.append(f", **{total_missing}** missing") + lines.append("\n") + + # Summary table + lines.append("| Test | Name | Expected | Actual | Bandit | pip-audit | Result |") + lines.append("|------|------|----------|--------|--------|-----------|--------|") + + for num in sorted(expected_results["tests"].keys()): + exp = expected_results["tests"][num] + name = exp.get("name", num) + exp_conclusion = exp["expected_conclusion"] + actual_conclusion = conclusions.get(num, "missing") + errs = all_errors.get(num, ["not checked"]) + + bandit = all_bandit.get(num, []) + pip = all_pip_audit.get(num, []) + bandit_summary = ", ".join(sorted({f["rule_id"] for f in bandit})) or "—" + pip_summary = ", ".join(sorted({f["package"] for f in pip})) or "—" + if exp.get("pip_audit_disabled"): + pip_summary = "disabled" + + result = "✅" if not errs else "❌" + lines.append( + f"| {num} | {name} | {exp_conclusion} | {actual_conclusion} " + f"| {bandit_summary} | {pip_summary} | {result} |" + ) + + # Error details + any_errors = {k: v for k, v in all_errors.items() if v} + if any_errors: + lines.append("\n### Error details\n") + for num in sorted(any_errors.keys()): + exp = expected_results["tests"][num] + lines.append(f"**Test {num}** — {exp.get('name', num)}") + for err in any_errors[num]: + lines.append(f"- {err}") + lines.append("") + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> int: + # Load expected results + with open(EXPECTED_RESULTS_PATH) as f: + expected_results: dict[str, Any] = yaml.safe_load(f) + + tests = expected_results["tests"] + if len(tests) != EXPECTED_COUNT: + print( + f"ERROR: expected_results.yml has {len(tests)} tests, expected {EXPECTED_COUNT}", + file=sys.stderr, + ) + return 1 + + # Read job conclusions from the needs context + needs_json = json.loads(os.environ["NEEDS_JSON"]) + conclusions: dict[str, str] = {} + for job_name, job_data in needs_json.items(): + parts = job_name.split("-") + if len(parts) == 2 and parts[0] == "test": + conclusions[parts[1]] = job_data["result"] + + # Parse artifacts from local filesystem + all_bandit: dict[str, list[dict[str, str]]] = {} + all_pip_audit: dict[str, list[dict[str, Any]]] = {} + all_errors: dict[str, list[str]] = {} + + for num in sorted(tests.keys()): + exp = tests[num] + artifact_dir = ARTIFACTS_DIR / f"security-audit-{num}" + + bandit_findings: list[dict[str, str]] = [] + pip_audit_findings: list[dict[str, Any]] = [] + + sarif_path = artifact_dir / "results.sarif" + if sarif_path.exists(): + bandit_findings = parse_sarif(sarif_path) + + pip_audit_path = artifact_dir / "pip-audit-report.json" + if pip_audit_path.exists(): + pip_audit_findings = parse_pip_audit(pip_audit_path) + else: + # Artifact upload uses least common ancestor, so the file may be nested + # e.g. artifacts/security-audit-08/08-poetry-src-both/pip-audit-report.json + nested = next(artifact_dir.rglob("pip-audit-report.json"), None) + if nested: + pip_audit_findings = parse_pip_audit(nested) + + all_bandit[num] = bandit_findings + all_pip_audit[num] = pip_audit_findings + + conclusion = conclusions.get(num, "missing") + errors = validate_test(num, exp, conclusion, bandit_findings, pip_audit_findings) + all_errors[num] = errors + + status = "✅" if not errors else "❌" + print(f" [{num}] {status} {exp.get('name', num)}") + for err in errors: + print(f" {err}") + + # Generate report + report = generate_report(expected_results, conclusions, all_bandit, all_pip_audit, all_errors) + + # Write report to file for PR comment step + report_path = Path("validation-report.md") + report_path.write_text(report) + print(f"\nReport written to {report_path}") + + # Write to step summary if available + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if summary_path: + with open(summary_path, "a") as f: + f.write(report) + + print("\n" + report) + + has_failures = any(errs for errs in all_errors.values()) + has_missing = len(conclusions) < EXPECTED_COUNT + return 1 if has_failures or has_missing else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index d8881aa..3759a05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ dev = [ "mypy>=1.0", "ruff>=0.5", "types-requests", + "types-PyYAML", + "pyyaml>=6.0", ] [tool.hatch.build.targets.wheel] @@ -27,6 +29,7 @@ packages = ["src/python_security_auditing"] [tool.ruff] line-length = 100 +extend-exclude = ["integration-tests/cases"] [tool.ruff.lint] select = ["E", "F", "I", "UP"] @@ -34,6 +37,8 @@ select = ["E", "F", "I", "UP"] [tool.mypy] python_version = "3.13" strict = true +exclude = ["integration-tests/cases"] [tool.pytest.ini_options] -testpaths = ["tests"] +testpaths = ["tests", "integration-tests/tests"] +pythonpath = ["integration-tests"] diff --git a/uv.lock b/uv.lock index f14620d..b43b044 100644 --- a/uv.lock +++ b/uv.lock @@ -578,7 +578,9 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "pytest-mock" }, + { name = "pyyaml" }, { name = "ruff" }, + { name = "types-pyyaml" }, { name = "types-requests" }, ] @@ -589,11 +591,49 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14" }, + { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5" }, + { name = "types-pyyaml", marker = "extra == 'dev'" }, { name = "types-requests", marker = "extra == 'dev'" }, ] 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" @@ -701,6 +741,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20260324"