From 28705c62132b3d54266398823c124e5063b8aefe Mon Sep 17 00:00:00 2001 From: YONGBEOM GWON Date: Wed, 29 Jul 2026 16:25:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(audit):=20=EC=9E=90=EC=82=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=80=EC=82=AC=EA=B0=80=20gitignore=20=EB=90=9C?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=EB=A5=BC=20=EA=B1=B4=EB=84=88=EB=9B=B4?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이 검사는 저장소 폴더 전체를 훑으면서 git 추적 여부를 보지 않았다. 그래서 커밋할 생각이 없는 로컬 작업 파일(참고자료 스크린샷 등)이 있으면 라이선스 유출로 신고됐다. CI 는 clone 한 트리를 보므로 통과하고 로컬만 빨간불이 뜨는 상태가 되는데, 이러면 개발자가 게이트 결과를 믿지 않게 된다. gitignore 된 파일은 정의상 배포물에 들어가지 않으므로 유출일 수 없다. `git check-ignore` 로 걸러낸다. 면제는 딱 거기까지다 — 추적되지 않았을 뿐 gitignore 되지도 않은 파일은 곧 커밋될 수 있으므로 여전히 검사한다. git 이 없거나 저장소가 아니면 아무것도 면제하지 않고 예전처럼 전부 검사한다. `-z` 는 필수다. 없으면 git 이 비ASCII 경로를 따옴표로 감싸고 8진 이스케이프해 내보내(예: "docs/\354\235\274\354\240\225.png") 원래 경로와 대조되지 않는다. 한글 경로가 많은 이 저장소에서는 무시 설정이 통째로 먹히지 않는 결과가 된다. 회귀 방지 테스트에 한글 경로 케이스를 명시적으로 넣었다. 함께: 대회 주최측 오리엔테이션 자료(제3자 저작물)를 .gitignore 에 추가. 재배포 대상이 아니다. 테스트 5개 추가 (403 → 408). --- .gitignore | 3 ++ scripts/check_assets_separation.py | 41 +++++++++++++++- tests/test_assets_separation.py | 75 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01692ee..cd7da9a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ dev-only/ # Internal working handoff (not a public artifact) HANDOFF.md .gstack/ + +# 대회 주최측이 배포한 오리엔테이션 자료 — 제3자 저작물이라 재배포하지 않는다 +docs/contest/ot-2026/ diff --git a/scripts/check_assets_separation.py b/scripts/check_assets_separation.py index 6672f76..009bcd3 100644 --- a/scripts/check_assets_separation.py +++ b/scripts/check_assets_separation.py @@ -15,6 +15,7 @@ from __future__ import annotations import re +import subprocess from pathlib import Path PICTOGRAM_EXTS = {".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"} @@ -40,12 +41,50 @@ def _iter_files(root: Path): yield path +def _gitignored(root: Path, paths: list[Path]) -> set[Path]: + """``paths`` 중 git 이 무시하는 것들의 집합. + + 이 검사가 지키려는 것은 배포되는 트리의 라이선스 분리다. gitignore 된 파일은 + 정의상 배포물에 들어가지 않으므로 유출일 수 없다 — 로컬 참고자료 스크린샷 + 따위를 유출로 신고하면 거짓 경보가 나고 게이트가 무뎌진다. + + git 이 없거나 저장소가 아니면 **빈 집합**을 돌려준다. 즉 아무것도 면제되지 + 않고 예전처럼 전부 검사한다 — 폴백이 게이트를 끄면 안 된다. + """ + if not paths: + return set() + try: + # -z 는 필수다. 이것 없이는 git 이 비ASCII 경로를 따옴표로 감싸고 + # 8진 이스케이프해서 내보내(예: "docs/\354\235\274\354\240\225.png") + # 원래 경로와 대조가 되지 않는다 — 한글 경로가 조용히 전부 어긋난다. + # -z 를 주면 입출력 모두 NUL 구분이 되고, 공백·개행이 든 경로도 안전하다. + proc = subprocess.run( + ["git", "-C", str(root), "check-ignore", "-z", "--stdin"], + input="\0".join(str(p) for p in paths), + capture_output=True, + text=True, + timeout=60, + ) + except (OSError, subprocess.SubprocessError): + return set() + # 0 = 일부가 무시됨, 1 = 무시되는 것 없음. 그 밖(128 = git 저장소 아님 등)은 + # 판단 불가로 보고 아무것도 면제하지 않는다. + if proc.returncode not in (0, 1): + return set() + return {Path(entry) for entry in proc.stdout.split("\0") if entry} + + def find_asset_leaks(repo_root: Path | str) -> list[str]: root = Path(repo_root).resolve() assets_dir = root / "assets" leaks: list[str] = [] - for path in _iter_files(root): + candidates = list(_iter_files(root)) + ignored = _gitignored(root, candidates) + + for path in candidates: + if path in ignored: + continue rel = path.relative_to(root) in_assets = assets_dir in path.parents diff --git a/tests/test_assets_separation.py b/tests/test_assets_separation.py index 7bbaf06..c10c45e 100644 --- a/tests/test_assets_separation.py +++ b/tests/test_assets_separation.py @@ -1,10 +1,25 @@ +import subprocess from pathlib import Path +import pytest + from scripts.check_assets_separation import find_asset_leaks ROOT = Path(__file__).resolve().parent.parent +def _git_repo(tmp_path: Path, gitignore: str) -> Path: + """`.gitignore` 를 가진 최소 git 저장소를 만든다. git 이 없으면 skip.""" + try: + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True, + capture_output=True, timeout=30) + except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover + pytest.skip(f"git unavailable: {exc}") + (tmp_path / ".gitignore").write_text(gitignore, encoding="utf-8") + (tmp_path / "assets").mkdir(exist_ok=True) + return tmp_path + + def test_assets_dir_exists_with_readme(): assert (ROOT / "assets").is_dir() readme = (ROOT / "assets" / "README.md").read_text(encoding="utf-8") @@ -108,3 +123,63 @@ def test_skip_dirs_use_relative_parts(tmp_path): assert not any("output.svg" in p for p in leaks) # scripts/logo.png MUST appear assert any("logo.png" in p for p in leaks) + + +# --------------------------------------------------------------------------- +# gitignore 존중 (2026-07-29) +# +# 이 스캐너가 지키려는 것은 "배포되는 트리에서 CC BY-SA 픽토그램과 Apache 코드가 +# 섞이지 않는가" 다. gitignore 된 파일은 정의상 배포물에 들어가지 않는다 — +# 참고자료 스크린샷, 내려받은 자료 같은 로컬 작업 파일까지 유출로 신고하면 +# 거짓 경보가 나고, 진짜 유출과 구분이 안 되어 게이트가 무뎌진다. +# --------------------------------------------------------------------------- + + +def test_gitignored_pictogram_is_not_a_leak(tmp_path): + """gitignore 된 이미지는 배포되지 않으므로 유출이 아니다.""" + repo = _git_repo(tmp_path, "docs/scratch/\n") + (repo / "docs" / "scratch").mkdir(parents=True) + (repo / "docs" / "scratch" / "reference-shot.png").write_bytes(b"\x89PNG\r\n") + assert find_asset_leaks(repo) == [] + + +def test_tracked_pictogram_outside_assets_is_still_a_leak(tmp_path): + """gitignore 되지 않은 이미지는 여전히 유출이다 — 게이트를 무디게 하지 않는다.""" + repo = _git_repo(tmp_path, "docs/scratch/\n") + (repo / "ttobak").mkdir() + (repo / "ttobak" / "stray_glyph.svg").write_text("", encoding="utf-8") + leaks = find_asset_leaks(repo) + assert any("stray_glyph.svg" in p for p in leaks) + + +def test_gitignored_data_uri_is_not_a_leak(tmp_path): + """규칙 2(base64 인라인)도 같은 기준을 따른다.""" + repo = _git_repo(tmp_path, "ttobak/generated/\n") + (repo / "ttobak" / "generated").mkdir(parents=True) + (repo / "ttobak" / "generated" / "out.html").write_text( + '\n', encoding="utf-8" + ) + assert find_asset_leaks(repo) == [] + + +def test_non_git_directory_still_scans_everything(tmp_path): + """git 저장소가 아니면 예전처럼 전부 검사한다(폴백이 게이트를 끄면 안 된다).""" + (tmp_path / "assets").mkdir() + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "shot.png").write_bytes(b"\x89PNG\r\n") + leaks = find_asset_leaks(tmp_path) + assert any("shot.png" in p for p in leaks) + + +def test_gitignored_non_ascii_path_is_matched(tmp_path): + """한글 등 비ASCII 경로도 정확히 대조된다. + + `git check-ignore` 는 -z 없이 실행하면 비ASCII 경로를 따옴표로 감싸고 8진 + 이스케이프해서 출력한다("docs/\\354\\235\\274...png"). 그러면 원래 경로와 + 매칭에 실패해 무시 설정이 조용히 통째로 먹히지 않는다 — 한글 경로가 많은 + 이 저장소에서는 사실상 기능이 없는 것과 같다. + """ + repo = _git_repo(tmp_path, "문서/자료/\n") + (repo / "문서" / "자료").mkdir(parents=True) + (repo / "문서" / "자료" / "배점표.png").write_bytes(b"\x89PNG\r\n") + assert find_asset_leaks(repo) == []