Skip to content

Commit e1e8051

Browse files
committed
Address follow-up security workflow review
1 parent 397e0d7 commit e1e8051

6 files changed

Lines changed: 88 additions & 18 deletions

File tree

.github/bandit-baseline.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"results": [
3+
{
4+
"code": "34 run_cmd,\n35 shell=True,\n36 capture_output=True,\n37 text=True,\n38 cwd=cwd,\n39 timeout=300,\n40 )\n41 output = {\n42 \"exit_code\": proc.returncode,\n43 \"stdout\": proc.stdout,\n",
5+
"col_offset": 19,
6+
"end_col_offset": 13,
7+
"filename": "src/specify_cli/workflows/steps/shell/__init__.py",
8+
"issue_confidence": "HIGH",
9+
"issue_cwe": {
10+
"id": 78,
11+
"link": "https://cwe.mitre.org/data/definitions/78.html"
12+
},
13+
"issue_severity": "HIGH",
14+
"issue_text": "subprocess call with shell=True identified, security issue.",
15+
"line_number": 35,
16+
"line_range": [
17+
33,
18+
34,
19+
35,
20+
36,
21+
37,
22+
38,
23+
39,
24+
40
25+
],
26+
"more_info": "https://bandit.readthedocs.io/en/1.9.4/plugins/b602_subprocess_popen_with_shell_equals_true.html",
27+
"test_id": "B602",
28+
"test_name": "subprocess_popen_with_shell_equals_true"
29+
}
30+
]
31+
}

.github/workflows/security.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929

3030
- name: Run pip-audit
3131
run: |
32-
uv export --quiet --extra test --frozen --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
32+
uv export --quiet --extra test --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
3333
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
3434
3535
static-analysis:
@@ -48,4 +48,4 @@ jobs:
4848
python-version: "3.13"
4949

5050
- name: Run Bandit
51-
run: uvx --from bandit==1.9.4 bandit -r src -lll
51+
run: uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json

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 export --quiet --extra test --frozen --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
87+
uv export --quiet --extra test --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
8888
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
89-
uvx --from bandit==1.9.4 bandit -r src -lll
89+
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 uses the locked 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.
9393

9494
### Manual testing
9595

src/specify_cli/__init__.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,21 +397,24 @@ def callback(
397397
console.print()
398398

399399
def run_command(cmd: list[str], check_return: bool = True, capture: bool = False, shell: bool = False) -> Optional[str]:
400-
"""Run a shell command and optionally capture output."""
400+
"""Run a command without invoking a shell and optionally capture output."""
401+
if shell:
402+
raise ValueError(
403+
"run_command does not support shell=True; use a reviewed "
404+
"subprocess.run call for shell-specific behavior."
405+
)
406+
401407
try:
402408
if capture:
403-
# shell=True is only available to callers that opt in explicitly.
404-
result = subprocess.run( # nosec B602
409+
result = subprocess.run(
405410
cmd,
406411
check=check_return,
407412
capture_output=True,
408413
text=True,
409-
shell=shell,
410414
)
411415
return result.stdout.strip()
412416
else:
413-
# shell=True is only available to callers that opt in explicitly.
414-
subprocess.run(cmd, check=check_return, shell=shell) # nosec B602
417+
subprocess.run(cmd, check=check_return)
415418
return None
416419
except subprocess.CalledProcessError as e:
417420
if check_return:

src/specify_cli/workflows/steps/shell/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
3232
try:
3333
proc = subprocess.run(
3434
run_cmd,
35-
shell=True, # nosec B602
35+
shell=True,
3636
capture_output=True,
3737
text=True,
3838
cwd=cwd,

tests/test_security_workflow.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,32 @@
22

33
from __future__ import annotations
44

5+
import json
56
import re
67
from pathlib import Path
78

9+
import pytest
810
import yaml
911

1012

1113
REPO_ROOT = Path(__file__).resolve().parent.parent
1214
SECURITY_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "security.yml"
1315
CONTRIBUTING = REPO_ROOT / "CONTRIBUTING.md"
16+
BANDIT_BASELINE = REPO_ROOT / ".github" / "bandit-baseline.json"
1417

1518
AUDIT_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
)
2023
PIP_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

2733
def _load_security_workflow() -> dict:
@@ -39,11 +45,13 @@ def _step_run(job_name: str, step_name: str) -> str:
3945
class 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

Comments
 (0)