22
33from __future__ import annotations
44
5+ import json
56import re
67from pathlib import Path
78
9+ import pytest
810import yaml
911
1012
1113REPO_ROOT = Path (__file__ ).resolve ().parent .parent
1214SECURITY_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "security.yml"
1315CONTRIBUTING = REPO_ROOT / "CONTRIBUTING.md"
16+ BANDIT_BASELINE = REPO_ROOT / ".github" / "bandit-baseline.json"
1417
1518AUDIT_REQUIREMENTS = "/tmp/spec-kit-audit-requirements.txt"
16- EXPORT_TEST_DEPS = (
17- "uv export --quiet --extra test --frozen -- format requirements.txt "
19+ EXPORT_TEST_EXTRA_DEPS = (
20+ "uv export --quiet --extra test --format requirements.txt "
1821 f"--no-emit-project --output-file { AUDIT_REQUIREMENTS } "
1922)
2023PIP_AUDIT = (
2124 "uvx --from pip-audit==2.10.0 pip-audit "
2225 f"-r { AUDIT_REQUIREMENTS } --progress-spinner off"
2326)
24- BANDIT = "uvx --from bandit==1.9.4 bandit -r src -lll"
27+ BANDIT = (
28+ "uvx --from bandit==1.9.4 bandit -r src -lll "
29+ "--baseline .github/bandit-baseline.json"
30+ )
2531
2632
2733def _load_security_workflow () -> dict :
@@ -39,11 +45,13 @@ def _step_run(job_name: str, step_name: str) -> str:
3945class TestSecurityWorkflow :
4046 """Guard the security workflow against review-feedback regressions."""
4147
42- def test_dependency_audit_uses_locked_test_extra_export (self ):
48+ def test_dependency_audit_uses_test_extra_export_without_lockfile_flags (self ):
4349 run = _step_run ("dependency-audit" , "Run pip-audit" )
4450
45- assert EXPORT_TEST_DEPS in run
51+ assert EXPORT_TEST_EXTRA_DEPS in run
4652 assert PIP_AUDIT in run
53+ assert "--frozen" not in run
54+ assert "--locked" not in run
4755 assert "uvx pip-audit ." not in run
4856
4957 def test_security_tools_are_pinned (self ):
@@ -61,10 +69,38 @@ def test_bandit_does_not_globally_skip_b602(self):
6169 assert run == BANDIT
6270 assert "--skip" not in run
6371 assert "--skip B602" not in workflow_text
72+ assert "--baseline .github/bandit-baseline.json" in run
73+
74+ def test_bandit_baseline_only_ignores_shell_step_b602 (self ):
75+ baseline = json .loads (BANDIT_BASELINE .read_text (encoding = "utf-8" ))
76+ results = baseline ["results" ]
77+
78+ assert len (results ) == 1
79+ assert results [0 ]["test_id" ] == "B602"
80+ assert (
81+ results [0 ]["filename" ]
82+ == "src/specify_cli/workflows/steps/shell/__init__.py"
83+ )
84+
85+ def test_b602_is_not_suppressed_in_source (self ):
86+ source_text = "\n " .join (
87+ path .read_text (encoding = "utf-8" )
88+ for path in (REPO_ROOT / "src" ).rglob ("*.py" )
89+ )
90+
91+ assert "# nosec B602" not in source_text
92+
93+ def test_run_command_rejects_shell_true (self ):
94+ from specify_cli import run_command
95+
96+ with pytest .raises (ValueError , match = "shell=True" ):
97+ run_command (["echo" , "hello" ], shell = True )
6498
6599 def test_contributing_documents_security_commands (self ):
66100 contributing_text = CONTRIBUTING .read_text (encoding = "utf-8" )
67101
68- assert EXPORT_TEST_DEPS in contributing_text
102+ assert EXPORT_TEST_EXTRA_DEPS in contributing_text
69103 assert PIP_AUDIT in contributing_text
70104 assert BANDIT in contributing_text
105+ assert "--frozen" not in contributing_text
106+ assert "--locked" not in contributing_text
0 commit comments