From 76a06f2ff13a1a8d870932e6d3a3ceddf84bff21 Mon Sep 17 00:00:00 2001 From: changukyu Date: Sat, 8 Aug 2026 09:49:55 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20CLAUDE.md=20/=20PROJECT=5FMAP=20?= =?UTF-8?q?=EC=8B=A0=EC=84=A4,=207/11=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=AC=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세션 진입 문서가 없어 41.6k 라인 · 70모듈 구조를 매번 재발견하고 있었다. - CLAUDE.md: 진입점 + 이 프로젝트에만 해당하는 규칙(폐쇄망 의존성 금지, app-*.js 로드 순서 계약, SCORER_VERSION 갱신 의무, 직렬화 라운드트립 불변식, LLM 은 실모델로 프로브, cp949 비ASCII 출력 주의) - docs/PROJECT_MAP.md: 파일·라우트·스키마 위치, app-*.js 로드 순서, 알려진 부채 - README: verify 명령, app-*.js 구조, 인증 스로틀 동작 설명 7/11 리메디에이션 체크리스트를 코드와 대조해 갱신했다. 미체크 항목 중 6건은 이미 수정돼 있었고(naver 멀티페이지 쿼터, #10 Origin/Referer CSRF, env_file BOM, daily-limit fail-open, __main__ 경로 앵커링, 그리고 Batch 8 전체가 Batch 6 과 중복) 각 줄에 확인 근거를 남겼다. 남은 미체크는 전부 근거가 기재된 명시적 연기다. --- CLAUDE.md | 36 ++++ README.md | 27 ++- docs/INDEX.md | 2 + docs/PROJECT_MAP.md | 177 ++++++++++++++++++ .../2026-07-11-remediation-checklist.md | 37 ++-- 5 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/PROJECT_MAP.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cc8e2ad --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# POSA Copyrighter + +이미지 저작권 위험 자동 분석 + 운영자 판정 콘솔. **폐쇄망(air-gapped) 납품 대상.** + +## 먼저 읽기 + +`docs/PROJECT_MAP.md` (구조·라우트·스키마 위치) → `docs/INDEX.md` (작업 이력). +전체 소스 재스캔 금지 — MAP 에서 위치를 찾고 필요한 부분만 연다. + +## 검증 + +```powershell +.\verify.ps1 # ruff + pytest. 커밋 전 게이트, CI 도 이 명령 하나 +``` + +모든 패치를 적용한 뒤 **마지막에 1회**만 실행한다. + +## 이 프로젝트의 규칙 + +- **폐쇄망 우선**: 새 런타임 의존성 추가는 기본적으로 금지. 필요하면 오프라인 휠 반입 절차 + (`docs/operations/offline-install.md`)까지 함께 처리한다. CDN·원격 fetch·자동업데이트 금지. +- **`app-*.js` 로드 순서는 계약**: 클래식 스크립트 15개가 전역 스코프를 공유한다. 추가·제거· + 재배치 시 `index.html` 과 `test_static_workbench.py` 의 `APP_SCRIPT_NAMES` 를 함께 고친다. +- **점수를 바꾸면 `SCORER_VERSION` 을 올린다** (`analysis/risk_scoring.py`). 이 스탬프가 + 가중치 변경과 스코어링 버그를 사후 구분하는 유일한 근거다. +- **직렬화 라운드트립은 원 점수를 재현해야 한다** (`store_serialization.py`). 과거에 + `result_url` → `url` 별칭이 리스코어 시 0점을 45점으로 바꾼 사고가 있었다. +- **LLM 은 요약기가 아니라 증거 판독기**다. 인용 없는 claim 은 폐기한다. 프롬프트나 모델을 + 바꾸면 가짜 transport 가 아니라 **실모델로 프로브**한다 — 형식 예시 반향 결함은 실모델에서만 + 드러났다. +- **비ASCII 출력 주의**: 콘솔이 cp949 라 em-dash 등이 `UnicodeEncodeError` 로 프로세스를 + 죽인 이력이 있다. 스크립트 stdout 에 넣지 말 것. + +## 커밋 + +의미 있는 묶음으로 1커밋 → `docs/INDEX.md` 에 결과 기록. 원격은 Forgejo(`tea` CLI, `gh` 아님). diff --git a/README.md b/README.md index d437f1e..ea9bbb0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ python run_copyrighter_server.py --host 127.0.0.1 --port 9500 기본값으로 실행하면 `http://127.0.0.1:9500` 에서 운영자 콘솔이 열립니다. > 보안: `COPYRIGHTER_AUTH_TOKEN`이 비어 있으면 데이터 라우트가 **인증 없이** 열립니다. 단일 호스트 개발 환경 외에는 반드시 토큰을 설정하세요. +> +> 토큰을 설정하면 **틀린 토큰을 제시한 시도**는 클라이언트 주소별로 60초당 10회로 제한되고, 초과 시 `429`와 `Retry-After`를 반환합니다. 토큰을 아직 입력하지 않은 요청(콘솔 최초 로드)은 시도로 세지 않으므로 정상 운영자가 잠기지 않습니다. ## 프로젝트 구조 @@ -55,17 +57,32 @@ src/rights_filter/ 분석·서버·통합·도메인 로직 (Python) server/ http.server 기반 API + 정적 파일 서빙, SQLite 저장소 analysis/ 이미지 분석·위험도 산출 integrations/ Naver / Google / LLM 어댑터 -web/operator-gui/ 운영자 콘솔 (정적 SPA: index.html, app.js, styles.css) +web/operator-gui/ 운영자 콘솔 (정적 SPA: index.html, app-*.js, styles.css) tests/ pytest 테스트 operator_gui/ 운영자 GUI 정적 검증 + Playwright E2E docs/ 기획·설계·운영 문서 ``` -## 테스트 +콘솔 스크립트는 관심사별로 `app-*.js` 15개로 나뉘어 있고, **`index.html` 의 로드 순서가 +의미를 갖습니다**(클래식 스크립트가 하나의 전역 스코프를 공유). 순서와 목록은 +[`docs/PROJECT_MAP.md`](docs/PROJECT_MAP.md) 에 있으며 정적 테스트가 이를 고정합니다. + +## 검증 / 테스트 + +커밋 전 게이트는 **단일 명령**입니다(CI 도 같은 명령을 호출합니다). ruff 린트와 pytest 전체를 +순서대로 실행합니다. + +```powershell +.\verify.ps1 # 린트 + 테스트 전체 +.\verify.ps1 -SkipLint # 빠른 내부 루프 (테스트만) +.\verify.ps1 -Coverage # src/ 커버리지 리포트 포함 +``` + +개별 실행이 필요할 때: ```bash -# 전체 -python -m pytest +python -m pytest # 또는 그냥 pytest — pytest.ini 가 경로를 설정합니다 +python -m ruff check src tests # 설정: ruff.toml # 운영자 GUI E2E (Playwright) playwright install chromium # 빌드 머신에서 1회 — 브라우저 바이너리는 오프라인 번들에 포함 @@ -77,6 +94,8 @@ python -m pytest tests/operator_gui/ ## 문서 +- [프로젝트 구조 지도 (PROJECT_MAP)](docs/PROJECT_MAP.md) — 파일·라우트·스키마 위치, 로드 순서, 알려진 부채 +- [작업 이력 (INDEX)](docs/INDEX.md) - [프로젝트 소개 및 기술 구현](docs/project-introduction-and-technical-implementation.md) - [운영 워크리스트](docs/operations/copyrighter-operation-worklist.md) - [오프라인 설치 절차](docs/operations/offline-install.md) diff --git a/docs/INDEX.md b/docs/INDEX.md index 1f48802..86538df 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,5 +1,7 @@ # Project INDEX +- 2026-08-08 · 프로젝트 분석 후 인프라·구조·보안 정비 — **원격 백업 복구**(20커밋이 로컬에만 있었음: `main` 2 + 작업브랜치 18 → Forgejo push), **단일 verify 게이트 신설**(`verify.ps1` + `pytest.ini` + `ruff.toml`, ruff 0.12.0 dev 핀; 린트 18건 정리), **`app.js` 5,428줄 → `app-*.js` 15개 분할**(순수 라인범위 추출로 바이트 단위 라운드트립 검증, 로드 순서를 정적 테스트로 고정), **보안 2종 실수정**(SSRF DNS-rebinding TOCTOU — 단일 DNS 해석 후 그 답변으로만 연결하는 `_guarded_create_connection`; 실패 인증 스로틀 — 자격증명 제시 실패만 10회/60초, `429`+`Retry-After`), 7/11 체크리스트 재확인(6건은 이미 수정돼 있던 낡은 항목, Batch 8 은 Batch 6 과 중복이라 정리), `CLAUDE.md`·`docs/PROJECT_MAP.md` 신설. E2E 플레이크(업로드 대기 10초 → `UPLOAD_ANALYSIS_TIMEOUT_MS` 30초) 해소. **pytest 562 passed**(착수 전 533/1 failed), 린트 클린. 실렌더 확인: 큐·케이스 심사 정상 렌더, 스크립트 15개 전부 200, 새로고침 후 콘솔 메시지 0 + - 2026-07-24 · 멀티에이전트 프로젝트 점검(에이전트 109, 적대적 검증 포함) — pytest 377/379, 런타임 스모크 9/9 통과, 신규 CONFIRMED 43건(HIGH 7) + 7/11 체크리스트 중 3건 실수정 확인 + 기능 제안 상위 5 선정 → [docs/reviews/2026-07-24-project-review.md](reviews/2026-07-24-project-review.md) - 2026-07-26 · 운영자 콘솔 실렌더 검증 + 결함 5종 수정 (`d20e6d6`) — 서버 기동 불가(cp949에서 em-dash `UnicodeEncodeError`로 프로세스 사망, 납품 환경 치명) · 요약이 모델이 못 본 78건을 "연결 근거"로 주장(B단계가 절반만 막았음) · `grounded/droppedClaims` 미영속화 · app.js가 서버 그룹 분류를 자체 매핑으로 덮어씀 · 기본 모델이 클라이언트/배선에서 불일치(미설치 모델 지목). 실렌더 확인: 출처 표기 그룹 자체 섹션 렌더, `[E1,E2...]` 인용 2단 요약, `sourceIds` 78→10, qwen2.5:7b 6.0초. **pytest 534 passed** - 2026-07-25 · C단계: 출처 신호 2종 (`5a9fcc0`) — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드, GPS·시리얼·Make/Model 미판독) + 방송캡처 포렌식(레터박스·자막밴드·프레임크기, 독립 카테고리 2개 이상). 하나의 `EvidenceSource.PROVENANCE`·하나의 GUI 그룹으로 배관 12곳 비용 1회 지불. **둘 다 0점 라벨**(캘리브레이션 전). 실측: 실제 이미지 6장에서 캡처 오탐 0·권리메타 히트 0(정밀도 확인, 가치는 이 코퍼스에서 미입증). XMP/C2PA는 `defusedxml` 미반입으로 제외. **pytest 531 passed** diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md new file mode 100644 index 0000000..762a4db --- /dev/null +++ b/docs/PROJECT_MAP.md @@ -0,0 +1,177 @@ +# PROJECT MAP + +> 어디에 무엇이 있는지에 대한 참조 문서. **작업 전 여기서 위치를 찾고 필요한 부분만 연다** +> (전체 소스 재스캔 금지). 새 파일·라우트·스키마가 생기면 해당 섹션을 갱신한다. +> +> 무엇을 했는지의 기록은 [INDEX.md](INDEX.md)에 있다. 이 문서는 **현재 구조**만 다룬다. + +## 한 줄 요약 + +이미지 저작권 위험을 자동 분석하고, 운영자가 콘솔에서 근거를 확인해 승인/보류/반려를 +판정하는 폐쇄망(air-gapped) 도구. Python 3.13 표준 `http.server` + SQLite + 정적 SPA. +런타임 서드파티는 이미지 처리 3종(`numpy`/`opencv-python-headless`/`pillow`)뿐. + +## 검증 (단일 명령) + +```powershell +.\verify.ps1 # ruff + pytest 전체 — 커밋 전 게이트, CI도 이 명령 +.\verify.ps1 -SkipLint # 빠른 내부 루프 +.\verify.ps1 -Coverage # src/ 커버리지 포함 +``` + +설정은 `pytest.ini`(testpaths·pythonpath·timeout)와 `ruff.toml`(py313·line-length 120)에 있다. + +## 실행 + +```powershell +python run_copyrighter_server.py # 기본 127.0.0.1:9500 +``` + +런처가 `PYTHONPATH=src`와 작업 디렉터리를 고정한다. 옵션: `--host --port --db --images --static --env`. + +--- + +## 소스 지도 (`src/rights_filter/`) + +### `analysis/` — 이미지 분석과 위험도 산출 + +| 파일 | 역할 | +|---|---| +| `preprocessing.py` | 디코드, 외부 전송용 파생물 생성(메타데이터 제거), 얼굴 크롭 파생물 | +| `fingerprints.py` | 이미지 지문. `_perceptual_hash`는 **DCT pHash가 아니라 8x8 평균해시(aHash)** — 이름과 실제가 다름(알려진 부채) | +| `face_person_detection.py` | YuNet(ONNX) 얼굴 검출 + 인물 등급(`identifiable`/`background`). 가중치는 `models/`(gitignore) | +| `internal_analyzer.py` | 내부 분석 오케스트레이션 | +| `risk_scoring.py` | `RiskScorer.score()` — 점수·밴드 산출. **`SCORER_VERSION` 은 가중치/버킷/밴드 변경 시 반드시 올린다** | +| `evidence_normalizer.py`, `evidence_enrichment.py` | 증거 정규화·보강 | +| `search_query_generation.py`, `search_result_promoter.py` | 자동 검색 쿼리 생성, 검색 결과의 증거 승격 | +| `llm_assistance.py`, `llm_prompts.py` | LLM을 **요약기가 아닌 증거 판독기**로 사용. 입력 화이트리스트 9필드 + 상위 12건 상한, `format=json` 구조화 인용, 라벨 집합 포함 검사로 미인용 claim 폐기 | +| `provenance.py` | 출처 신호 — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드), 방송캡처 포렌식. **둘 다 현재 0점 라벨**(캘리브레이션 전) | +| `derivatives.py`, `reason_builder.py` | 파생물 기록, 사유 문자열 조립 | + +### `server/` — HTTP + 저장소 + +`http_app.py` (559줄) 가 라우팅·인증·보안 헤더를 담당하고, 저장소는 `CopyrighterStore` +하나를 **믹스인 8개**로 조립한다 (`sqlite_store.py:77`). + +| 파일 | 역할 | +|---|---| +| `http_app.py` | `build_server()`, `CopyrighterHandler`. 라우팅 / Bearer+세션쿠키 인증 / CSP·CSRF / 바디 상한 10MB | +| `auth_rate_limit.py` | 실패한 인증 시도 스로틀(고정 윈도우 10회/60초, 클라이언트 주소별) | +| `sqlite_store.py` | `CopyrighterStore` 조립 + 시드·리스코어 | +| `store_schema.py` | DDL·제약·타입 컬럼 마이그레이션. `SCHEMA_VERSION` | +| `store_persistence.py` | `_put`/`_all` 등 저수준 upsert (PK 충돌 시 update) | +| `store_serialization.py` | 도메인 ↔ DB payload 왕복. **라운드트립은 원 점수를 재현해야 한다**(과거 위반 이력 있음) | +| `store_enrichment.py` | provider 상태 동기화, 얼굴 증거/크롭, 재분석, 자동 Naver/Google 검색 드라이버 | +| `store_operations.py` | 운영자 액션(판정·메모·재분석 요청) | +| `store_workflow.py` | 일괄 판정(`MAX_BATCH_DECISIONS`), 워크플로 | +| `store_search_candidates.py` | 검색 후보 수집·유사도 비교 | +| `store_page_scrape.py` | 후보 페이지/스타일시트 파싱 | +| `store_remote_fetch.py` | **네트워크 경계**. SSRF 가드 — 사전 URL 검사 + `_guarded_create_connection`(단일 DNS 해석으로 rebinding TOCTOU 차단) + 리다이렉트 홉 재검증 | +| `store_queue.py`, `store_duplicates.py`, `store_backup.py`, `store_text.py`, `store_url_utils.py`, `store_constants.py` | 큐, 지문 중복, 백업/복구, 텍스트·URL 유틸, 상수 | +| `image_store.py` | 제출 이미지 파일 저장소 + manifest | +| `env_file.py` | `.env` 로더 (utf-8-sig — BOM이 첫 키에 붙지 않도록) | + +### `integrations/` — 외부 어댑터 + +`naver_search.py`, `google_custom_search.py`, `cloud_vision_web_detection.py`, +`web_detection_result_mapper.py`, `http_json.py`. +쿼터/정책은 `search_policy.py`(`SearchApiPolicy` — `try_reserve`/`release` 로 원자적 예약)와 +`external_policy.py`(`ExternalApiPolicy`). **둘 다 날짜 경계 윈도우 리셋 있음**. +`env_clients.py` 가 `.env` → 어댑터/정책 조립(`build_provider_runtime`). + +### `domain/` · `governance/` · `jobs/` · `admin/` + +- `domain/records.py` — `Evidence`, `EvidenceSource`, `ScoreResult` 등 핵심 레코드 +- `governance/` — 접근·보정·보존 정책 (얼굴 크롭 보존기간 등) +- `jobs/batch_analyzer.py` — 배치 분석, `retry_policy.py`, `usage_limits.py` +- `admin/` — 운영자 리뷰 핸들러·프레젠터 + +--- + +## 운영자 콘솔 (`web/operator-gui/`) + +빌드 스텝 없음. 전부 클래식 스크립트(모듈 아님)라 **하나의 전역 스코프를 공유**한다. + +`index.html` → `styles.css` + 보조 스크립트 4개(`operator-labels.js`, `submission-import.js`, +`evidence-guidance.js`, `operator-search.js`) → **`app-*.js` 15개**. + +### `app-*.js` 로드 순서 (변경 금지 — 순서가 곧 계약) + +``` +app-state.js 상태·라벨 임포트 (나머지 전부가 읽는다) +app-api.js 토큰 프롬프트, fetch 래퍼, bootstrap/refresh +app-format.js 커버리지 계산, HTML/URL 이스케이프, 라벨 포맷터 +app-selectors.js 상태 파생 조회 (선택 케이스, 위험/출처 클래스, 증거 필터) +app-render-queue.js 큐 목록, 임포트 상태, 케이스 리뷰 셸 +app-render-evidence.js 증거 그룹화·정렬·마크업 +app-render-panels.js 지식DB·후보·보정·provider·감사 패널, renderAll +app-navigation.js 뷰/탭 전환, 케이스 선택, 판정, 수동 검색, 재분석 +app-keyboard-batch.js 키보드 심사 모드, 중복 그룹, 일괄 판정 +app-decision-tools.js 메모 프리셋, 판정 내보내기, 백업, cleared reference, LLM 초안 +app-stats.js 판정 통계·캘리브레이션 +app-submissions.js 제출물 리로드/임포트/업로드, 지식 항목 생성 +app-collections.js 후보 수집·기각·선택·승격 +app-admin.js 지식/보정/provider/증거상태 관리 액션 +app-bootstrap.js bindEvents, init, DOM 리스너 ← 유일하게 로드 시점에 실행됨 +``` + +이 순서는 `tests/operator_gui/test_static_workbench.py` 의 +`test_index_loads_every_app_part_exactly_once_in_declared_order` 가 고정한다. +파일을 추가·제거·재배치하면 그 테스트의 `APP_SCRIPT_NAMES` 와 `index.html` 을 함께 고친다. + +--- + +## HTTP 라우트 (`http_app.py`) + +인증: `COPYRIGHTER_AUTH_TOKEN` 설정 시 `/api/*` 와 미디어 라우트는 Bearer 헤더 또는 세션 쿠키 +필요. 정적 셸과 `/health` 는 열려 있다(운영자가 토큰을 입력할 수 있어야 하므로). + +**GET** — `/health` · `/api/bootstrap` · `/api/review-queue` · `/api/submissions/{id}/review` · +`/api/providers` · `/api/providers/health` · `/api/audit-events` · `/api/decision-memo-presets` · +`/api/decisions/export` · `/api/admin/backups` · `/api/stats/decisions` · +`/media/` · `/knowledge-media/` · `/collected-media/` · `/face-crop-media/` + +**POST** — `/api/submissions/{id}/search-auto|rerun-enrichment|decision|clear-reference` · +`/api/submissions/reload|import-folder|upload-image|decision-batch` · +`/api/evidence/{id}/status` · `/api/search/manual` · `/api/knowledge/manual` · +`/api/knowledge/{id}/promote-watchlist|exclude-watchlist|deactivate|reactivate` · +`/api/collections/keyword` · `/api/collections/candidates/promote-batch` · +`/api/collections/candidates/{id}/promote|dismiss` · +`/api/decision-memo-presets` (+ `/{id}/delete`) · `/api/admin/backup` · +`/api/providers/emergency-disable` + +**PATCH** — `/api/knowledge/{id}` + +--- + +## SQLite 스키마 (`store_schema.py`, `SCHEMA_VERSION = 2`) + +제약 있는 테이블 7개 — `submissions`, `evidence`, `providers`, `knowledge_entries`, +`collection_candidates`, `corrections`, `audit_events`. 추가로 `submission_queues`. + +각 테이블은 조회·정렬용 타입 컬럼 + 전체 도메인 객체를 담은 `payload` JSON 컬럼 구조. +`evidence.submission_id` 는 `submissions(id)` 에 `on delete cascade`. +WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore). + +--- + +## 테스트 (`tests/`, 562건) + +- `tests/rights_filter/` — 단위·통합. `conftest.py` 가 `src` 를 sys.path 에 넣는다 +- `tests/operator_gui/` — 정적 검증 + Playwright E2E. + - Page Object: `pages/workbench_page.py` (`OperatorWorkbench`). 업로드 대기는 + `UPLOAD_ANALYSIS_TIMEOUT_MS`(30초) — 업로드는 서버에서 실분석을 돌리므로 일반 대기보다 길다 + - Chromium 없으면 **자동 skip**(`importorskip`) +- 정적 테스트는 `app-*.js` 전체를 이어붙여(`_read_app()`) 내용을 검사한다 — + 어느 파일에 있는지가 아니라 콘솔에 그 동작이 있는지를 본다 + +--- + +## 알려진 부채 + +| 항목 | 위치 | 비고 | +|---|---|---| +| `_perceptual_hash` 가 pHash가 아닌 aHash | `analysis/fingerprints.py:47` | 균일/저디테일 이미지 유사도 오탐. 교체 시 0.9 임계 재보정 필요 | +| src 4파일이 800줄 초과 | `store_page_scrape.py`(972) `store_operations.py`(968) `sqlite_store.py`(904) `store_enrichment.py`(853) | 저장소 코어라 분할 리스크 있음. 별도 작업으로 | +| 출처 신호 2종 0점 | `analysis/provenance.py` | 캘리브레이션 전. 실측 코퍼스에서 가치 미입증 | +| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 | diff --git a/docs/reviews/2026-07-11-remediation-checklist.md b/docs/reviews/2026-07-11-remediation-checklist.md index d4a4b3a..766a555 100644 --- a/docs/reviews/2026-07-11-remediation-checklist.md +++ b/docs/reviews/2026-07-11-remediation-checklist.md @@ -3,6 +3,11 @@ > 기준 리뷰: `docs/reviews/2026-07-11-project-review.md` (HEAD 89c1fe4) > 신규 HIGH 9 + 유닛 추가 HIGH 3(통합/프론트) + MEDIUM ~28 + LOW ~14. > 배치는 risk-per-effort 순. 각 배치 후 `pytest tests/rights_filter -q` 통과 확인. +> +> **2026-08-08 재확인**: 미체크로 남아 있던 항목을 코드와 대조했다. 6건은 이미 수정돼 있었고 +> (체크리스트가 낡았던 것 — 각 항목에 확인 근거 기재), 보안 2건(#8 rate limit, SSRF +> DNS-rebinding TOCTOU)은 실제로 열려 있어 이번에 수정했다. 남은 미체크는 전부 **명시적 연기** +> 이며 각 줄에 근거가 있다. ## Batch 1 — 스코어링 파이프라인 복구 (최우선) ✅ 완료 직렬화 라운드트립 필드 손실 1개 패턴이 HIGH 3건을 만듦. 프로모터 3건은 배포 즉시 노출되므로 동일 배치. @@ -46,8 +51,8 @@ - [x] LOW `store_remote_fetch.py` — SSRF `not is_global`(CGNAT 등 특수대역 차단) - [x] LOW `env_clients.py` — NAVER display 3종 클램프(1~100) - [x] M `env_clients.py` — daily-limit malformed → 보수적 100(fail-open 제거) + 테스트 갱신 -- [ ] M `naver_search.py` — 멀티페이지 쿼터 실제 요청 수 (미착수) -- [ ] M `store_remote_fetch.py` — SSRF DNS-rebinding TOCTOU (미착수 — 커스텀 opener 필요) +- [x] M `naver_search.py` — 멀티페이지 쿼터 실제 요청 수 (2026-08-08 확인: `_requested_calls` + `try_reserve(requested_calls=…)`/`release` 로 이미 구현돼 있었음. 항목이 낡았던 것) +- [x] M `store_remote_fetch.py` — SSRF DNS-rebinding TOCTOU (2026-08-08 수정: `_guarded_create_connection` 이 getaddrinfo 를 **1회만** 수행하고 그 답변의 모든 주소를 검증한 뒤 그중 하나로만 연결. `_Guarded*Connection`/`*Handler` 로 opener 에 배선 — 사전검사와 실제 다이얼의 해석이 갈라지지 않는다. 리바인딩 회귀 테스트 포함 14건) - [ ] LOW: EUC-KR 스니핑, dead code 삭제(레이아웃 테스트가 고정 → 유지) ## Batch 5 — HTTP 보안 ✅ (M10/M11/#6) @@ -56,8 +61,8 @@ - [x] #6 앱셸 CSP/X-Frame-Options/nosniff + 테스트 - [x] #8(부분) 토큰 길이<32 경고 + static 부재 경고(`__main__`) - [ ] #7 쿠키 분리+Secure (연기 — Secure는 localhost HTTP서 쿠키 미전송→인증 붕괴; HTTPS 전제 필요) -- [ ] #8 rate limit (미착수) -- [ ] #10 Origin/Referer CSRF 폴백 (미착수) +- [x] #8 rate limit (2026-08-08 수정: `server/auth_rate_limit.py` — **틀린 자격증명을 제시한** 시도만 클라이언트 주소별 고정윈도우 10회/60초로 제한, 초과 시 `429` + `Retry-After`. 성공하면 카운터 초기화. 토큰 미입력 요청(콘솔 최초 로드가 여러 건 발생시킴)은 세지 않으므로 정상 운영자가 잠기지 않는다. 테이블은 1024 클라이언트로 상한. 테스트 12건) +- [x] #10 Origin/Referer CSRF 폴백 (2026-08-08 확인: `_origin_ok()` 로 이미 구현돼 있었음 — 7/24 리뷰가 지적한 낡은 항목) ## Batch 6 — 프론트엔드 ✅ - [x] H8 이미지 뷰어 버튼 구현 + 케이스별 리셋 @@ -92,16 +97,22 @@ - [ ] #20 ScoreResult.reasons tuple (연기 — 테스트 다수 `== []` 단언 충돌) ## Batch 7 — env/config/docs -- [ ] `env_file.py:12` — UTF-8-sig BOM -- [ ] daily-limit fail-open → 보수적 기본 -- [ ] `env_clients.py:239` — LLM assistant OLLAMA_* 게이팅 -- [ ] `__main__.py` — 경로 ROOT 앵커링 + static 부재 경고 +- [x] `env_file.py:12` — UTF-8-sig BOM (2026-08-08 확인: `read_text(encoding="utf-8-sig")` 로 이미 적용됨) +- [x] daily-limit fail-open → 보수적 기본 (2026-08-08 확인: `_daily_limit()` 이 malformed 값에 warning + 100 적용. Batch 4 의 동일 항목과 중복 기재였음) +- [ ] `env_clients.py:239` — LLM assistant OLLAMA_* 게이팅 (연기 — env={} 로 LLM 을 기대하는 테스트 다수가 의존) +- [x] `__main__.py` — 경로 ROOT 앵커링 + static 부재 경고 (2026-08-08 확인: `6ab6da3` 에서 적용됨) - [ ] offline-install.md — Ollama/Playwright/lock 해시 - [ ] python 3.13 가드, .gitignore, package.json 정리 ## Batch 8 — 에러핸들링/기타 백로그 -- [ ] #14 무음 예외 로깅 -- [ ] #16 skip 사유 reasons 오염 -- [ ] #19 EXIF/GPS PIL 폴백 경고 -- [ ] #20 ScoreResult.reasons tuple -- [ ] #18 dedup 핫패스 타깃 쿼리 + +> **2026-08-08: 이 배치는 Batch 6 과 중복된 착수 전 계획 목록이었다.** #14·#16·#18·#19 는 +> Batch 6 에서 이미 처리돼 [x] 로 기재돼 있고, #20 은 같은 곳에 연기 근거와 함께 남아 있다. +> 여기서 중복 추적하지 않는다 — 상태는 Batch 6 이 기준. + +- [x] #14 무음 예외 로깅 → Batch 6 참조 (잔여 swallow 는 Batch 6 에 별도 미체크로 남음) +- [x] #16 skip 사유 reasons 오염 → Batch 6 참조 +- [x] #19 EXIF/GPS PIL 폴백 경고 → Batch 6 참조 (테스트 실행 로그에서 실제 warning 발생 확인: + `preprocessing.py:52 external derivative fell back to original bytes…`) +- [x] #18 dedup 핫패스 타깃 쿼리 → Batch 6 참조 +- [ ] #20 ScoreResult.reasons tuple → Batch 6 의 연기 항목과 동일 건