Skip to content

Commit 129d19e

Browse files
committed
Address latest security workflow review
1 parent 6f1da27 commit 129d19e

3 files changed

Lines changed: 75 additions & 28 deletions

File tree

.github/workflows/security.yml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,29 @@ on:
1313

1414
jobs:
1515
dependency-audit:
16-
name: Dependency audit
17-
runs-on: ubuntu-latest
16+
name: Dependency audit (${{ matrix.os }}, Python ${{ matrix.python-version }})
17+
runs-on: ${{ matrix.os }}
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
os: [ubuntu-latest, windows-latest]
22+
python-version: ["3.11", "3.12", "3.13"]
1823
steps:
1924
- name: Checkout
2025
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
2126

2227
- name: Install uv
2328
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
2429

25-
- name: Set up Python
30+
- name: Set up Python ${{ matrix.python-version }}
2631
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
2732
with:
28-
python-version: "3.13"
33+
python-version: ${{ matrix.python-version }}
2934

3035
- name: Run pip-audit
3136
run: |
32-
uv pip compile pyproject.toml --extra test --quiet --output-file /tmp/spec-kit-audit-requirements.txt
33-
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
37+
uv pip compile pyproject.toml --extra test --python-version "${{ matrix.python-version }}" --generate-hashes --quiet --output-file "${{ runner.temp }}/spec-kit-audit-requirements.txt"
38+
uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r "${{ runner.temp }}/spec-kit-audit-requirements.txt" --progress-spinner off
3439
3540
static-analysis:
3641
name: Static analysis

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ Run this when you change agent metadata, context update scripts, or integration
8484
#### Security checks
8585

8686
```bash
87-
uv pip compile pyproject.toml --extra test --quiet --output-file /tmp/spec-kit-audit-requirements.txt
88-
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
87+
uv pip compile pyproject.toml --extra test --generate-hashes --quiet --output-file spec-kit-audit-requirements.txt
88+
uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r spec-kit-audit-requirements.txt --progress-spinner off
8989
uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json
9090
```
9191

92-
Run these before changing dependency metadata, workflow execution code, subprocess usage, or security-sensitive paths. The dependency audit resolves the runtime and `test` extra dependency set used by CI and contributors.
92+
Run these before changing dependency metadata, workflow execution code, subprocess usage, or security-sensitive paths. The dependency audit resolves the runtime and `test` extra dependency set used by CI and contributors. CI runs the dependency audit across the supported Python and OS matrix; locally, run these commands from the environment you want to reproduce.
9393

9494
### Manual testing
9595

tests/test_security_workflow.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@
1515
CONTRIBUTING = REPO_ROOT / "CONTRIBUTING.md"
1616
BANDIT_BASELINE = REPO_ROOT / ".github" / "bandit-baseline.json"
1717

