1515CONTRIBUTING = REPO_ROOT / "CONTRIBUTING.md"
1616BANDIT_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)
2737BANDIT = (
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:
4555class 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