Skip to content

Commit a6b16ea

Browse files
authored
chore: bump python version (#4)
* chore: bump python version * fix: security
1 parent b33f42f commit a6b16ea

11 files changed

Lines changed: 904 additions & 63 deletions

File tree

.github/dependabot.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ updates:
66
interval: weekly
77
commit-message:
88
prefix: "ci"
9+
cooldown:
10+
default-days: 7
911

1012
- package-ecosystem: pip
1113
directory: /
1214
schedule:
1315
interval: weekly
1416
commit-message:
1517
prefix: "deps"
18+
cooldown:
19+
default-days: 7

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ on:
55
branches: [main]
66
pull_request:
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
pre-commit:
1013
runs-on: ubuntu-latest
1114
steps:
1215
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
16+
with:
17+
persist-credentials: false
1318
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
1419
with:
1520
python-version: '3.13'
@@ -19,6 +24,8 @@ jobs:
1924
runs-on: ubuntu-latest
2025
steps:
2126
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27+
with:
28+
persist-credentials: false
2229
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
2330
with:
2431
python-version: '3.13'
@@ -31,6 +38,8 @@ jobs:
3138
runs-on: ubuntu-latest
3239
steps:
3340
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41+
with:
42+
persist-credentials: false
3443
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
3544
with:
3645
python-version: '3.13'

.github/workflows/release-please.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ jobs:
2020
# Move major version tag (e.g. v1) after a release is cut
2121
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2222
if: ${{ steps.release.outputs.release_created }}
23+
with:
24+
persist-credentials: false
2325
- name: Tag major version
2426
if: ${{ steps.release.outputs.release_created }}
27+
env:
28+
GITHUB_TOKEN: ${{ github.token }}
2529
run: |
2630
git config user.name "github-actions[bot]"
2731
git config user.email "github-actions[bot]@users.noreply.github.com"
32+
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
2833
git tag -fa "v${{ steps.release.outputs.major }}" \
2934
-m "Release v${{ steps.release.outputs.tag_name }}"
3035
git push origin "v${{ steps.release.outputs.major }}" --force

.pre-commit-config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ repos:
1313
additional_dependencies:
1414
- pydantic-settings>=2.0
1515
- bandit>=1.8
16+
- pytest>=8.0
1617

1718
- repo: https://github.com/zizmorcore/zizmor-pre-commit
1819
rev: v1.23.1
@@ -24,4 +25,5 @@ repos:
2425
rev: '1.9.4'
2526
hooks:
2627
- id: bandit
27-
args: [-r, src/]
28+
args: [-r, src/, --skip, "B404,B603,B607"]
29+
pass_filenames: false

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "python-security-auditing"
77
version = "0.1.0"
88
description = "Reusable GitHub Action for Python security auditing with bandit and pip-audit"
99
license = { text = "MIT" }
10-
requires-python = ">=3.11"
10+
requires-python = ">=3.13"
1111
dependencies = [
1212
"pydantic-settings>=2.0",
1313
"bandit>=1.8",
@@ -33,7 +33,7 @@ line-length = 100
3333
select = ["E", "F", "I", "UP"]
3434

3535
[tool.mypy]
36-
python_version = "3.11"
36+
python_version = "3.13"
3737
strict = true
3838

3939
[tool.pytest.ini_options]

src/python_security_auditing/report.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,7 @@ def _bandit_section(report: dict[str, Any], settings: Settings) -> str:
4949
lines.append("✅ No issues found.\n")
5050
return "\n".join(lines)
5151

52-
lines.append(
53-
"| Severity | Confidence | File | Line | Issue |\n"
54-
"|---|---|---|---|---|\n"
55-
)
52+
lines.append("| Severity | Confidence | File | Line | Issue |\n" "|---|---|---|---|---|\n")
5653
for r in results:
5754
sev = r.get("issue_severity", "")
5855
conf = r.get("issue_confidence", "")
@@ -82,8 +79,7 @@ def _pip_audit_section(report: list[dict[str, Any]], settings: Settings) -> str:
8279
return "\n".join(lines)
8380

8481
lines.append(
85-
"| Package | Version | ID | Fix Versions | Description |\n"
86-
"|---|---|---|---|---|\n"
82+
"| Package | Version | ID | Fix Versions | Description |\n" "|---|---|---|---|---|\n"
8783
)
8884
for pkg in vulnerable:
8985
name = pkg.get("name", "")
@@ -95,12 +91,7 @@ def _pip_audit_section(report: list[dict[str, Any]], settings: Settings) -> str:
9591
lines.append(f"| {name} | {version} | {vid} | {fix_versions} | {desc} |")
9692

9793
total_vulns = sum(len(pkg.get("vulns", [])) for pkg in vulnerable)
98-
fixable = sum(
99-
1
100-
for pkg in vulnerable
101-
for v in pkg.get("vulns", [])
102-
if v.get("fix_versions")
103-
)
94+
fixable = sum(1 for pkg in vulnerable for v in pkg.get("vulns", []) if v.get("fix_versions"))
10495
lines.append(
10596
f"\n_{total_vulns} vulnerability/vulnerabilities found "
10697
f"({fixable} fixable) across {len(vulnerable)} package(s)._\n"

src/python_security_auditing/runners.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def run_bandit(scan_dirs: list[str]) -> dict[str, Any]:
7676
)
7777

7878
if output_file.exists():
79-
return dict(json.loads(output_file.read_text())) # type: ignore[arg-type]
79+
return dict(json.loads(output_file.read_text()))
8080
return {"results": [], "errors": []}
8181

8282

tests/test_report.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,62 @@
44

55
import json
66
from pathlib import Path
7+
from typing import Any, cast
78

89
import pytest
9-
1010
from python_security_auditing.report import build_markdown, check_thresholds, write_step_summary
1111
from python_security_auditing.settings import Settings
1212

1313
FIXTURES = Path(__file__).parent / "fixtures"
1414

1515

16-
def load(name: str) -> object:
16+
def load(name: str) -> Any:
1717
return json.loads((FIXTURES / name).read_text())
1818

1919

2020
@pytest.fixture()
21-
def bandit_clean() -> dict: # type: ignore[type-arg]
22-
return load("bandit_clean.json") # type: ignore[return-value]
21+
def bandit_clean() -> dict[str, Any]:
22+
return cast(dict[str, Any], load("bandit_clean.json"))
2323

2424

2525
@pytest.fixture()
26-
def bandit_issues() -> dict: # type: ignore[type-arg]
27-
return load("bandit_issues.json") # type: ignore[return-value]
26+
def bandit_issues() -> dict[str, Any]:
27+
return cast(dict[str, Any], load("bandit_issues.json"))
2828

2929

3030
@pytest.fixture()
31-
def pip_clean() -> list: # type: ignore[type-arg]
32-
return load("pip_audit_clean.json") # type: ignore[return-value]
31+
def pip_clean() -> list[Any]:
32+
return cast(list[Any], load("pip_audit_clean.json"))
3333

3434

3535
@pytest.fixture()
36-
def pip_fixable() -> list: # type: ignore[type-arg]
37-
return load("pip_audit_fixable.json") # type: ignore[return-value]
36+
def pip_fixable() -> list[Any]:
37+
return cast(list[Any], load("pip_audit_fixable.json"))
3838

3939

4040
@pytest.fixture()
41-
def pip_unfixable() -> list: # type: ignore[type-arg]
42-
return load("pip_audit_unfixable.json") # type: ignore[return-value]
41+
def pip_unfixable() -> list[Any]:
42+
return cast(list[Any], load("pip_audit_unfixable.json"))
4343

4444

4545
# ---------------------------------------------------------------------------
4646
# check_thresholds
4747
# ---------------------------------------------------------------------------
4848

4949

50-
def test_clean_no_blocking(bandit_clean: dict, pip_clean: list) -> None: # type: ignore[type-arg]
50+
def test_clean_no_blocking(bandit_clean: dict[str, Any], pip_clean: list[Any]) -> None:
5151
s = Settings()
5252
assert check_thresholds(bandit_clean, pip_clean, s) is False
5353

5454

55-
def test_bandit_high_blocks(bandit_issues: dict, pip_clean: list) -> None: # type: ignore[type-arg]
55+
def test_bandit_high_blocks(bandit_issues: dict[str, Any], pip_clean: list[Any]) -> None:
5656
s = Settings() # threshold=HIGH
5757
assert check_thresholds(bandit_issues, pip_clean, s) is True
5858

5959

60-
def test_bandit_medium_does_not_block_at_high_threshold(bandit_issues: dict, pip_clean: list) -> None: # type: ignore[type-arg]
60+
def test_bandit_medium_does_not_block_at_high_threshold(
61+
bandit_issues: dict[str, Any], pip_clean: list[Any]
62+
) -> None:
6163
"""bandit_issues has HIGH and MEDIUM; only HIGH should block when threshold=HIGH."""
6264
s = Settings()
6365
# Remove HIGH results so only MEDIUM remain
@@ -68,7 +70,9 @@ def test_bandit_medium_does_not_block_at_high_threshold(bandit_issues: dict, pip
6870
assert check_thresholds(medium_only, pip_clean, s) is False
6971

7072

71-
def test_bandit_medium_blocks_at_medium_threshold(bandit_issues: dict, pip_clean: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
73+
def test_bandit_medium_blocks_at_medium_threshold(
74+
bandit_issues: dict[str, Any], pip_clean: list[Any], monkeypatch: pytest.MonkeyPatch
75+
) -> None:
7276
monkeypatch.setenv("BANDIT_SEVERITY_THRESHOLD", "MEDIUM")
7377
s = Settings()
7478
medium_only = {
@@ -78,29 +82,39 @@ def test_bandit_medium_blocks_at_medium_threshold(bandit_issues: dict, pip_clean
7882
assert check_thresholds(medium_only, pip_clean, s) is True
7983

8084

81-
def test_pip_fixable_blocks_by_default(bandit_clean: dict, pip_fixable: list) -> None: # type: ignore[type-arg]
85+
def test_pip_fixable_blocks_by_default(
86+
bandit_clean: dict[str, Any], pip_fixable: list[Any]
87+
) -> None:
8288
s = Settings() # pip_audit_block_on=fixable
8389
assert check_thresholds(bandit_clean, pip_fixable, s) is True
8490

8591

86-
def test_pip_unfixable_does_not_block_on_fixable(bandit_clean: dict, pip_unfixable: list) -> None: # type: ignore[type-arg]
92+
def test_pip_unfixable_does_not_block_on_fixable(
93+
bandit_clean: dict[str, Any], pip_unfixable: list[Any]
94+
) -> None:
8795
s = Settings() # pip_audit_block_on=fixable
8896
assert check_thresholds(bandit_clean, pip_unfixable, s) is False
8997

9098

91-
def test_pip_unfixable_blocks_on_all(bandit_clean: dict, pip_unfixable: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
99+
def test_pip_unfixable_blocks_on_all(
100+
bandit_clean: dict[str, Any], pip_unfixable: list[Any], monkeypatch: pytest.MonkeyPatch
101+
) -> None:
92102
monkeypatch.setenv("PIP_AUDIT_BLOCK_ON", "all")
93103
s = Settings()
94104
assert check_thresholds(bandit_clean, pip_unfixable, s) is True
95105

96106

97-
def test_pip_fixable_does_not_block_on_none(bandit_clean: dict, pip_fixable: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
107+
def test_pip_fixable_does_not_block_on_none(
108+
bandit_clean: dict[str, Any], pip_fixable: list[Any], monkeypatch: pytest.MonkeyPatch
109+
) -> None:
98110
monkeypatch.setenv("PIP_AUDIT_BLOCK_ON", "none")
99111
s = Settings()
100112
assert check_thresholds(bandit_clean, pip_fixable, s) is False
101113

102114

103-
def test_bandit_only_tool_skips_pip(bandit_issues: dict, pip_fixable: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
115+
def test_bandit_only_tool_skips_pip(
116+
bandit_issues: dict[str, Any], pip_fixable: list[Any], monkeypatch: pytest.MonkeyPatch
117+
) -> None:
104118
monkeypatch.setenv("TOOLS", "bandit")
105119
s = Settings()
106120
# pip-audit not in enabled tools, so fixable vulns should not block
@@ -109,15 +123,19 @@ def test_bandit_only_tool_skips_pip(bandit_issues: dict, pip_fixable: list, monk
109123
assert result is True
110124

111125

112-
def test_pip_only_tool_skips_bandit(bandit_issues: dict, pip_fixable: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
126+
def test_pip_only_tool_skips_bandit(
127+
bandit_issues: dict[str, Any], pip_fixable: list[Any], monkeypatch: pytest.MonkeyPatch
128+
) -> None:
113129
monkeypatch.setenv("TOOLS", "pip-audit")
114130
s = Settings()
115131
# bandit not in enabled tools, bandit HIGH issues should not block
116132
result = check_thresholds(bandit_issues, pip_fixable, s)
117133
assert result is True # pip-audit fixable issues do block
118134

119135

120-
def test_pip_only_no_bandit_blocking(bandit_issues: dict, pip_clean: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
136+
def test_pip_only_no_bandit_blocking(
137+
bandit_issues: dict[str, Any], pip_clean: list[Any], monkeypatch: pytest.MonkeyPatch
138+
) -> None:
121139
monkeypatch.setenv("TOOLS", "pip-audit")
122140
s = Settings()
123141
assert check_thresholds(bandit_issues, pip_clean, s) is False
@@ -128,41 +146,43 @@ def test_pip_only_no_bandit_blocking(bandit_issues: dict, pip_clean: list, monke
128146
# ---------------------------------------------------------------------------
129147

130148

131-
def test_markdown_contains_header(bandit_clean: dict, pip_clean: list) -> None: # type: ignore[type-arg]
149+
def test_markdown_contains_header(bandit_clean: dict[str, Any], pip_clean: list[Any]) -> None:
132150
s = Settings()
133151
md = build_markdown(bandit_clean, pip_clean, s)
134152
assert "# Security Audit Report" in md
135153

136154

137-
def test_markdown_clean_result(bandit_clean: dict, pip_clean: list) -> None: # type: ignore[type-arg]
155+
def test_markdown_clean_result(bandit_clean: dict[str, Any], pip_clean: list[Any]) -> None:
138156
s = Settings()
139157
md = build_markdown(bandit_clean, pip_clean, s)
140158
assert "No blocking issues found" in md
141159
assert "✅" in md
142160

143161

144-
def test_markdown_blocking_result(bandit_issues: dict, pip_clean: list) -> None: # type: ignore[type-arg]
162+
def test_markdown_blocking_result(bandit_issues: dict[str, Any], pip_clean: list[Any]) -> None:
145163
s = Settings()
146164
md = build_markdown(bandit_issues, pip_clean, s)
147165
assert "Blocking issues found" in md
148166
assert "❌" in md
149167

150168

151-
def test_markdown_bandit_table(bandit_issues: dict, pip_clean: list) -> None: # type: ignore[type-arg]
169+
def test_markdown_bandit_table(bandit_issues: dict[str, Any], pip_clean: list[Any]) -> None:
152170
s = Settings()
153171
md = build_markdown(bandit_issues, pip_clean, s)
154172
assert "B404" in md
155173
assert "src/app.py" in md
156174

157175

158-
def test_markdown_pip_table(bandit_clean: dict, pip_fixable: list) -> None: # type: ignore[type-arg]
176+
def test_markdown_pip_table(bandit_clean: dict[str, Any], pip_fixable: list[Any]) -> None:
159177
s = Settings()
160178
md = build_markdown(bandit_clean, pip_fixable, s)
161179
assert "requests" in md
162180
assert "GHSA-j8r2-6x86-q33q" in md
163181

164182

165-
def test_markdown_run_url(bandit_clean: dict, pip_clean: list, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[type-arg]
183+
def test_markdown_run_url(
184+
bandit_clean: dict[str, Any], pip_clean: list[Any], monkeypatch: pytest.MonkeyPatch
185+
) -> None:
166186
monkeypatch.setenv("GITHUB_REPOSITORY", "org/repo")
167187
monkeypatch.setenv("GITHUB_RUN_ID", "999")
168188
s = Settings()

tests/test_runners.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from unittest.mock import MagicMock, patch
88

99
import pytest
10-
1110
from python_security_auditing.runners import generate_requirements, run_bandit, run_pip_audit
1211
from python_security_auditing.settings import Settings
1312

@@ -103,7 +102,9 @@ def test_run_bandit_parses_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
103102
assert report["results"][0]["issue_severity"] == "HIGH"
104103

105104

106-
def test_run_bandit_returns_empty_on_missing_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
105+
def test_run_bandit_returns_empty_on_missing_file(
106+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
107+
) -> None:
107108
monkeypatch.chdir(tmp_path)
108109

109110
with patch("python_security_auditing.runners.subprocess.run") as mock_run:
@@ -146,7 +147,9 @@ def test_run_pip_audit_parses_json(tmp_path: Path, monkeypatch: pytest.MonkeyPat
146147
assert (tmp_path / "pip-audit-report.json").exists()
147148

148149

149-
def test_run_pip_audit_returns_empty_on_no_output(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
150+
def test_run_pip_audit_returns_empty_on_no_output(
151+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
152+
) -> None:
150153
monkeypatch.chdir(tmp_path)
151154

152155
with patch("python_security_auditing.runners.subprocess.run") as mock_run:
@@ -156,7 +159,9 @@ def test_run_pip_audit_returns_empty_on_no_output(tmp_path: Path, monkeypatch: p
156159
assert report == []
157160

158161

159-
def test_run_pip_audit_uses_requirements_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
162+
def test_run_pip_audit_uses_requirements_path(
163+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
164+
) -> None:
160165
monkeypatch.chdir(tmp_path)
161166
req_path = tmp_path / "custom-reqs.txt"
162167

0 commit comments

Comments
 (0)