18-
AUDIT_REQUIREMENTS = "/tmp/spec-kit-audit-requirements.txt"
19-
COMPILE_TEST_EXTRA_DEPS = (
20-
"uv pip compile pyproject.toml --extra test --quiet "
21-
f"--output-file {AUDIT_REQUIREMENTS}"
18+
WORKFLOW_AUDIT_REQUIREMENTS = '"${{ runner.temp }}/spec-kit-audit-requirements.txt"'
19+
LOCAL_AUDIT_REQUIREMENTS = "spec-kit-audit-requirements.txt"
20+
WORKFLOW_COMPILE_TEST_EXTRA_DEPS = (
21+
"uv pip compile pyproject.toml --extra test "
22+
'--python-version "${{ matrix.python-version }}" --generate-hashes --quiet '
23+
f"--output-file {WORKFLOW_AUDIT_REQUIREMENTS}"
2224
)
23-
PIP_AUDIT = (
24-
"uvx --from pip-audit==2.10.0 pip-audit "
25-
f"-r {AUDIT_REQUIREMENTS} --progress-spinner off"
25+
LOCAL_COMPILE_TEST_EXTRA_DEPS = (
26+
"uv pip compile pyproject.toml --extra test --generate-hashes --quiet "
27+
f"--output-file {LOCAL_AUDIT_REQUIREMENTS}"
28+
)
29+
WORKFLOW_PIP_AUDIT = (
30+
"uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes "
31+
f"-r {WORKFLOW_AUDIT_REQUIREMENTS} --progress-spinner off"
32+
)
33+
LOCAL_PIP_AUDIT = (
34+
"uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes "
35+
f"-r {LOCAL_AUDIT_REQUIREMENTS} --progress-spinner off"
2636
)
2737
BANDIT = (
2838
"uvx --from bandit==1.9.4 bandit -r src -lll "
@@ -45,25 +55,52 @@ def _step_run(job_name: str, step_name: str) -> str:
4555
class TestSecurityWorkflow:
4656
"""Guard the security workflow against review-feedback regressions."""
4757

48-
def test_dependency_audit_compiles_test_extra_requirements_without_lockfile(self):
58+
def test_dependency_audit_compiles_test_extra_requirements(self):
4959
run = _step_run("dependency-audit", "Run pip-audit")
5060

51-
assert COMPILE_TEST_EXTRA_DEPS in run
52-
assert PIP_AUDIT in run
61+
assert WORKFLOW_COMPILE_TEST_EXTRA_DEPS in run
62+
assert WORKFLOW_PIP_AUDIT in run
63+
assert "--generate-hashes" in run
64+
assert "--require-hashes" in run
65+
assert "--disable-pip" in run
66+
assert "${{ runner.temp }}" in run
5367
assert "uv export" not in run
5468
assert "--frozen" not in run
5569
assert "--locked" not in run
5670
assert "uv.lock" not in run
71+
assert "/tmp/" not in run
5772
assert "uvx pip-audit ." not in run
5873

74+
def test_dependency_audit_runs_supported_python_os_matrix(self):
75+
workflow = _load_security_workflow()
76+
matrix = workflow["jobs"]["dependency-audit"]["strategy"]["matrix"]
77+
78+
assert matrix["os"] == ["ubuntu-latest", "windows-latest"]
79+
assert matrix["python-version"] == ["3.11", "3.12", "3.13"]
80+
assert workflow["jobs"]["dependency-audit"]["runs-on"] == "${{ matrix.os }}"
81+
5982
def test_security_tools_are_pinned(self):
6083
workflow_text = SECURITY_WORKFLOW.read_text(encoding="utf-8")
6184

62-
assert PIP_AUDIT in workflow_text
85+
assert WORKFLOW_PIP_AUDIT in workflow_text
6386
assert BANDIT in workflow_text
6487
assert re.search(r"\buvx\s+pip-audit\b", workflow_text) is None
6588
assert re.search(r"\buvx\s+bandit\b", workflow_text) is None
6689

90+
def test_actions_are_pinned_to_full_commit_shas(self):
91+
workflow = _load_security_workflow()
92+
uses_refs = [
93+
step["uses"]
94+
for job in workflow["jobs"].values()
95+
for step in job["steps"]
96+
if "uses" in step
97+
]
98+
99+
assert uses_refs
100+
for uses_ref in uses_refs:
101+
assert re.search(r"@[0-9a-f]{40}$", uses_ref), uses_ref
102+
assert re.search(r"@v\d+", uses_ref) is None
103+
67104
def test_bandit_does_not_globally_skip_b602(self):
68105
run = _step_run("static-analysis", "Run Bandit")
69106
workflow_text = SECURITY_WORKFLOW.read_text(encoding="utf-8")
@@ -84,13 +121,17 @@ def test_bandit_baseline_only_ignores_shell_step_b602(self):
84121
== "src/specify_cli/workflows/steps/shell/__init__.py"
85122
)
86123

87-
def test_b602_is_not_suppressed_in_source(self):
88-
source_text = "\n".join(
89-
path.read_text(encoding="utf-8")
90-
for path in (REPO_ROOT / "src").rglob("*.py")
91-
)
124+
def test_bandit_nosec_is_not_suppressed_in_source(self):
125+
nosec_lines = []
126+
for path in (REPO_ROOT / "src").rglob("*.py"):
127+
for line_number, line in enumerate(
128+
path.read_text(encoding="utf-8").splitlines(),
129+
start=1,
130+
):
131+
if re.search(r"#\s*nosec\b", line, flags=re.IGNORECASE):
132+
nosec_lines.append(f"{path.relative_to(REPO_ROOT)}:{line_number}")
92133

93-
assert "# nosec B602" not in source_text
134+
assert nosec_lines == []
94135

95136
def test_run_command_rejects_shell_true(self):
96137
from specify_cli import run_command
@@ -101,9 +142,10 @@ def test_run_command_rejects_shell_true(self):
101142
def test_contributing_documents_security_commands(self):
102143
contributing_text = CONTRIBUTING.read_text(encoding="utf-8")
103144

104-
assert COMPILE_TEST_EXTRA_DEPS in contributing_text
105-
assert PIP_AUDIT in contributing_text
145+
assert LOCAL_COMPILE_TEST_EXTRA_DEPS in contributing_text
146+
assert LOCAL_PIP_AUDIT in contributing_text
106147
assert BANDIT in contributing_text
148+
assert "/tmp/" not in contributing_text
107149
assert "uv export" not in contributing_text
108150
assert "--frozen" not in contributing_text
109151
assert "--locked" not in contributing_text

0 commit comments

Comments
 (0)