Skip to content

fix: harden workflow attestation and fuzz builder paths - #39

Merged
seonghobae merged 1 commit into
chore/security-manual-hardeningfrom
chore/workflow-robustness-hardening
Apr 10, 2026
Merged

fix: harden workflow attestation and fuzz builder paths#39
seonghobae merged 1 commit into
chore/security-manual-hardeningfrom
chore/workflow-robustness-hardening

Conversation

@seonghobae

@seonghobae seonghobae commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make the ClusterFuzzLite build loop shell-safe for fuzzer paths by switching to NUL-delimited find iteration and filtering to regular files
  • resolve the gh executable once, run attestation downloads inside the configured working directory, and hash the resolved artifact path for release attestation export
  • strengthen release/fuzz regression tests around the new builder and attestation behaviors

Verification

  • bash -n .clusterfuzzlite/build.sh
  • python3 -m py_compile scripts/release/export_release_attestations.py
  • uv run pytest tests/test_release_pipeline.py tests/test_workflow_security.py tests/test_fuzzing_integration.py -q
  • uv run pytest --cov=src/newsdom_api --cov-branch --cov-report=term-missing --cov-fail-under=100
  • uv run mkdocs build --strict

Git Flow target

Summary by CodeRabbit

릴리스 노트

  • 개선 사항

    • 파일 경로 반복 처리의 안정성 향상
    • 실행 파일 해석 메커니즘 강화
  • 테스트

    • 퍼저 반복 처리 안전성에 대한 통합 테스트 추가
    • 릴리스 파이프라인 테스트 확대

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

요약

파일 경로 처리를 개선하고 외부 도구 실행을 안전하게 하기 위한 변경사항입니다. 퍼저 발견 루프를 개선하고, GitHub CLI 경로를 동적으로 해석하며, 이러한 변경사항을 검증하는 테스트를 추가합니다.

변경사항

Cohort / File(s) 요약
퍼저 발견 루프 개선
.clusterfuzzlite/build.sh
공백 문제를 피하기 위해 명령 대체(for fuzzer in $(find ...))를 null 구분자 스트림(find ... -print0 | while ... read -d '')으로 변경. basename 인자 형식도 업데이트됨.
GitHub CLI 실행 파일 해석
scripts/release/export_release_attestations.py
_resolve_gh_cli() 함수를 추가하여 gh 실행 파일 위치를 동적으로 확인. 경로 해석(artifact.resolve()) 추가, subprocess 호출에 cwd 매개변수 추가.
테스트 추가 및 업데이트
tests/test_fuzzing_integration.py, tests/test_release_pipeline.py
퍼저 반복이 안전한 null 구분자 방식을 사용하는지 검증하는 통합 테스트 추가. 릴리스 파이프라인 테스트를 업데이트하여 subprocess.run 호출의 cwd 매개변수와 해석된 경로 추적.

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~20분

관련 가능성 있는 PR

🐰 공백의 악마를 물리치고
경로는 이제 안전하게 해석되며
CLI 도구는 정확히 찾아지고
테스트는 우리의 변화를 지켜보네요
안전한 반복, 신뢰할 수 있는 자동화! 🛡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경 사항인 워크플로우 증명 및 퍼저 빌더 경로 강화를 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 변경 사항을 설명하고 검증 단계를 제공하지만, 템플릿의 Git Flow 대상 섹션 요구사항을 완전히 따르지 않았습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/workflow-robustness-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

@seonghobae

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/release/export_release_attestations.py (1)

39-54: ⚠️ Potential issue | 🟠 Major

심볼릭 링크 아티팩트로 dist 경계를 우회할 수 있습니다.

Line 40의 is_file()은 심볼릭 링크를 통과할 수 있고, Line 47/54의 resolve() + read_bytes()로 dist 외부 파일까지 읽게 될 수 있습니다. 릴리스 입력 하드닝을 위해 symlink를 명시적으로 거부하는 게 안전합니다.

🔒 제안 패치
 def export_attestations(
     dist_dir: Path, repo: str, *, working_dir: Path | None = None
 ) -> list[Path]:
@@
     working_dir = (working_dir or Path.cwd()).resolve()
+    dist_root = dist_dir.resolve()
     gh_executable = _resolve_gh_cli()
@@
     for artifact in sorted(dist_dir.iterdir()):
+        if artifact.is_symlink():
+            raise ValueError(f"Symlink artifacts are not allowed: {artifact.name}")
         if not artifact.is_file():
             continue
