사후 패치 검증 (Post-Patch Validation)
보안 패치를 보고된 버그와 그 영향 범위 내의 코드를 대상으로 검증합니다. 패치 작성자에게 재현 가능한 실패 지점을 제공하여 수정해야 할 부분을 명확히 하고, 여전히 테스트가 필요한 영역을 식별합니다. 인간과 에이전트가 생성한 패치 모두에 동일한 검증 기준을 적용합니다. diff, 작성자, 업스트림 구현, 원본 개념 증명(PoC)만으로는 정확성을 확립할 수 없습니다.
사용 시기
- 보안 수정, 수정 커밋, 패치 파일 또는 풀 리퀘스트가 이미 존재할 때.
- AI가 생성한 패치가 인간의 검토 또는 병합 전에 검증이 필요할 때.
- 수정이 하나의 익스플로잇 경로만 다루고 동일한 근본 원인의 변종을 놓칠 수 있을 때.
- 보안 수정이 정당한 동작을 변경하거나 새로운 취약점을 도입할 수 있을 때.
- 패치 작성자가 다음 수정 전에 구체적인 실패 지점과 커버리지 격차를 필요로 할 때.
사용하지 말아야 할 때
- 아직 패치가 존재하지 않는 경우: 대신 취약점 발견 또는 수정 구현을 먼저 수행하세요.
- 패치 증거를 실행하지 않고 감사 결과를 보고서와 비교 검토하는 작업인 경우.
- 작업이 단순히 발견점을 영구 프로젝트 테스트로 변환하는 것일 때.
- 대상이 원격 또는 프로덕션 환경일 때. 이 스킬은 로컬 코드와 테스트만 실행합니다.
- 사용자가 리포지토리의 코드 또는 테스트 스위트 실행을 승인하지 않은 경우.
빠른 시작
-
취약한 기준 버전과 패치된 입력을 고정(pin)합니다. 불변 커밋(immutable commit)을 선호합니다. 커밋되지 않은 작업의 경우 먼저 바이너리 패치 파일을 생성하세요. 사용자의 작업 트리에서 검증을 수행하지 마십시오.
-
고정된 계획을 위한 스캐폴드(Scaffold)를 생성합니다:
uv run {baseDir}/scripts/post_patch_validation.py scaffold \ --repo . \ --base-ref <vulnerable-ref> \ --patched-ref <patched-ref> \ --finding-id <stable-id> \ --finding-summary "<root cause and impact>" \ --evidence-level runtime \ --output post-patch-validation/plan.json--patched-ref대신--patch-file <path>를 사용하여 패치 아티팩트를 지정할 수 있습니다. 가장 높은 수준의 정직한 증거 수준을 선택하세요:source는 소스/패치 불변 조건만,build는 대상 코드가 컴파일되거나 분석되었지만 보고된 동작이 실행되지 않는 경우,runtime은 검사가 보고된 동작과 안전성 어설션을 실행하는 경우입니다. -
발견점, diff, 호출자, 형제 경로, 정리/오류 경로, 기존 테스트를 검사합니다. 생성된 계획에
checks를 채웁니다. 구조적 스키마를 보려면print-schema를 실행하세요:uv run {baseDir}/scripts/post_patch_validation.py print-schema -
코드를 실행하기 전에
validate-plan을 실행하여 커버리지, 명령 제한 및 고정된 입력을 포함한 전체 검증을 수행합니다:uv run {baseDir}/scripts/post_patch_validation.py validate-plan \ --plan post-patch-validation/plan.json -
증거 계획을 실행합니다:
uv run {baseDir}/scripts/post_patch_validation.py run \ --plan post-patch-validation/plan.json \ --output post-patch-validation/results -
result.json,report.md, 증거 수준 및 전체assessment를 보고합니다. 각 발견점을 패치 작성자에게 해당 검사 ID, 어설션 및 저장된 로그와 함께 반환합니다. 지원되는 발견점과 공존하는 격차를 포함하여 각 검증 격차를 별도로 식별합니다. 작성자가 패치를 수정한 후에는 새 입력을 고정하고 새로운 검증 실행을 저장합니다. 이전 증거는 보존합니다. 제공된 검사를 통과하더라도 수락 전에 인간의 검토가 여전히 필요합니다.
증거 계약 (Evidence Contract)
실행기(runner)는 불완전한 계획을 거부합니다. 각 종류에 대해 최소 하나의 검사를 제공하세요:
| 종류(kind) | 필수 관찰 사항 |
|---|---|
control | 무해한(harness) 코드가 기준(base)과 패치(patch) 모두에서 성공 |
exploit | 원본 안전성 어설션이 기준에서는 실패하고 패치에서는 성공 |
variant | 별개의 근본 원인 변종이 기준에서는 실패하고 패치에서는 성공 |
behavior | 영향받지 않은 동작이 바이트 단위로 동일한 선택된 출력으로 성공 |
regression | 대상이 된 비보안 회귀 검사가 두 리비전 모두에서 성공 |
security | 인접/신규 취약점 검사가 기준과 패치 모두에서 성공 |
suite | 기존 프로젝트 스위트, sanitizer 또는 결정론적 퍼즈(fuzz) 캠페인이 패치에서 성공 |
명령어는 argv 배열이며, 절대 셸 문자열이 아닙니다. 복잡한 설정은 체크인되거나 계획 아티팩트 스크립트에 넣고 {plan_dir}으로 호출하세요. 실행기는 로케일/시간대/해시 시드 입력을 고정하고, 검사를 어휘 ID 순서로 실행하며, 원시 stdout/stderr를 기록하고, 원래 작업 트리를 절대 편집하지 않습니다. 각 검사의 timeout_seconds는 기본값 300이며 1에서 3600 사이의 정수를 허용합니다. 시간 초과가 발생하면 검증 격차가 남습니다. 시간 초과만으로는 회귀가 확립되지 않습니다. 모든 계획에는 정렬된 submodules 배열([] when none)도 포함됩니다. 스캐폴딩은 변경된 파일 인벤토리에서 영향을 받는 Gitlink를 유추합니다. 실행기는 .gitmodules 네트워크 URL이 아닌 소스 리포지토리의 기존 Git 모듈 객체에서 해당 고정 커밋을 초기화합니다. 검증 전에 소스 리포지토리에서 초기화하거나 가져오세요(fetch).
익스플로잇 및 변종 검사는 실행되었음을 증명해야 합니다
0이 아닌 종료 코드가 취약점이 재현되었음을 의미하지는 않습니다. 임포트 오류, 빌드 실패, 누락된 종속성, 안전성 어설션 실패는 모두 0이 아닌 종료 코드를 가지며 실행기로는 구별할 수 없습니다. 모든 exploit 및 variant 검사는 두 리비전 모두에서 어설션을 평가하기 직전에 PPV_REACHED를 출력하고 플러시(flush)해야 합니다:
"argv": ["python3", "-c", "import app; value = app.render('<'); print('PPV_REACHED', flush=True); assert value == '<'"]
이 토큰은 환경 변수 PPV_REACHED_MARKER로도 제공됩니다. 자체 행으로 stdout에 기록되어야 합니다. stderr는 스캔되지 않습니다. Python SyntaxError 트레이스백은 문제가 되는 소스를 출력하므로 아무것도 실행하지 않은 하네스에 대해 검사를 만족시킬 수 있기 때문입니다. 이 마커가 없는 실행은 marker_missing 격차를 남깁니다. 다른 검사의 독립적인 발견점은 결과에 계속 남습니다. 명시적으로 플러시하세요: 페이로드가 세그폴트를 일으키거나 _exit를 호출하는 하네스는 버퍼링된 출력을 잃고 자체 증거를 상실합니다.
이러한 검사는 side-blind 방식으로도 실행됩니다. {side}는 이들에 대해 확장되지 않으며, PPV_SIDE는 그들의 환경에 없고, 체크아웃 디렉토리 이름은 무작위로 지정되며, 계획 검증기는 argv 또는 env에서 이를 언급하는 exploit 또는 variant 검사를 거부합니다. 자신이 어떤 리비전에 있는지 알 수 있는 어설션은 코드 대신 그것에 대해 어설션할 수 있으며, 이는 재현 후 수정을 위조하는 가장 쉬운 방법입니다.
환경
검사는 고정된 최소 환경에서 실행됩니다: PATH, HOME, 소수의 임시/사용자 키, 그리고 LANG/LC_ALL=C, TZ=UTC, PYTHONHASHSEED=0, NO_COLOR, TERM=dumb. 호출자의 환경에 있는 다른 모든 것은 삭제됩니다. 더 많은 것이 필요한 툴체인은 명시적으로 얻습니다:
uv run {baseDir}/scripts/post_patch_validation.py run \
--plan post-patch-validation/plan.json \
--output post-patch-validation/results \
--allow-env JAVA_HOME --allow-env CARGO_HOME
전달된 이름과 값은 result.json에 기록됩니다. 요청된 변수가 설정되지 않은 경우 빈 문자열이 아닌 오류입니다. 두 가지 클래스는 절대 거부됩니다: 자격 증명으로 읽히는 이름(*SECRET*, *TOKEN*, *API_KEY*, ...)은 값이 결과에 기록되기 때문이고, 실행 내용을 변경하는 이름(LD_PRELOAD, BASH_ENV, NODE_OPTIONS, GIT_SSH_COMMAND, ...)은 해당 변수가 실행되는 코드를 변경할 수 있기 때문입니다. 실행기의 고정 변수와 모든 PPV_* 이름도 예약되어 있으며 전달될 수 없습니다.
argv 및 검사별 env 값에서 확장되는 플레이스홀더: {checkout} (테스트 중인 리비전), {plan_dir} (해당 호출을 위한 계획 아티팩트의 격리된 복사본), {scratch} (해당 검사 호출을 위한 새 불투명 디렉토리), {side} (base 또는 patched, exploit/variant 검사에는 사용 불가). 동일한 값이 PPV_CHECKOUT, PPV_PLAN_DIR, PPV_SCRATCH, PPV_SIDE, PPV_CASE_ID로 제공됩니다. {scratch} 아래에만 쓰세요; 증거 디렉토리 경로는 검사에 전달되지 않습니다. 기준 및 패치 호출은 실행기 관리 스크래치, 계획 또는 작업 트리 루트를 공유하지 않습니다. 각 호출이 종료된 후 해당 스크래치 트리는 결정론적 경로인 results/scratch/<check-id-and-side> 아래에 보관되고, 개인 계획 복사본은 폐기되며, 파일로 확인되는 모든 읽기 가능한 argv 요소는 argv_files에서 해시됩니다. 격리된 계획 또는 체크아웃 루트 내부의 파일은 추가로 results/helpers/<sha256> 아래에 최대 16MiB까지 보존됩니다; 기록은 다른 파일이 보관되지 않은 이유를 설명합니다. plan.json에 대해 전용 디렉토리를 사용하세요: 그 형제 파일과 디렉토리는 각 호출의 {plan_dir}에 복사됩니다. 헬퍼 코드는 해당 디렉토리의 checks/ 디렉토리 아래에 두거나 대상 리포지토리에 체크인하여 바이트를 검토 가능하게 유지하세요. 커밋 핀, 현재 출력 디렉토리 및 감지된 이전 결과 트리를 포함하는 머신 계획은 제외됩니다; 심볼릭 링크는 거부됩니다. 깨끗한 스냅샷은 실행기 메모리에만 남아 있으며, 익스플로잇/변종 쪽은 무작위 순서로 실행되는 동안 증거 파일 이름은 결정론적으로 유지됩니다. Stdout/stderr은 익명 또는 무작위 이름의 캡처 디스크립터를 사용하고 하위 프로세스가 종료된 후에만 명명된 증거 파일에 복사되므로 fd 검사로는 쪽 레이블을 알 수 없습니다.
이 격리는 호스트 샌드박스가 아닙니다: 검사는 호출자의 권한으로 실행되며 악성 헬퍼는 임의의 외부 상태를 사용하거나 소스 또는 Git 메타데이터에서 의도적으로 리비전을 유추할 수 있습니다. 콘텐츠 주소 지정된 헬퍼 아티팩트를 검사하고, 검사 코드 자체를 신뢰할 수 없는 경우 OS/컨테이너 샌드박스를 사용하세요.
활성 검증 작업 트리는 커널 파일 잠금으로 뒷받침되는 무작위 소유자 토큰으로 Git 잠금이 설정되어 있으므로 다른 동시 검증자가 이를 제거할 수 없고 PID 재사용이 소유자를 가장할 수 없습니다. 실행기가 강제로 종료되면 다음 실행이 오래된 검증자 소유 등록을 잠금 해제합니다. 수동 복구를 위해 git worktree list를 검사한 다음 git worktree unlock <path> 및 git worktree remove --force <path>를 사용하세요(또는 경로가 사라진 후 git worktree prune).
커버리지 설계, 변종 선택, 발견점 및 검증 격차 해석 시 evidence-model.md를 읽으세요. 일상적인 CLI 실행을 위해 읽지 마십시오.
커버리지 규칙
- 변종은 원본 페이로드의 표면적 돌연변이가 아닌 근본 원인에서 파생하세요.
- 수정이 영향을 미치는 형제 호출 사이트, 대체 콜백/출력, 오류 경로, 정리(teardown), 소유권, 직렬화 및 경계 값을 열거하세요.
- 각 익스플로잇 또는 변종 테스트가 안전한 동작을 어설션하도록 만드세요. 취약한 기준에서 실패해야 합니다; 두 리비전 모두에서 통과하는 테스트는 수정에 대해 아무것도 증명하지 않습니다. 기준 쪽 stderr를 읽고 실패가 작성한 어설션임을 확인하세요, 거기에 도달하지 못한 하네스가 아닌지 확인하세요.
- 익스플로잇 및 변종 어설션을 보안 불변 조건으로 제한하세요. 작동 가능성(liveness), 정확한 오류 유형/메시지, 타이밍 및 호환성은 별도의
behavior또는regression검사로 테스트하세요; 그렇지 않으면 관련 없는 계약 변경이 취약점이 남아 있다는 증거로 위장할 수 있습니다. control하네스를 무해하게 유지하고 변경된 구성 요소를 실행하게 하세요. 이는 하네스가 두 리비전 모두에서 작동함을 확립합니다. 제어 실패는 격차를 남기고 다른 실패를 패치에 귀속시키는 것을 방지합니다. 원시 관찰은 검토를 위해 계속 사용할 수 있습니다.behavior는 변경되지 않은 상태로 유지되어야 하는 동작에만 사용하세요. 정확한 출력 비교는 의도적입니다; 불안정한 값은 설명에서 정규화하는 대신 결정론적 테스트 하네스 뒤에 배치하세요.security검사는 취약한 기준에서 통과한 후에야 패치된 실패를 새로 도입된 것으로 처리하세요. 기준선 실패는 귀속을 해결되지 않은 상태로 둡니다.- 검증 중에 패치를 편집하지 마세요. 실패를 패치 작성자에게 반환하고 새로 고정된 실행을 시작하세요.
결과 읽기
result.json 스키마 2.0에는 status, findings, gaps 및 human_review_required가 포함된 assessment가 있습니다. 모든 필수 검사가 유용한 증거를 생성한 경우 status는 complete입니다(일부 검사에서 실패를 찾았더라도). 격차가 남아 있으면 status는 incomplete입니다. Findings는 검사 ID, 검사 종류 및 실패한 기대를 명명합니다. Gaps는 검사 ID와 누락된 증거를 명명하며, 정리 실패와 같은 실행 수준 문제에 대해서는 null 검사 ID를 사용합니다.
스위트 검사는 먼저 패치된 리비전에서 실행됩니다. 완료된 실패는 기준선에서 동일한 검사를 트리거합니다. 기준선이 통과하면 패치 후 실패를 보고합니다. 둘 다 실패하면 두 로그를 모두 보존하고 귀속이 해결되지 않았다고 보고합니다. 일치하는 종료 코드가 동일한 실패를 의미한다고 가정하지 마십시오.
실행기는 완전한 검사에 대한 결과가 없으면 0으로 종료되고, 결과가 있는 완전한 검사는 1, 불완전한 검증은 10, 잘못된 입력은 64로 종료됩니다. 종료 코드 10 이후에도 아티팩트를 읽으세요: 발견점과 함께 격차가 포함될 수 있습니다. 소스 또는 빌드 증거는 런타임 동작을 확립할 수 없습니다. 통과 결과와 함께 증거 수준을 명시하세요.
Claude 동적 워크플로우
Claude Code는 번들 워크플로우를 /post-patch-validation:validate-patch로 노출합니다. Workflow 도구를 통해 구조화된 입력을 전달하려면 선행 슬래시 없이 이름을 사용하세요:
Workflow({
name: 'post-patch-validation:validate-patch',
args: {
finding: '<finding text or local path>',
baseRef: '<vulnerable-ref>',
patchRef: '<patched-ref>',
workdir: 'post-patch-validation',
},
})
해당하는 경우 patchRef 대신 patchFile을 사용하세요. 워크플로우는 고정된 커버리지 렌즈를 사용하여 검사를 제안하고 고정된 실행기를 사용하여 이 스킬을 실행합니다. 에이전트는 테스트 아티팩트를 작성할 수 있습니다. Python 실행기는 발견점과 격차를 기록하고, 검토자는 커버리지 또는 증거 문제를 별도로 보고합니다. NEEDS_REPAIR은 다른 검사가 격차를 남긴 경우에도 지원된 실패를 반환합니다. BLOCKED는 증거가 지원된 실패 없이 불완전함을 의미합니다. 통과 검사는 두 증거 검토가 모두 승인한 후에만 READY_FOR_HUMAN_REVIEW로 진행하며, 그렇지 않으면 REVIEW_REQUIRED입니다. 워크플로우는 시작 후 질문을 할 수 없으므로 모든 입력을 미리 전달하세요.
거부해야 할 합리화
| 합리화 | 요구되는 재... |
[truncated for translation]