[Security hardening] Add automated security audit workflow#2442
[Security hardening] Add automated security audit workflow#2442PascalThuet wants to merge 8 commits intogithub:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new GitHub Actions workflow to introduce automated security checks for the Python codebase, aiming to catch dependency advisories and high-severity static-analysis findings in CI alongside the existing test/lint workflows.
Changes:
- Add
.github/workflows/security.ymlwith a dedicatedSecurity Auditworkflow. - Run
pip-auditon pushes tomain, pull requests, a weekly cron, and manual dispatch. - Run Bandit against
src/, temporarily skippingB602pending the shell-step hardening tracked in #2440.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/security.yml |
New CI workflow that adds dependency-audit and static-analysis jobs for the Python project. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 1/1 changed files
- Comments generated: 5
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback. If not applicable, please explain why
|
Addressed the Copilot feedback in the latest push:
Local validation: uv export --quiet --extra test --frozen --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
uvx --from bandit==1.9.4 bandit -r src -lll
git diff --checkResults: |
|
Added automated regression coverage for the security workflow in The new tests statically verify that:
Validation after this commit: uv run python -m pytest tests/test_security_workflow.py -q
uv export --quiet --extra test --frozen --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
uvx --from bandit==1.9.4 bandit -r src -lll
git diff --checkAll passed locally. |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
src/specify_cli/init.py:414
- This second
# nosec B602suppresses Bandit for the non-capturing branch as well, so neither path throughrun_commandwill ever raise B602 again. If the intent is only to defer the current finding until #2440, the skip needs to stay in the workflow/configuration layer rather than permanently muting this call site in source.
# shell=True is only available to callers that opt in explicitly.
subprocess.run(cmd, check=check_return, shell=shell) # nosec B602
- Files reviewed: 5/5 changed files
- Comments generated: 5
|
Please address Copilot feedback. If not applicable, please explain why. Note the shell step should indeed be ignored |
|
Addressed the follow-up review in
Validation: uv export --quiet --extra test --format requirements.txt --no-emit-project --output-file /tmp/spec-kit-audit-requirements.txt
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json
uv run python -m pytest tests/test_security_workflow.py tests/test_workflows.py -q
uvx ruff check src/
git diff --checkI also verified the |
|
One more small cleanup after re-review: pushed Reason: it still audits the runtime + Revalidated locally: uv pip compile pyproject.toml --extra test --quiet --output-file /tmp/spec-kit-audit-requirements.txt
uvx --from pip-audit==2.10.0 pip-audit -r /tmp/spec-kit-audit-requirements.txt --progress-spinner off
uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json
uv run python -m pytest tests/test_security_workflow.py -q
git diff --checkAll passed. |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
tests/test_security_workflow.py:91
- Bandit doesn't require the exact literal
# nosec B602to suppress this finding; plain# nosec,#nosec, and multi-ID forms also disable B602. This check leaves those suppression paths untested, so a future source-level bypass can slip in without failing CI.
)
def test_b602_is_not_suppressed_in_source(self):
source_text = "\n".join(
path.read_text(encoding="utf-8")
for path in (REPO_ROOT / "src").rglob("*.py")
)
- Files reviewed: 5/5 changed files
- Comments generated: 3
|
Please address Copilot feedback |
|
Addressed the latest Copilot review in
Validation: uv run python -m pytest tests/test_security_workflow.py -q
uv pip compile pyproject.toml --extra test --python-version 3.11 --generate-hashes --quiet --output-file /tmp/spec-kit-audit-py311.txt
uv pip compile pyproject.toml --extra test --python-version 3.12 --generate-hashes --quiet --output-file /tmp/spec-kit-audit-py312.txt
uv pip compile pyproject.toml --extra test --python-version 3.13 --generate-hashes --quiet --output-file /tmp/spec-kit-audit-py313.txt
uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r /tmp/spec-kit-audit-py311.txt --progress-spinner off
uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r /tmp/spec-kit-audit-py312.txt --progress-spinner off
uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r /tmp/spec-kit-audit-py313.txt --progress-spinner off
uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json
git diff --checkAll passed locally. |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 5/5 changed files
- Comments generated: 5
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
|
Addressed the latest Copilot review round in commit aa02062:
Local validation passed:
The new GitHub workflow runs are currently waiting for fork PR approval ( |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 6/6 changed files
- Comments generated: 2
|
Please address Copilot feedback |
|
Added a follow-up hardening commit: This addresses the similar risk surfaces we discussed after the Copilot review:
Local validation passed:
The latest GitHub workflow runs are again waiting for fork PR approval ( |
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 24/24 changed files
- Comments generated: 5
| normalized = file_path.replace("\\", "/") | ||
| normalized_path = PurePosixPath(normalized) | ||
| has_windows_drive = re.match(r"^[A-Za-z]:/", normalized) is not None | ||
| if normalized_path.is_absolute() or any( | ||
| part == ".." for part in normalized_path.parts | ||
| ) or has_windows_drive: |
| with self._open_url(download_url, timeout=60) as response: | ||
| zip_data = response.read() | ||
| zip_data = read_response_limited( | ||
| response, | ||
| error_type=ExtensionError, | ||
| label=f"extension '{extension_id}' download", | ||
| ) | ||
|
|
||
| verify_sha256( | ||
| zip_data, | ||
| ext_info.get("sha256"), | ||
| error_type=ExtensionError, | ||
| label=f"extension '{extension_id}' download", |
| with self._open_url(download_url, timeout=60) as response: | ||
| zip_data = response.read() | ||
| zip_data = read_response_limited( | ||
| response, | ||
| error_type=PresetError, | ||
| label=f"preset '{pack_id}' download", | ||
| ) | ||
|
|
||
| verify_sha256( | ||
| zip_data, | ||
| pack_info.get("sha256"), | ||
| error_type=PresetError, | ||
| label=f"preset '{pack_id}' download", | ||
| ) |
| - name: Run pip-audit (scheduled live resolution) | ||
| if: ${{ github.event_name == 'schedule' }} | ||
| run: uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r "${{ runner.temp }}/spec-kit-audit-requirements.txt" --progress-spinner off | ||
|
|
| if len(members) > max_entries: | ||
| _raise( | ||
| error_type, | ||
| f"ZIP archive contains too many entries ({len(members)} > {max_entries})", | ||
| ) | ||
|
|
||
| normalized_members: list[tuple[zipfile.ZipInfo, str]] = [] | ||
| total_size = 0 | ||
| for member in members: | ||
| normalized_name = _safe_zip_name(member.filename, error_type=error_type) | ||
|
|
||
| mode = member.external_attr >> 16 | ||
| if stat.S_ISLNK(mode): | ||
| _raise(error_type, f"Unsafe symlink in ZIP archive: {member.filename}") | ||
|
|
||
| member_path = (target_dir / normalized_name).resolve() | ||
| try: | ||
| member_path.relative_to(target_root) | ||
| except ValueError: | ||
| _raise( | ||
| error_type, | ||
| f"Unsafe path in ZIP archive: {member.filename} " | ||
| "(potential path traversal)", | ||
| ) | ||
|
|
||
| if not member.is_dir(): | ||
| if member.file_size > max_member_bytes: | ||
| _raise( | ||
| error_type, | ||
| f"ZIP member {member.filename} exceeds maximum size " | ||
| f"of {max_member_bytes} bytes", | ||
| ) | ||
| total_size += member.file_size | ||
| if total_size > max_total_bytes: | ||
| _raise( | ||
| error_type, | ||
| f"ZIP archive exceeds maximum uncompressed size " | ||
| f"of {max_total_bytes} bytes", | ||
| ) |
|
Please address Copilot feedback. Thank you once again for working through all these reviews! |
Summary
Security context
This creates a repeatable CI baseline for dependency advisories and high-severity static-analysis issues while preserving a scheduled live-resolution signal for newly published dependency problems. It also closes similar supply-chain and archive-handling gaps found during follow-up review.
Closes #2438
Validation