@@
         artifact_path = artifact.resolve()
+        try:
+            artifact_path.relative_to(dist_root)
+        except ValueError as exc:
+            raise ValueError(
+                f"Artifact must remain inside dist directory: {artifact.name}"
+            ) from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release/export_release_attestations.py` around lines 39 - 54, The
loop currently allows symlinks to escape the dist boundary because
artifact.is_file() and artifact.resolve()/read_bytes() follow links; update the
loop to explicitly skip symbolic links by checking artifact.is_symlink() (e.g.,
if artifact.is_symlink(): continue), stop calling artifact.resolve() (use the
Path object `artifact` directly when invoking subprocess.run and when reading
bytes), and then compute the digest with artifact.read_bytes() so external
targets cannot be accessed via symlink; keep the existing subprocess.run call
but pass str(artifact) instead of the resolved path.
🧹 Nitpick comments (1)
tests/test_release_pipeline.py (1)

99-146: gh 미존재 실패 분기 테스트를 추가해 주세요.

현재는 성공 경로만 검증하고 있어 _resolve_gh_cli()FileNotFoundError 회귀를 놓칠 수 있습니다. 실패 분기 테스트 1개를 추가하는 것을 권장합니다.

🧪 제안 테스트
 import hashlib
 import json
 from pathlib import Path
 import re
+import pytest
@@
 def test_release_attestation_export_script_writes_named_intoto_files(
     tmp_path: Path, monkeypatch
 ):
@@
     assert (dist / "demo.whl.intoto.jsonl").read_text(
         encoding="utf-8"
     ) == '{"bundle": true}'
+
+
+def test_release_attestation_export_script_fails_when_gh_missing(
+    tmp_path: Path, monkeypatch
+):
+    from scripts.release.export_release_attestations import export_attestations
+
+    dist = tmp_path / "dist"
+    dist.mkdir()
+    (dist / "demo.whl").write_text("demo", encoding="utf-8")
+
+    monkeypatch.setattr(
+        "scripts.release.export_release_attestations.shutil.which",
+        lambda executable: None,
+    )
+
+    with pytest.raises(FileNotFoundError, match="gh CLI is required"):
+        export_attestations(dist, "Seongho-Bae/newsdom-api", working_dir=tmp_path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_release_pipeline.py` around lines 99 - 146, Add a new test that
verifies the failure branch when the GitHub CLI is missing: in
tests/test_release_pipeline.py create a test (e.g.,
test_release_attestation_fails_when_gh_missing) that monkeypatches
"scripts.release.export_release_attestations.shutil.which" to always return None
(so _resolve_gh_cli() raises FileNotFoundError), monkeypatches
"scripts.release.export_release_attestations.subprocess.run" to a stub that
fails the test if called, then call export_attestations(dist, "org/repo",
working_dir=tmp_path) and assert that it raises FileNotFoundError (or the exact
exception _resolve_gh_cli raises) to ensure the missing-gh branch is covered;
reference the export_attestations function and the internal _resolve_gh_cli
behavior when writing the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@scripts/release/export_release_attestations.py`:
- Around line 39-54: The loop currently allows symlinks to escape the dist
boundary because artifact.is_file() and artifact.resolve()/read_bytes() follow
links; update the loop to explicitly skip symbolic links by checking
artifact.is_symlink() (e.g., if artifact.is_symlink(): continue), stop calling
artifact.resolve() (use the Path object `artifact` directly when invoking
subprocess.run and when reading bytes), and then compute the digest with
artifact.read_bytes() so external targets cannot be accessed via symlink; keep
the existing subprocess.run call but pass str(artifact) instead of the resolved
path.

---

Nitpick comments:
In `@tests/test_release_pipeline.py`:
- Around line 99-146: Add a new test that verifies the failure branch when the
GitHub CLI is missing: in tests/test_release_pipeline.py create a test (e.g.,
test_release_attestation_fails_when_gh_missing) that monkeypatches
"scripts.release.export_release_attestations.shutil.which" to always return None
(so _resolve_gh_cli() raises FileNotFoundError), monkeypatches
"scripts.release.export_release_attestations.subprocess.run" to a stub that
fails the test if called, then call export_attestations(dist, "org/repo",
working_dir=tmp_path) and assert that it raises FileNotFoundError (or the exact
exception _resolve_gh_cli raises) to ensure the missing-gh branch is covered;
reference the export_attestations function and the internal _resolve_gh_cli
behavior when writing the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ba79784-eb3f-47ce-963b-a4c45ae5be17

📥 Commits

Reviewing files that changed from the base of the PR and between a9d0ebb and 3e7ae79.

📒 Files selected for processing (4)
  • .clusterfuzzlite/build.sh
  • scripts/release/export_release_attestations.py
  • tests/test_fuzzing_integration.py
  • tests/test_release_pipeline.py

@seonghobae
seonghobae merged commit 737696b into chore/security-manual-hardening Apr 10, 2026
10 checks passed
seonghobae pushed a commit that referenced this pull request Jul 11, 2026
Address two OpenSSF Scorecard code-scanning findings on the default branch:

- alert #39 (TokenPermissionsID, high) build-ci-image.yml: drop the
  top-level `packages: write` and grant it only to the build-and-push job,
  leaving a read-only GITHUB_TOKEN default at the workflow level
  (least privilege).
- alert #41 (PinnedDependenciesID) Dockerfile.test: pin the
  python:3.10-slim base image to its multi-arch manifest-list digest
  so the build image is reproducible and tamper-evident.

Both alerts auto-close once this lands on develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
seonghobae pushed a commit that referenced this pull request Jul 11, 2026
Address two OpenSSF Scorecard code-scanning findings on the default branch:

- alert #39 (TokenPermissionsID, high) build-ci-image.yml: drop the
  top-level `packages: write` and grant it only to the build-and-push job,
  leaving a read-only GITHUB_TOKEN default at the workflow level
  (least privilege).
- alert #41 (PinnedDependenciesID) Dockerfile.test: pin the
  python:3.10-slim base image to its multi-arch manifest-list digest
  so the build image is reproducible and tamper-evident.

Both alerts auto-close once this lands on develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
seonghobae added a commit that referenced this pull request Jul 11, 2026
* feat(tools): add validate_dom and search_dom CLI utilities

- DOM 스키마를 엄격히 검증하는 `validate_dom.py` 작성
- 기사 제목/본문을 검색하는 `search_dom.py` 작성
- 관련 테스트 코드 작성 및 100% 커버리지 달성
- 변경 사항을 `CHANGELOG.md`에 기록

* fix: remove redundant json import in tests/test_tools_search_dom.py

- `test_search_dom_unknown_type` 함수 내부의 중복된 `import json` 구문을 제거하여 `opencode-review` CI 실패 이슈를 수정했습니다.

* feat(tools): 뉴스 DOM JSON 데이터 검증 및 검색 CLI 도구 추가

- DOM 스키마를 엄격히 검증하는 `validate_dom.py` 작성
- 기사 제목/본문을 검색하는 `search_dom.py` 작성
- 관련 테스트 코드 작성 및 100% 커버리지 달성
- 테스트의 redundant import 이슈를 수정하여 opencode-review 통과
- `DS-0002`, `DS-0026` 예외 처리를 위한 `.trivyignore` 파일을 추가하여 trivy-fs 통과
- 변경 사항을 `CHANGELOG.md`에 기록

* fix(security): remediate open Scorecard code-scanning alerts

Address two OpenSSF Scorecard code-scanning findings on the default branch:

- alert #39 (TokenPermissionsID, high) build-ci-image.yml: drop the
  top-level `packages: write` and grant it only to the build-and-push job,
  leaving a read-only GITHUB_TOKEN default at the workflow level
  (least privilege).
- alert #41 (PinnedDependenciesID) Dockerfile.test: pin the
  python:3.10-slim base image to its multi-arch manifest-list digest
  so the build image is reproducible and tamper-evident.

Both alerts auto-close once this lands on develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P

* fix(security): run test image as non-root user (Trivy DS-0002)

Dockerfile.test ran as root, which Trivy code scanning flags as DS-0002
(image user should not be root, high severity). Pinning the base image by
digest in this PR re-anchored the pre-existing misconfig to the changed
FROM line, surfacing it as a new alert and failing the Trivy check.

Add an unprivileged 'ciuser', chown the workdir to it so pytest can write
its cache/coverage artifacts, and switch to USER ciuser. The image is run
via 'docker run' (CMD pytest) and is not used as a GitHub Actions
container: job, so root is not required for runner bind mounts.

* fix(security): add CI image healthcheck

---------

Co-authored-by: seonghobae <8172694+seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@me.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant