무엇을 만드나요
리뷰어는 몇 가지 명확한 부분으로 구성된 작은 Python 프로젝트입니다:| Part | What it does |
|---|---|
| Pydantic 모델 | Evidence, Finding, Chain 정의, LLM과 나머지 프로그램 사이에 단단한 검증 경계 제공 |
| Venice 클라이언트 | Venice의 OpenAI 호환 endpoint를 가리키는 OpenAI Python SDK를 래핑 |
| AST 레포 맵 | Python의 ast 모듈로 타겟 트리를 순회해 모든 모듈의 공개 심볼과 import 엣지의 결정적 맵을 빌드 |
| Scanner 에이전트 | 한 번에 한 Python 파일과 레포 맵의 파일별 neighborhood 슬라이스를 읽고, file:line 증거가 있는 원자적 취약점 발견을 출력 |
| Chainer 에이전트 | 발견의 합집합과 압축된 전체 레포 맵을 읽고, 둘 이상의 발견을 결합한 익스플로잇 체인을 출력 |
| Reference validator | Scanner가 생성하지 않은 finding ID를 참조하거나, 참조된 findings 중 어디서도 오지 않은 파일을 명명하는 체인을 모두 폐기 |
| Markdown 보고서 | findings와 chains를 사람이 읽기 쉬운 보고서로 렌더링 |
| CLI | Typer로 모든 것을 연결 |
- 타겟 디렉터리에서
.py파일 순회. - 결정적 레포 맵 빌드(imports, 공개 심볼, 시그니처).
- 각 파일에 대해 Scanner에 소스와 맵의 파일별 neighborhood 슬라이스를 전송해 원자적 발견 수집.
- 발견의 합집합과 압축된 레포 맵을 Chainer에 전송하고 익스플로잇 체인 수집.
- Scanner가 생성하지 않은 finding ID를 참조하거나, 참조된 findings 중 어디서도 오지 않은 파일을 명명하는 체인을 모두 폐기.
- Markdown 보고서 작성.
ast로 타겟 트리를 순회하고 구조 맵을 빌드합니다. Scanner는 파일별 neighborhood를 봅니다(이 파일에서 import하는 사람, 이 파일이 import하는 것, 그 외부 심볼의 시그니처). Chainer는 압축된 전체 맵을 봅니다(모든 모듈, 모든 공개 심볼, 모든 import 엣지, 소스 없음). 이것이 전체 코드베이스를 모든 prompt에 채우는 토큰 비용을 지불하지 않고도 Chainer가 모듈 경계를 넘는 데이터 흐름의 체인을 구성할 수 있게 하는 최소량의 context 엔지니어링입니다.
사전 요구사항
- Python 3.12+
- venice.ai의 Venice API 키
- Pydantic, Python의
ast모듈, OpenAI Python SDK에 대한 기본 친숙도
uv를 사용하지만, 일반 가상 환경도 동등하게 동작합니다.
프로젝트 설정
새 프로젝트 생성 및 의존성 설치:pip를 선호한다면 가상 환경을 만드세요:
.env 파일 생성:
src/venice_security_reviewer/ 아래에 두고, prompt는 레포 루트의 prompts/ 아래에 두어 다른 소스 아티팩트처럼 리뷰하고 diff할 수 있게 합니다:
Venice 클라이언트 설정
Venice는 OpenAI 호환이므로 공식 OpenAI Python SDK를 사용하고base_url을 Venice로 가리키기만 하면 됩니다. 클라이언트 생성을 한 파일에 중앙화하면 나머지 코드는 어떤 공급자와 통신하는지 알 필요가 없습니다: 백엔드 교체는 이 모듈만 건드리면 됩니다.
src/venice_security_reviewer/client.py 생성:
- 강력한 범용 Venice 모델이라서
zai-org-glm-5를 기본값으로 사용하지만,VENICE_MODEL환경 변수로 override할 수 있습니다. 더 크거나 더 미묘한 코드베이스에서는 더 강력한 모델로 교체하면 Chainer의 서술 품질이 눈에 띄게 개선될 수 있습니다. build_client는 클라이언트 와 모델 ID를 반환해, 호출자가 직접 환경 변수를 읽을 필요가 없고 테스트는 monkeypatching 없이 가짜 config를 주입할 수 있습니다.
데이터 모델 정의
raw dict를 주고받는 대신 Pydantic을 사용하는 핵심은 LLM과 나머지 프로그램 사이에 단단한 검증 경계를 얻는 것입니다. 모델이 잘못된 JSON을 반환하거나 존재하지 않는 finding ID를 만들어내면 파싱이 시끄럽게 실패하고 환각을 보고서로 전파하지 않습니다.src/venice_security_reviewer/models.py 생성:
Finding.id와Chain.id는F-001,C-001같은 정규식으로 제한됩니다. 모델이 포맷을 창의적으로 만들면 검증이 실패합니다.Chain.findings는 최소 두 개의 항목을 요구합니다: 하나의 finding으로 만든 “체인”은 그냥 finding입니다.Chain.severity는high또는critical로 제한됩니다. 가장 높은 개별 severity 위로 영향을 올리지 않는 findings 조합은 보고할 가치가 있는 체인이 아닙니다.Evidence는end_line >= start_line을 강제해 모델이 무의미한 라인 범위를 반환할 수 없게 합니다.
models.py에 다음 함수를 추가하세요:
AST 레포 맵 빌드
레포 맵은 Python 코드베이스의 구조적 골격입니다: 모든 모듈의 공개 표면, 모든 import 엣지, “모듈 M”에서 “M에서 import하는 모듈들”로의 역방향 인덱스. 실행이 아닌 Python의ast로 스캔 실행당 한 번 빌드되므로 적대적 코드에도 안전하게 실행할 수 있습니다: 파서는 스캔된 트리에서 어떤 것도 import하거나 호출하지 않습니다.
맵을 두 가지 모양으로 소비합니다. Scanner는 prompt 크기가 제한되도록 파일별 neighborhood 슬라이스를 받습니다. Chainer는 파일 간 체인을 구성할 수 있도록 압축된 전체 맵을 받습니다.
src/venice_security_reviewer/repo_map.py를 생성하고 맵을 설명하는 Pydantic 모델로 시작하세요:
__all__ 목록. 함수 시그니처와 클래스 헤더는 LLM이 직접 읽을 수 있는 컴팩트한 문자열로 렌더링됩니다:
_SIGNATURE_CHAR_CAP은 (타입 힌트를 포함한) 일반적인 실제 시그니처를 보존하면서, 200줄짜리 타이프된 union 같은 병적인 경우가 prompt를 폭발시키는 것을 막습니다.
다음으로, 파싱된 모듈에서 구조 데이터를 뽑아내는 추출기. ast.FunctionDef, ast.ClassDef, 상수에 대한 최상위 ast.Assign과 ast.AnnAssign, import 엣지에 대한 ast.Import와 ast.ImportFrom을 모두 처리합니다. 상대 import는 절대 점 표기 형태로 해결되어 Chainer가 나중에 모듈 이름과 매칭할 수 있게 합니다:
tree.body를 순회하고 각 최상위 노드에 대해 SymbolDef와 ImportEdge 항목을 출력합니다. 레퍼런스 레포의 repo_map.py에 있는 _extract 함수가 전체 구현을 다룹니다. 결과 모양은 파일당 하나의 ModuleEntry 객체 목록입니다.
흥미로운 부분은 그 항목들로 무엇을 하느냐입니다. 두 개의 소비자 측 메서드가 있는 RepoMap으로 감싸세요:
neighborhood(path)가 Scanner가 각 파일에 대해 호출하는 것입니다. 모듈 자체, 그것에서 import하는 모든 다른 모듈, 그것이 다른 곳에서 import하는 모든 in-repo 심볼(해결된 시그니처와 함께)을 포함하는 ModuleNeighborhood 객체를 반환합니다. 그러면 Scanner는 전체 코드베이스를 prompt에 끌어들이지 않고도 cross-file context에서만 명확한 findings를 표시할 충분한 context를 갖습니다.
condensed_dict()가 Chainer가 받는 것입니다. snippet과 시그니처는 떨어집니다. 경로, 모듈 이름, 공개 exports, import 엣지만 남습니다. 그것이 Chainer가 모듈 간 데이터 흐름에 대해 추론할 수 있게 하는 가장 작은 표현입니다.
마지막으로, 전체를 빌드하는 진입점:
Scanner 에이전트 작성
Scanner는 타겟 경로를 순회하고, Python 소스 파일을 가져와 한 번에 한 파일씩 원자적 취약점을 식별하도록 Venice에 요청합니다. 파일별 스캐닝은 prompt를 작게 유지하고 실패를 격리합니다: 잘못된 파일 하나가 전체 실행을 죽이지 않습니다. prompt 자체를 별도 파일에 두어 다른 소스 아티팩트처럼 리뷰하고 diff할 수 있게 합니다.prompts/scanner.md 생성:
- 모델에게 prose나 fence 없이 JSON만 출력하라고 말합니다. OpenAI SDK는 API 측에서 이를 강제하는
response_format={"type": "json_object"}파라미터를 지원하지만, prompt에서 다시 강화하면 엣지 케이스를 줄입니다. - Scanner가 cross-file 체인을 생성하는 것을 명시적으로 금지합니다. 체인은 Chainer의 일이며, Scanner에게 둘 다 하라고 요청하면 책임이 흐려집니다.
- snippet을 verbatim으로 복사하도록 요구합니다. 그러면 보고서는 모델이 봤다고 주장하는 정확한 바이트를 인용할 수 있고, 리뷰어는 snippet을 소스와 비교해 finding을 spot-check할 수 있습니다.
src/venice_security_reviewer/scanner.py를 생성하고 파일 walker와 prompt 로더로 시작하세요:
MAX_FILE_BYTES는 안전 상한입니다. 약 200 KB를 넘으면 비싸고 품질도 낮을 가능성이 높은 거대한 prompt를 보내는 대신 건너뜁니다.
다음 부품은 prompt 빌더입니다. 템플릿은 {filename}, {source}, {neighborhood}를 자리표시자로 사용합니다. 템플릿이 리터럴 중괄호가 있는 JSON 예시를 포함하므로 .format() 대신 str.replace를 사용합니다:
F-001 같은 ID를 출력하지만, Chainer는 전체 레포에 걸쳐 findings를 참조해야 합니다. 전역적으로 고유하도록 단조 증가 카운터에 대해 ID를 재발급합니다:
response_format={"type": "json_object"}와 낮은 temperature로 Venice에 보내고, 결과를 파싱합니다:
- 파싱 후 증거 파일 경로를
repo_root에 상대적으로 패치합니다. 모델은 우리가 준 파일 이름을 그대로 돌려주지만 보고서 전체에 걸쳐 하나의 표준 형식을 원하기 때문입니다. temperature=0.1은 의도적으로 낮습니다. Scanner가 실행 간 보수적이고 일관되기를 원합니다. 창의성은 Chainer의 일입니다.
Chainer 에이전트 작성
Chainer는 Scanner findings의 합집합과 압축된 레포 맵을 받아 Venice에 findings 중 어떤 것이 실제 익스플로잇 체인으로 결합되는지 묻습니다. 두 개의 결정적 가드레일이 LLM과 보고서 사이에 있습니다:- 모든 체인은 Scanner가 생성한 finding ID만 참조해야 합니다.
- 모든 체인은 최소 하나의 참조된 finding 증거가 닿는 파일만 주장해야 합니다.
prompts/chainer.md에 있습니다. 핵심은 다음과 같습니다:
files_involved에 무엇이 들어가는지 결정하는 방법, 결정적으로 체인하지 않을 때를 설명합니다. 모델에게 “많은 코드베이스가 체인되지 않는 findings를 갖는 것은 옳고 기대되는 일”이라고 말하는 것이 모델이 생산적으로 보이려고 체인을 발명하는 것을 막습니다.
이제 에이전트 코드. src/venice_security_reviewer/chainer.py 생성:
MAX_REPO_MAP_CHARS = 8000은 Chainer prompt의 JSON 렌더링된 레포 맵 블록에 대한 소프트 상한입니다. 대략 토큰당 4자로, 약 2000 토큰이며, findings와 narrative 예산이 위에 있어도 모든 Venice 모델의 context 윈도우 안에 편안하게 들어갑니다.
findings를 컴팩트한 JSON 블록으로 직렬화합니다. 여기서 의도적으로 evidence에서 snippet을 제거합니다: Chainer는 두 findings가 결합되는지 결정하기 위해 raw 바이트가 필요 없고, 그것들을 포함시키면 실제 코드베이스에서 토큰 비용이 대략 두 배가 됩니다:
_pruned, _kept, _total 마커로 주석을 달아 맵이 트림된 경우 Chainer prompt가 모델에 경고할 수 있도록 합니다.
응답 파싱은 Scanner와 같은 모양입니다: 역직렬화, 각 체인을 Pydantic으로 검증, 잘못된 항목 드롭:
- findings가 2개 미만일 때는 모델을 호출하기 전에 빠져나옵니다. 단일 finding으로는 체인을 만들 수 없고, 호출을 건너뛰면 보장된 빈 결과에 토큰을 태우지 않습니다.
temperature=0.2는 Scanner의0.1보다 약간 높습니다. Chainer는 명백하지 않은 조합을 발견하는 데 약간 더 많은 창의성의 혜택을 받지만, 여전히 받은 findings와 맵에 근거하기를 원합니다.- 파싱 후 우리가 앞서 작성한 결정적 cross-reference 검사인
validate_chain_references가 실행됩니다. 살아남는 것은 렌더링하기에 안전하고, 그렇지 않은 것은 운영자가 모델이 무언가를 발명하려 했다는 것을 알 수 있도록 로그됩니다.
Markdown 보고서 렌더링
렌더링을 에이전트 로직과 분리하면 같은Finding과 Chain 객체가 나중에 Scanner나 Chainer를 건드리지 않고 다른 형식(JSON, SARIF, HTML)에 공급될 수 있습니다.
작은 템플릿 파일과 함께 Jinja2를 사용합니다. src/venice_security_reviewer/templates/report.md.j2 생성:
src/venice_security_reviewer/report.py의 렌더러:
.html 템플릿에는 활성화된 상태로 둡니다.
CLI 연결
CLI는 오케스트레이터입니다: 레포 맵 빌드, 스캔, 체인, 렌더링. 인수 파싱은 Typer, 멋진 요약 테이블 출력은 Rich를 사용합니다.src/venice_security_reviewer/cli.py 생성:
pyproject.toml에 스크립트 진입점 추가:
가드레일 테스트
이 빌드 전반에 걸쳐 한 가지 아이디어에 강하게 의존했습니다: 결정적 가드레일이 유용한 보안 도구와 자신 있게 틀린 것을 분리합니다. 그 주장은 가드레일이 실제로 유지된다는 것을 증명할 수 있을 때만 가치가 있으므로, 이 프로젝트에서 가장 가치 있는 테스트는 Venice를 전혀 호출하지 않습니다. Pydantic 경계와 prompt 조립 배관을 잠그며, 그것은 API 키와 토큰 비용 없이 오프라인에서 밀리초 단위로 실행된다는 의미입니다. 먼저 dev 의존성 추가:tests/test_models.py 생성:
F-### 패턴에 맞지 않는 ID, 단일 finding의 “체인”. 그것들 중 어느 것이라도 raise를 멈추면 환각 클래스 하나 전체가 조용히 다시 가능해진 것입니다.
가장 중요한 테스트는 cross-reference validator를 다룹니다. 이는 실제로 발명된 체인을 떨어뜨리는 함수이기 때문입니다:
F-999는 Scanner가 결코 생성하지 않았으므로 그것을 참조하는 체인은 dropped에 들어가고 보고서에 도달하지 않습니다. 레퍼런스 레포의 동반 테스트인 test_validate_chain_references_drops_unknown_files는 findings 중 어디서도 오지 않은 파일을 주장하는 체인에 대해 같은 일을 합니다.
테스트할 가치가 있는 두 번째는 Chainer에 공급하는 배관입니다. prompt 조립을 리팩토링하고 조용히 cross-file context 전달을 멈추기 쉬우며, 그러면 Chainer는 계속 작동하지만 조용히 나빠집니다. 이 테스트는 2-모듈 fixture를 빌드하고, prompt를 렌더링하고, cross-file 정보가 실제로 존재하는지 다시 한 번 Venice 왕복 없이 주장합니다. tests/test_cross_file_chain.py 생성:
tests/test_scanner_parse.py, tests/test_chainer_parse.py, tests/test_repo_map.py가 포함되어 있으며, JSON 파싱 엣지 케이스(잘못된 항목이 실행을 충돌시키는 대신 드롭됨)와 AST 레포 맵 빌더를 다룹니다.
프로젝트 실행
실제 코드베이스에서 시도하려면 CLI를 Python 소스 디렉터리에 가리키세요:pip install -e .로 virtualenv에 설치하고 venice-security-reviewer scan path/to/your/code를 실행하세요.
출력은 대략 다음과 같이 보입니다:
examples/vulnerable_app— 3개의 “low” findings가 있는 다중 파일 Flask 앱으로, 그 중 2개가 파일 간 critical 권한 상승 체인으로 결합됩니다. Chainer가 결합할 것에 대해 선별적인지 테스트합니다.examples/url_preview— 반복마다 적용되지 않는 방어적 allowlist가 있는 다중 파일 URL fetcher. 배포 토폴로지와 결합된 cross-file 데이터 흐름 추론을 테스트합니다(link-local IP는 클라우드 자격 증명 게이트웨이).examples/csv_query—__class__.__base__.__subclasses__()를 통한eval샌드박스 탈출이 있는 단일 파일 CSV 필터. HTTP 흐름보다는 언어 수준 추론을 테스트합니다.examples/webhook_handler— JSON 파서 차분 취약점이 있는 단일 파일 HMAC verifier. 여러 명세에 걸친 추론을 테스트합니다.
chainer referenced N unknown finding id(s) or file(s); chains dropped를 로그하는 것을 보면, 그것은 모델이 체인을 발명하는 현장을 cross-reference validator가 잡은 것입니다. 폐기된 체인은 보고서에 도달하지 않습니다. prompt를 조정하거나 추가 Chainer 실행을 샘플링하는 데 사용할 수 있는 경고만 받습니다.
이 예제 확장
두 에이전트 모양은 잘 일반화됩니다. 탐색할 가치가 있는 몇 가지 방향:- 더 많은 언어. Scanner는 prompt 수준에서 언어 독립적입니다. AST 빌더가 Python 전용입니다.
tree-sitter로 교체하면 TypeScript, Go, Rust에 대해 같은 neighborhood/압축 맵 모양을 빌드할 수 있습니다. - 수정을 위한 세 번째 에이전트. 체인을 얻으면 Patcher 에이전트에 그 구성 findings 중 하나를 무력화하는 통합 diff를 초안하도록 요청하는 것이 작은 단계입니다. 같은 evidence-file 집합에 대해 diff를 Pydantic-검증하면 같은 환각 가드를 공짜로 얻습니다.
- 출력 형식.
render_report는 Markdown에 대해 아는 유일한 곳입니다. SARIF 렌더러를 추가하면 같은 findings가 GitHub code scanning에 들어갈 수 있습니다. JSON 렌더러를 추가하면 결과를 다운스트림 시스템에 파이프할 수 있습니다. - 파일 해시로 캐싱. Scanner의 파일별 호출은 독립적이고 멱등입니다.
(file_hash, prompt_hash, model)로 캐싱하면 한 파일이 변경된 레포를 다시 스캔할 때 그 한 파일에서만 Scanner가 재실행됩니다. - Chainer를 위한 샘플링. 고위험 실행의 경우 약간 더 높은 temperature에서 Chainer를 N번 호출하고 결과를 교차시키세요. 모델이 일관되게 찾는 체인은 실제일 가능성이 더 높고, 한 번 찾고 다시 못 찾는 체인은 노이즈일 가능성이 높습니다.
- 더 강력한 모델.
zai-org-glm-5가 기본값인 이유는 조합적 추론에 대해 비용과 품질 사이의 좋은 균형을 이루기 때문이지만, 더 어려운 코드베이스의 경우 더 강력한 Venice 모델(VENICE_MODEL로 설정)로 교체하면 Chainer의 narrative가 눈에 띄게 더 타이트해질 수 있습니다.