레퍼런스
CLI 레퍼런스
gori 커맨드라인 레퍼런스입니다. 서브커맨드 없이 gori를 실행하면 TUI가 시작됩니다.
gori [command] [options]
| Command | Description |
|---|---|
tui |
프록시와 터미널 UI 시작 (기본값) |
run |
프로젝트 단위 비대화형 스위트 |
mcp |
Model Context Protocol stdio 서버 |
ca |
루트 CA 경로 / PEM 출력, 또는 CA 재생성 / 가져오기 |
settings |
settings.json 표시 또는 편집 |
wizard |
대화형 최초 실행 설정 |
tutorial |
가이드형 TUI 투어 (탐색, 팔레트, 스페이스 메뉴, 편집 모드) |
update |
채널 인식 자체 업데이트 (바이너리 / Homebrew / Snap / AUR / Nix) |
전역 플래그: -v / --version, -h / --help.
gori tui
인터셉트 프록시와 TUI를 시작합니다. 서브커맨드를 주지 않으면 이것이 기본값입니다.
gori
gori tui --listen 0.0.0.0 --port 8080
| Option | Description |
|---|---|
-l, --listen=HOST |
이 프로세스의 전역 바인드 주소 (settings.json 기본값, 없으면 127.0.0.1). 저장되지 않음. 프로젝트 자체 바인드가 설정되어 있으면 그쪽이 우선 |
-p, --port=PORT |
이 프로세스의 전역 바인드 포트, 0-65535 (settings.json 기본값, 없으면 8070). 저장되지 않음. 프로젝트 net.bind_port가 설정되어 있으면 그쪽이 우선 |
--db=PATH |
SQLite 데이터베이스 경로 |
--ca-dir=PATH |
루트 CA 디렉터리 |
--insecure-upstream |
업스트림 TLS 인증서를 검증하지 않음 |
GORI_HOME은 플래그가 아니라 환경 변수입니다. TUI에서는 프로젝트 피커로 프로젝트를 고릅니다. 바인드 플래그는 이번 실행에 한해 전역 계층만 설정합니다. 설정을 참고하세요. 루트 CA 경로는gori ca를 사용하세요.
gori run
비대화형 스위트입니다. 각 서브커맨드는 프로젝트 단위로 동작합니다. --project와 --db가 모두 없으면 가장 최근에 활성화한 프로젝트를 쓰고, 어느 프로젝트를 골랐는지 stderr에 한 번 알립니다(gori run: using project demo (most recently active)). 어디서든 프로젝트를 하나 만들면 이후 모든 명령이 조용히 그쪽을 향하기 때문입니다. 둘은 택일입니다. 둘 다 주면 --db가 조용히 이기는 게 아니라 사용법 오류로 거절합니다. 실제 사용 패턴은 스크립팅 가이드를 참고하세요.
gori run <subcommand> [verb] [options]
| Subcommand | Description |
|---|---|
capture |
프록시를 실행하고 캡처한 플로우를 STDOUT으로 스트리밍 |
history (ls) |
캡처한 플로우 목록 / 쿼리 |
history delete <id> · delete -q QL · clear |
플로우 하나를 완전 삭제, 쿼리에 매칭되는 플로우 전부 삭제 (--yes), 또는 프로젝트 History 전체 비우기 (--yes) |
show <flow-id> |
플로우 하나의 요청과 응답 출력 |
compare <id-a> <id-b> |
두 플로우의 요청 또는 응답 diff |
diff --from A --to B |
리테스트 리포트: 프로젝트 두 개를 엔드포인트 단위로 비교 (added / gone / changed / unchanged / not seen) |
intercept |
캡처 중인 TUI의 라이브 인터셉트 큐 조회 및 조작 |
repeater <flow-id> · list · create · send |
캡처한 플로우 재전송, 또는 Repeater 세션 목록 / 생성 / 실행 (WebSocket 포함) |
repeater minimize <id> |
저장된 요청을 응답이 유지되는 최소 형태로 축약 |
repeater h2 |
순서가 있는 HPACK 필드 목록으로 필드 단위 HTTP/2 요청 전송 |
repeater move <id> · delete <id>… |
탭 번호로 워크벤치 스트립 재정렬, 또는 저장된 세션 하나 이상 닫기 |
fuzz [<flow-id>] |
Intruder 스타일 퍼저 |
fuzz save · list · show · delete |
스윕의 모든 결과를 영구 저장하고, 그 아카이브를 페이지 단위로 읽거나 정리 |
mine [<flow-id>] |
숨은 파라미터 탐색 |
sequence (seq) [<flow-id>] |
토큰 무작위성 평가 (라이브 리플레이, 또는 붙여넣은 목록은 --tokens) |
authorize [<flow-id>…] |
캡처된 플로우를 여러 아이덴티티로 재전송하고 각 응답을 기준선과 비교 (접근 제어 결함) |
probe [QL] |
패시브 보안 스캔 (요청 없음) |
probe issues · dismiss · promote · delete |
저장된 Probe 발견 항목 트리아지 |
probe rules · mode |
스캔 규칙 목록 / 무장, 스캔 모드 조회 및 설정 |
discover |
엔드포인트를 스파이더링 & 브루트포스하여 Sitemap으로 반영 |
import |
HAR / URL 목록 / OpenAPI / Postman / Insomnia / Burp / WSDL 파일에서 History로 플로우 일괄 임포트 |
sitemap [QL] |
호스트 → 경로 엔드포인트 트리 |
sitemap tag |
Sitemap 경로에 자유 텍스트 메모를 고정 / 해제 / 목록 |
oast listen · presets |
아웃오브밴드 콜백 리스너 (interactsh 및 유사 서비스) |
oast list · resume · release |
프로젝트에 저장된 OAST 리스닝 세션 목록 / 재개 / 릴리스 |
oast providers |
저장된 OAST 프로바이더 목록 / 추가 / 수정 / 활성화 / 비활성화 / 삭제 |
jwt [<token>] |
JWT 디코드, 재서명, 또는 공격 페이로드 생성 |
cookie [<cookie>] |
Flask / Rack / Django 세션 쿠키 디코드, 검증, 브루트포스, 위조 |
decoder <chain> [input] |
Decoder 인코드 / 디코드 / 해시 체인 실행 |
notes [<n>] · create · delete |
프로젝트 노트 읽기, 작성, 삭제 |
issues · create · update · delete |
이슈 목록 / 내보내기, 또는 이슈 작성과 삭제 |
links · add · delete |
이슈나 노트에서 플로우, Repeater 세션, 잡으로 이어지는 증거 포인터 |
rewriter · add · rm · enable · disable · preview |
Match & Replace 규칙 관리 |
rewriter preset list · add |
응답 수정 프리셋 목록, 그리고 하나를 평범한 Match & Replace 규칙으로 설치 |
rewriter extract · bindings |
세션 바인딩 추출 규칙 관리, 그 규칙이 선언한 $NAME 목록 |
colormarker · add · rm · enable · disable · move · preview · color |
History 행 색상 규칙 관리 |
views · add · set · rename · scope · rm |
저장된 History 뷰 관리 — 목록을 좁히는 이름 붙은 QL 쿼리를 렌즈로 적용 |
session · add · from-flow · edit · rm · baseline · show · activate |
세션 슬롯 — 전송이나 Authorize 실행이 그 이름으로 나가는 신원 |
grpc [schema] · reflect · forget |
gRPC .proto 렌즈: 무엇이 로드됐는지 보기, 서버 리플렉션으로 디스크립터 받기, 캐시된 대상 버리기 |
project [list] |
알려진 프로젝트 목록 |
project create <name> |
이름으로 프로젝트 생성 (같은 이름이면 다시 열기) |
project delete <name> |
프로젝트와 그 안에 캡처된 모든 것 삭제 (--yes로 확인) |
project scope |
스코프 규칙 목록 / 추가 / 수정 / 삭제 / 활성화 / 비활성화 |
project sandbox |
하드 컨테인먼트 샌드박스 게이트 조회 / 설정 (status, on, off) |
project env |
프로젝트 env 변수 목록 / 설정 / 삭제 ($KEY 치환) |
project host-override |
프로젝트 호스트 → IP 다이얼 오버라이드 목록 / 추가 / 수정 / 삭제 |
읽기 서브커맨드에 공통인 플래그: --project=NAME, --db=PATH, --format=FMT (보통 text 또는 json). 전역 플래그는 동사 뒤에 옵니다. gori run rewriter rm 1 --project=x는 되지만 gori run rewriter --project=x rm 1은 조용히 목록만 찍는 대신 사용법 오류로 거부됩니다.
읽기 서브커맨드는 스토어를 읽기 전용으로 열고 캡처 락을 잡지 않으므로, 라이브 TUI가 캡처 중인 프로젝트를 대상으로 실행해도 안전합니다. body: 질의는 검색 인덱스를 비우므로 쓰기입니다.
출력 계약
STDOUT은 데이터를 나릅니다. 경고, 개수, 내보내기 확인 메시지는 STDERR로 가므로 파이프가 깨끗하게 유지됩니다. 읽는 쪽이 파이프를 먼저 닫아도(… | head) 조용히 0으로 끝납니다.
실행이 스트리밍되는 곳에서는 json과 jsonl의 형태가 늘 같지는 않습니다.
| 서브커맨드 | --format json |
--format jsonl |
|---|---|---|
capture, history |
한 줄에 JSON 객체 하나 | json의 별칭 — 출력 동일 |
fuzz, mine, discover, authorize |
버퍼링 후 마지막에 JSON 배열 하나 | 결과가 나올 때마다 한 줄씩 |
| 종료 코드 | 의미 |
|---|---|
0 |
성공 |
1 |
오류 — 전송 실패, 열 수 없는 프로젝트, 적용되지 못한 변경 |
3 |
run fuzz --fail-if-no-matches가 완료했지만 매칭이 없음 |
130 |
SIGINT/SIGTERM으로 중단 — fuzz, mine, discover, sequence, authorize, repeater minimize는 모아 둔 것을 먼저 내보낸 뒤 130으로 종료하므로, 스크립트의 && next-step이 잘린 실행을 끝난 실행으로 오해하지 않습니다 |
--fail-if-no-matches 없이 실행하면, 매칭이 없으면서 동시에 모든 전송이 실패한 fuzz는 1로 끝납니다. "결과 없음"과 "대상에 닿지도 못함"이 구분됩니다. 플래그를 주면 3이 우선합니다.
run capture
gori run capture --port 8070 --format json --for 5m
| Option | Description |
|---|---|
-l, --listen; -p, --port |
이 프로세스의 전역 바인드 (설정 기본값; 프로젝트 오버라이드가 여전히 우선) |
--project=NAME |
기록할 프로젝트 (기본값 default) |
--db=PATH |
데이터베이스 경로 |
-k, --insecure-upstream |
업스트림 TLS 검증 생략 |
--format=FMT |
text 또는 json (JSON Lines) |
--for=DURATION |
예: 30s, 5m, 1h 이후 중지 |
--max=N |
플로우 N개 이후 중지 |
run history / ls
gori run history -q 'status:5xx' --limit 100 --format json
| Option | Description |
|---|---|
-q, --query=QL |
쿼리 언어 필터 (위치 인자로도 허용) |
-n, --limit=N |
최대 행 수 (기본값 50) |
--view=NAME |
저장된 뷰를 적용합니다 — 그 쿼리는 -q를 대체하지 않고 AND로 얹힙니다. TUI의 v 피커가 필터 바 위에 얹히는 것과 같습니다. 명시할 때만 적용됩니다 — TUI가 보고 있는 뷰가 여기 자동으로 걸리는 일은 없으며, --in-scope가 저장된 ⇧S 렌즈에 대해 긋는 선과 같습니다. 없는 이름은 무시하지 않고 거절하며(있는 이름들을 알려 줍니다), 목록에서만 씁니다 |
--in-scope |
프로젝트에 설정된 스코프 안의 플로우만 — TUI의 ⇧S 렌즈로, 옵트인이며 그 렌즈의 활성화 여부와 무관합니다. 캡처는 여전히 전부 기록하며, 스코프 규칙이 없으면 빈 결과 |
--lenient |
없는 필드 이름을 쓴 쿼리를 거절하지 않고 그 토큰을 텍스트로 검색 |
--column=SPEC |
행마다 추출한 값을 함께 출력합니다 (반복 가능). [LABEL=][req|res:]kind:selector — 예: header:x-request-id, RID=req:header:authorization, jsonpath:data.id, regex:token=(\w+), position:0:32. --column을 하나라도 주면 이 프로젝트에 설정된 History 컬럼을 대체합니다 |
--no-columns |
이 프로젝트에 설정된 History 컬럼을 그리지 않습니다 |
--format=FMT |
text, json / jsonl (둘 다 JSON-Lines), 또는 har |
서브커맨드: history show <id> (run show와 동일), history delete <id>, history delete -q QL --yes, history clear --yes.
이 프로젝트의 History 컬럼은 기본으로 함께 그려지므로, 헤드리스 목록도 TUI의 History 탭과 같은 값을 보여 줍니다. 컬럼 없는 기본 목록으로 돌아가려면 --no-columns를 쓰세요. text에서는 행 끝에 label=value로 붙고(빈 값도 포함해서 전부 — "이 디스크립터는 여기서 아무것도 못 찾았다"도 봐야 할 답입니다), json에서는 columns 객체로 실립니다(컬럼이 없으면 키 자체가 없습니다). =는 첫 :보다 앞에 올 때만 라벨 구분자이므로 regex:token=(\w+)는 regex:token이라는 컬럼이 아니라 패턴 그대로입니다. 컬럼 하나당 출력되는 행마다 읽기 한 번이 추가되고, 본문을 읽는 세 종류는 본문을 최대 512 KiB까지 읽습니다.
json/jsonl의 각 행은 플로우의 절대 url과 요청 헤더를 담은 headers 객체를 함께 싣습니다 (같은 이름이 반복되면 배열이 됩니다). 본문은 넣지 않습니다 — 그건 run show의 몫입니다.
각 행에는 source도 실립니다 — 이 플로우가 어디서 왔는지(proxy, repeater, fuzzer, discover, import …)이며, 출처를 기록하기 전에 캡처된 플로우는 null입니다. 값이 있을 때 source_surface(tui / cli / mcp)와 source_ref도 함께 나옵니다. text 형식은 평범한 캡처 트래픽이 아닌 행에 [repeater] 같은 칩을 찍습니다. src:로 필터링하며, MCP list_history와 get_flow도 같은 키를 냅니다.
history delete -q QL은 쿼리에 매칭되는 플로우를 전부 삭제하며 --yes가 필요합니다. --yes 없이 실행하면 몇 개가 지워질지 출력하고 거부합니다. QL이 모르는 필드를 쓴 쿼리(methd:)도 조용히 아무것도 지우지 않는 대신 거부합니다. id도 -q도 없으면 거부합니다 — 프로젝트를 통째로 비우는 건 history clear --yes입니다.
--format har은 결과 집합 전체를 하나의 HAR 1.2 log로 STDOUT에 씁니다. 오래된 항목이 먼저 오므로, 쿼리 결과를 동료에게 넘기거나 Burp, Charles, 브라우저 네트워크 패널에 그대로 불러올 수 있습니다. HAR 내보내기를 참고하세요.
run show
gori run show <flow-id> --format raw
--format은 text, json, raw(정확한 바이트), har(항목 하나짜리 HAR log), 또는 요청을 코드로 직렬화하는 curl, python(requests), fetch(JavaScript), go(net/http), httpie, csrf(스스로 제출하는 HTML CSRF PoC)입니다. 각각 TUI의 Space → Y Copy as… 에서 같은 이름의 항목이 복사하는 것과 바이트 단위로 동일한 텍스트를 냅니다. --request-only / --response-only로 출력을 제한하며, har에는 적용되지 않습니다. 요청을 코드로 내는 형식은 모두 요청 그 자체이므로 --response-only는 거부됩니다. 두 가지 주의사항은 STDOUT의 스니펫이 아니라 STDERR로 나갑니다. 캡처 한도에서 잘린 요청 본문은 짧은 채로 실리고, WebSocket 플로우는 업그레이드 핸드셰이크만 직렬화되며 프레임은 담기지 않습니다. 디코드된 SAML/JWT/GraphQL/파라미터, WebSocket 메시지, SSE 이벤트가 있으면 함께 포함됩니다.
HAR 내보내기
gori가 쓴 HAR은 다시 gori로 가져와도(gori run import --har) 같은 플로우가 되므로 왕복이 보장됩니다. 네 가지를 알아두세요.
- 본문은 와이어 바이트입니다. chunked만 풀고 압축은 풀지 않으며, 유효한 UTF-8이 아니면 base64로 인코딩합니다.
Content-Encoding헤더가headers에 그대로 남아 본문과 헤드가 같은 메시지를 가리킵니다. - 캡처 상한에 잘린 본문은 표시되며, 온전한 것처럼 나가지 않습니다.
bodySize와content.size는 실제 와이어 크기를 유지하고 텍스트에는 캡처된 앞부분만 담기며,content/postData의comment가 그 사실을 적습니다. 명령은 해당 개수도 STDERR로 보고합니다. - WebSocket 플로우는 메시지와 함께 내보내집니다. 실제
101핸드셰이크에 캡처된 전송 기록이 Chrome DevTools의_webSocketMessages필드로 나란히 실리며,gori run import --har로 다시 복원됩니다. 방향, opcode(제어 프레임 포함), 바이트(유효한 UTF-8이 아니면 base64), 밀리초 단위 타임스탬프가 유지됩니다. 프레임별 형태(FIN/RSV/마스크 키)는 이 형식에 담을 필드가 없으므로 필요하면--format json또는raw를 쓰세요. - 응답이 캡처되지 않은 플로우는 건너뜁니다. 전송 기록이 비어 있는 소켓도 마찬가지입니다 — 핸드셰이크만으로는 교환이 아니기 때문입니다. 개수와 이유는 STDERR로 나가고 STDOUT은 순수한 HAR 문서로 유지됩니다.
run compare
두 플로우의 줄 단위 diff이며, Comparer 탭과 동일한 결과를 냅니다.
gori run compare 41 42 --pane response --changes-only
| Option | Description |
|---|---|
--pane=PANE |
비교 대상: request 또는 response (기본값) |
--changes-only |
변경되지 않은 문맥은 빼고 추가 / 삭제된 줄만 출력 |
--context=N |
변경 지점 주변 N줄만 남기고 나머지 동일 구간은 @@ N unchanged lines @@ 마커로 접기 (--changes-only와 함께 쓸 수 없음) |
--format=FMT |
text (기본값) 또는 json |
diff 위에 양쪽의 status · size · time과 A→B 델타가 출력됩니다. 상태 코드가 뒤집혔는지, 크기가 얼마나 달라졌는지를 첫 줄을 읽기 전에 알 수 있습니다. --format=json에도 같은 값이 meta로 들어가고, 접힌 구간은 빈칸이 아니라 {"kind":"fold","hidden":N} 행이 됩니다.
--changes-only는 무엇이 바뀌었는지는 알려주지만 어디서 바뀌었는지는 지웁니다. 400줄 본문에서 한 줄만 다르면 위치 없는 두 줄만 남습니다. --context는 변경 지점을 제자리에 두고, 건너뛴 양을 함께 적습니다.
run diff
리테스트 리포트입니다. run compare가 메시지 두 개를 비교한다면, 이건 프로젝트 두 개를 엔드포인트 단위로 비교합니다. 실무의 절반을 차지하는 질문 — 지난번 대비 뭐가 바뀌었나 — 에 답합니다.
gori run diff --from q1-audit --to q3-retest --format md
| Option | Description |
|---|---|
--from=NAME |
기준 프로젝트 — 이전 엔게이지먼트(이름, slug, 짧은 id). 필수 |
--to=NAME |
이후 프로젝트 (기본값: 가장 최근에 활성화한 프로젝트) |
--from-db=PATH / --to-db=PATH |
레지스트리 프로젝트 대신 SQLite 파일을 직접 지정 |
-q, --query=QL |
양쪽 모두를 QL 쿼리로 좁힘 |
--in-scope |
각 프로젝트 자신의 스코프 규칙 안에 있는 호스트만 |
-n, --limit=N |
한쪽에서 읽을 엔드포인트 그룹 최대치 |
--verdict=LIST |
지정한 판정만 나열 (added,gone,changed,unchanged,removed) |
--unchanged |
변화 없는 엔드포인트도 나열 (개수는 항상 집계됨) |
--no-issues |
이슈 리테스트를 건너뜀 |
--format=FMT |
text (기본값), json, 또는 md — 리테스트 산출물에 그대로 붙여 넣을 섹션 |
아무것도 보내지 않습니다. 양쪽 모두 캡처된 트래픽만 비교합니다. 발견 항목이 아직 재현되는지 확인하려면 요청을 보내야 하고, 그건 Repeater로 직접 하는 선택으로 남습니다.
엔드포인트 동일성
엔게이지먼트 두 번이 같은 식별자를 캡처하는 일은 없습니다. 그래서 리터럴 경로로 키를 잡으면 모든 행이 removed 한 번 added 한 번으로 두 번 보고되고, 결국 아무 말도 하지 못합니다. 그래서 Sitemap이 그리는 폴딩 템플릿을 그대로 키로 씁니다 — /users/{uuid}, /items/{n}, /search(쿼리 변형은 경로로 접힘). 폴딩은 양쪽의 합집합에 대해 한 번만 돌기 때문에, 한쪽에서만 폴딩 임계치를 넘긴 라우트도 반대쪽과 서로 매칭됩니다.
다섯 가지 판정
| 판정 | 의미 |
|---|---|
added |
B에는 캡처됐고 A에는 없음 |
gone |
양쪽 모두 캡처됨 — 그런데 A는 도달 가능했던 반면 B가 받은 응답은 전부 404/410. 캡처만으로 "정말 사라졌다"를 말할 수 있는 유일한 근거 |
changed |
양쪽 모두 캡처됨. 상태 클래스, 인증, content type, 크기 중 최소 하나가 허용 범위를 넘어 움직임 |
unchanged |
양쪽 모두 캡처됐고 동등함 |
removed |
A에는 있는데 B는 그 엔드포인트로 요청을 아예 안 함 — 커버리지 공백이지 삭제의 근거가 아님 |
removed/gone 구분이 이 명령의 핵심입니다. 리테스트가 얕으면 방문한 엔드포인트도 적어지는데, 이 둘을 한 바구니에 넣으면 짧은 오후 작업이 대규모 수정처럼 보고됩니다. 모든 출력이 그 단서를 맨 앞에 두고, --verdict로 목록을 좁혀도 개수는 항상 다섯 판정 전부를 덮습니다.
changed 판정은 바이트 동일성이 아니라 허용 밴드로 내립니다 — repeater minimize와 mine이 쓰는 바로 그 캘리브레이션입니다. 그래서 캡처 사이에 길이가 흔들리는 페이지는 unchanged로 읽힙니다. 상태 코드는 클래스로 비교합니다. 200이 201이 된 건 리테스트 발견이 아니지만 200이 403이 된 건 발견이고, 그건 별도의 auth 축으로 보고됩니다.
이슈 리테스트
리포트 끝에는 기준 프로젝트에서 아직 열려 있는 이슈들과, 그 이슈가 걸려 있던 엔드포인트가 어떻게 됐는지가 붙습니다 — "여전히 같은 방식으로 응답함, 발견 항목이 아직 유효할 가능성이 높음", "이제 404/410으로 응답함", "새 캡처에서 요청된 적 없음, 닫기 전에 리테스트할 것". 요청은 보내지 않습니다. 수정 확인은 당신의 판단입니다.
커버리지와 스코프
개수 위에 양쪽의 플로우 수, 엔드포인트 수, 호스트 수, 캡처 기간이 함께 출력됩니다. B의 커버리지가 A보다 얇으면 바로 보입니다. 두 프로젝트의 스코프 규칙이 다르면 그 사실도 적습니다 — 그쪽 프록시가 애초에 기록하지 않아서 엔드포인트가 없는 것일 수 있으니까요.
run intercept
캡처 락을 쥔 TUI의 라이브 인터셉트 큐를 조작합니다. 인터셉트는 TUI 전용입니다. 헤드리스 gori run capture는 메시지를 붙잡지 않으며, 여기 서브커맨드는 상태를 게시하는 캡처 인스턴스가 없으면 모두 거부합니다.
gori run intercept # 붙잡힌 항목 + 인터셉트 상태
gori run intercept get 3 --format json
gori run intercept forward 3
gori run intercept edit 3 --raw-file edited.txt
gori run intercept direction request
| Subcommand | Description |
|---|---|
list (기본값) |
붙잡힌 항목과 캐치 상태, 방향, 필터 |
get <item-id> |
붙잡힌 항목 하나의 전체 상세 |
forward <item-id> |
바이트 그대로 통과 |
drop <item-id> |
폐기. 클라이언트는 정해진 502를 받음 |
edit <item-id> |
편집한 바이트로 통과: --raw=RAW 또는 --raw-file=PATH. 그대로 전달되며($KEY 확장 없음) Content-Length만 다시 맞춤 |
enable / disable |
라이브 캐치 켜기 / 끄기 |
filter <query> |
조건부 인터셉트 쿼리 설정. ""를 넘기면 해제 |
direction <both|request|response> |
캐치가 붙잡을 구간 선택 |
list와 get은 --include-sensitive를 주지 않으면 민감한 헤더 값을 가립니다. 쓰기 서브커맨드는 프로젝트 데이터베이스를 거쳐 TUI의 ack를 폴링합니다.
run repeater
캡처한 플로우 하나를 재전송하거나, TUI와 공유되는 Repeater 워크벤치 세션을 관리합니다.
gori run repeater <flow-id> --target https://staging.example.com --http2 --diff
| Option | Description |
|---|---|
--target=URL |
다른 오리진으로 전송. 경로와 쿼리는 유지 |
--http2 / --http1 (--no-http2) |
프로토콜 강제. 기본값은 플로우가 캡처된 방식을 따름 |
--sni=HOST |
TLS SNI 오버라이드 |
-k, --insecure-upstream |
업스트림 TLS 검증 생략 |
--timeout=SEC |
작업당 연결 + 유휴 타임아웃 |
-H, --header=HEADER |
요청 헤더 덮어쓰기/추가 (반복 가능). 같은 이름을 반복하면 중복 헤더 줄을 보냅니다. 명시한 Content-Length는 그대로 존중되어 CL 불일치 테스트에 쓸 수 있습니다 |
--rm-header=NAME |
해당 이름의 헤더를 모두 삭제 (반복 가능). Content-Length를 지우면 자동 재계산이, Host를 지우면 --target 동기화가 꺼집니다 |
-b, --body=BODY |
요청 본문 오버라이드 |
--keep-request-line |
저장된 요청 라인을 그대로 전송. 절대 형식(GET http://h/p)을 origin 형식으로 고치지 않습니다 |
--diff |
원본 응답과 비교 |
--allow-unscoped |
프로젝트 스코프 밖으로도 전송. 샌드박스와 명시적 제외 규칙은 매 전송을 여전히 거부합니다 |
--format=FMT |
text (기본값) 또는 json |
repeater list: 저장된 Repeater 세션 목록 (--format text|json).
repeater create: Repeater 세션 생성:
gori run repeater create --target https://api.example.com --request-file req.txt --name "login probe"
gori run repeater create --flow 42 --name "clone of 42"
| Option | Description |
|---|---|
-t, --target=URL |
대상 URL (--flow로 복제하는 경우가 아니면 필수) |
-f, --request-file=FILE |
FILE에서 원시 HTTP 요청을 읽음 |
-r, --request-raw=RAW |
원시 HTTP 요청 문자열 그대로 |
--flow=ID |
캡처한 플로우에서 요청 / 대상 / HTTP/2 복제 |
--name=NAME, --tags=TAGS |
사용자 지정 탭 이름, 그리고 TUI 하위 탭 라벨이 되는 자유 텍스트 태그 |
--http2 / --http1 (--no-http2) |
프로토콜 선택. --http1은 h2로 캡처된 --flow를 덮어씁니다 |
--no-auto-cl, --sni=HOST |
자동 Content-Length 생략, SNI 오버라이드 |
--keep-request-line |
--flow와 함께: 요청 라인을 캡처된 그대로(절대 형식 포함) 저장 |
--ws-keep-key |
WebSocket: 요청 자신의 Sec-WebSocket-Key를 전송. 키가 없거나 짧거나 중복이거나 base64가 아닌 경우를 테스트할 수 있습니다 |
--ws-http-only |
WebSocket: 이 세션을 평범한 HTTP로 저장. 업그레이드를 일반 요청으로 보내고 101을 응답으로 읽습니다 |
repeater send <repeater-id>: 저장된 세션을 실행합니다. HTTP와 WebSocket 모두 해당됩니다.
gori run repeater send 3 --diff
gori run repeater send 5 --message '{"op":"subscribe"}' --idle-ms 5000
| Option | Description |
|---|---|
--diff |
세션에 마지막으로 저장된 응답과 비교 |
--verbatim |
저장된 바이트를 정확히 그대로 전송: $VAR 확장(프로젝트 env 변수 와 세션 바인딩 모두 — $NAME이 와이어에 리터럴로 나감), 단독 LF 승격, Content-Length 재계산, HTTP/2→1.1 버전 보정, h2 필드명 소문자화를 모두 하지 않음. $ 문법을 아예 해석하지 않으므로 $$name 이스케이프도 소비되지 않습니다 — $name으로 쓰세요. 활성 --slot의 헤더 오버레이는 계속 적용됩니다 — 어떤 바이트가 아니라 누구로서 보낼지에 답하는 옵션이기 때문입니다. 저장된 헤더 그대로 보내려면 --slot을 주지 마세요 |
--reframe-grpc |
HTTP/2 전용: 실제로 전송되는 본문에 맞춰 gRPC 5바이트 길이 접두사를 다시 계산합니다(길이가 바뀐 단항 메시지용). 기본값은 꺼짐 — 페이로드와 어긋나는 접두사는 표준적인 파서 테스트이므로 쓴 그대로 나갑니다 |
--message=TEXT |
WebSocket: 보낼 텍스트 메시지 (반복 가능; 세션에 저장된 메시지를 대체) |
--message-frame=SPEC |
WebSocket: 형태를 명시한 프레임 하나. 쉼표로 구분한 key=value: opcode=text|bin|cont|close|ping|pong|<0-15>, fin, rsv, mask, mask_key, len, 그리고 hex=/b64=/text= 중 하나 |
--idle-ms=N |
WebSocket: 첫 수신 프레임 이후 서버 침묵 타임아웃 (100–60000, 기본값 3000) |
--http |
WebSocket: 이번 전송에 한해 핸드셰이크를 일반 HTTP 요청으로 전송. 바이트를 고치는 게 아니라 엔진을 고르는 것입니다 |
--record-history |
나가는 요청 + 응답을 History에 캡처 플로우로 기록하고 flow id를 stdout에 출력(HTTP 전용; Repeater 전송은 기본적으로 플로우를 남기지 않음) |
--ws-keep-key, -k, --timeout, --allow-unscoped, --format |
위와 동일 |
repeater move <repeater-id>: 워크벤치 스트립의 순서를 바꿉니다. --to N은 repeater list가 출력하는 1부터 시작하는 탭 번호이고, --up / --down은 한 칸씩 옮깁니다. 셋 중 하나만 주세요 — --to와 방향을 함께 주면 임의로 해석하지 않고 거절하며, 1-<개수> 범위를 벗어난 --to도 잘라 맞추지 않고 거절합니다. 명령이 지목하지 않은 자리에 세션이 놓이는 일이 없도록 하기 위해서입니다. --format json은 from_index / to_index / moved를 보고합니다.
gori run repeater move 5 --to 1 # 첫 번째 탭으로
gori run repeater move 5 --down
repeater delete <repeater-id> [<repeater-id>…] --yes: 저장된 세션 하나 이상을 닫고 스트립의 번호를 다시 매깁니다. --yes는 필수이며, 첫 삭제 전에 모든 id를 검사합니다 — 하나라도 모르는 id가 있으면 호출 전체를 거절하므로, 오타 하나로 워크벤치가 절반만 비는 일은 없습니다. 각 줄은 그 세션이 가지고 있던 탭 번호를 말합니다(무엇이든 밀려나기 전에 한 번만 읽습니다). --format json은 deleted(was_tui_index 포함), failed, remaining을 반환합니다. 지우지 못한 세션이 있으면 종료 코드가 0이 아닙니다.
repeater minimize <repeater-id>: 응답이 그대로 재현되는 최소 형태까지 요청을 줄입니다. --apply는 결과를 세션에 다시 씁니다. --verbatim은 저장된 바이트를 그대로 보내며, 이때 본문 파라미터는 프레이밍을 정직하게 유지할 수 없어 후보에서 빠집니다. -k/--insecure, --allow-unscoped, --format은 위와 같습니다.
repeater h2: 순서가 있는 HPACK 필드 목록으로 필드 단위 HTTP/2 요청을 보냅니다. 중복되거나 순서가 뒤바뀐 의사 헤더를 스크립트로 만들 수 있습니다.
gori run repeater h2 --target https://api.example.com --fields fields.json
--fields=FILE은 [[name, value], …] 배열이거나 {"fields": [[name, value], …], "body": "…"} 형태의 JSON 파일입니다(바이너리는 body_base64). 목록의 어떤 것도 정규화하지 않습니다 — 앞의 콜론, 앞 공백이 붙은 값, 대문자 이름이 곧 페이로드입니다. --target은 다이얼할 오리진을 정하므로, :authority와 :scheme 필드는 의도적으로 그와 어긋나게 둘 수 있습니다.
run fuzz
소스: --flow=ID, --repeater=ID, --request=FILE, 또는 stdin. 위치: §…§ 마커, --auto, --mark=TOKEN, 또는 스키마가 아는 gRPC 필드용 --field=SPEC.
| Group | Options |
|---|---|
| Source | --flow=ID(캡처 플로우), --repeater=ID(저장된 리피터 세션 — WebSocket 세션이면 핸드셰이크와 저장된 프레임을 함께 시드), --request=FILE, 또는 bare <flow-id> / stdin |
| Transport | --target=URL (--request/stdin에 필수), --http2, --sni=HOST, -k/--insecure-upstream |
| Mode | --mode= sniper (기본값), batteringram, pitchfork, clusterbomb |
| gRPC fields | --field=SPEC(반복 가능)는 단항 gRPC 요청의 옥텟 대신 스키마가 아는 필드를 스윕한다. SPEC은 필드 이름, 중첩 메시지 경로(profile.age), 필드 번호, 반복 필드의 특정 occurrence(name[i])이며, name¦chain은 선언된 타입이 바이트로 인코딩하기 전에 Decoder 체인을 돌린다. 페이로드는 필드 선언을 거쳐 바이트가 되고(-3은 int32·sint32·bool·enum마다 다른 옥텟이다), 메시지의 나머지 바이트는 캡처에서 그대로 복사되며, 5바이트 길이 접두사는 다시 계산된다. 해당 rpc를 해석할 descriptor set이 필요하다(gori run grpc schema). 필드 위치는 템플릿 자신의 §…§ 위치 뒤에 붙으므로 --mode와 페이로드 세트의 의미는 그대로다. 스키마가 선언하지 않은 필드, 선언과 와이어 타입이 충돌하는 필드, 선언된 타입이 담을 수 없는 페이로드는 모두 첫 요청 전에 거부된다 |
| Payloads | -w/--wordlist, --preset=NAME[:FILE] (내장: sqli, xss, traversal, format-string, bad-strings, command-injection), --payloads=LIST, --numbers=FROM-TO[:STEP], --null=N, --brute=CHARSET:MIN-MAX |
| Encoding | 쿼리 문자열이나 form-urlencoded 본문 값에 치환되는 페이로드는 기본으로 URL 인코딩됩니다. 경로 세그먼트·JSON/원시 본문·헤더·쿠키는 그대로 나갑니다. --no-encode는 쿼리/폼 위치도 원시로 보냅니다 — 페이로드 자체가 이미 퍼센트 이스케이프인 경우에 쓰세요(%00이 %2500으로 나가므로, origin의 디코더 자체를 겨눈 %00 / %c0%af / %2e%2e%2f 탐침은 그냥 텍스트로 도착합니다). --encode를 명시하면 기본 인코딩을 대체합니다 — 그 파이프라인이 모든 위치에 적용됩니다. --prefix / --suffix / --case / --hash / --regex-replace는 대체하지 않습니다: 페이로드가 무엇인지를 말할 뿐 와이어가 그것을 어떻게 적는지는 말하지 않으므로, 그 출력도 쿼리/폼 위치에서는 인코딩됩니다 |
| Processors | --prefix, --suffix, --encode (url|urlall|base64|hex), --case (upper|lower), --hash (md5|sha1|sha256), --regex-replace=/pat/rep/ |
| Rate | --concurrency (20), --rate=RPS, --throttle=MS, --timeout=SEC, --retries=N, --max-requests=N (총 요청 상한. 재시도와 리다이렉트 홉도 포함), --follow-redirects, --no-keep-alive |
| Framing | --verbatim — 템플릿의 Content-Length를 쓰인 그대로 전송. 페이로드 치환 후에도 재계산하지 않고, 길이 선언이 없는 본문에 추가하지도 않습니다 (CL / CL-TE 디싱크 페이로드용. Content-Length도 청크 Transfer-Encoding도 없는 본문은 오리진이 길이 0으로 읽으므로 경고합니다). --reframe-grpc — 페이로드가 단항 gRPC 메시지에 삽입된 뒤 5바이트 길이 접두사를 다시 계산합니다(기본값은 꺼짐: 오래된 접두사는 고치지 않고 보고만 합니다) |
| WebSocket | Upgrade: websocket 핸드셰이크를 선언한 템플릿은 프레임 교환으로 스윕한다 — 페이로드 하나가 세션 하나(완전한 RFC 6455 세션). --message=TEXT / --message-frame=SPEC로 송신 프레임을 작성하며(반복 가능, 지정한 순서대로; SPEC은 gori run repeater send와 같은 문법 — opcode=, fin=, rsv=, mask=, mask_key=, len=, 그리고 hex=|b64=|text= 중 하나), --flow/--repeater 시드가 가져온 프레임을 대체한다. §…§ 위치는 프레임 안에 표시한다 — 핸드셰이크도 위치 공간이며 둘은 한 번의 실행에서 함께 스윕된다. --idle-ms=N 세션별 침묵 대기(100–60000, 기본 3000), --ws-keep-key는 템플릿 자체의 Sec-WebSocket-Key를 보낸다. --ws-http-only는 핸드셰이크를 평범한 요청으로 스윕한다. 업그레이드가 성공하면 모든 행이 101이므로 행마다 ws_close_code와 ws_frames_in이 붙는다. 프레임 경로에서 --race, --http2, --record-history는 거부된다(셋 다 --ws-http-only 아래에서는 동작하며, 그쪽은 평범한 HTTP 스윕이라 History에도 기록된다). --follow-redirects / --timeout / --ac는 무의미하여 한 번 알려 준다. 송신 프레임이 없는 WebSocket 시드는 빈 프레임 세션이 아니라 평범한 HTTP로 스윕한다 |
| Matchers | --mc/--fc status, --mg/--fg grpc-status 트레일러의 gRPC 상태 (7, >0, 1-16), --ms/--fs size, --mw/--fw words, --ml/--fl lines, --mt/--ft 왕복 시간(ms, --mt '>=5000' — 시간 기반 블라인드 페이로드가 유일하게 움직이는 차원이며, 타임아웃된 전송도 여기서는 매치로 센다), --mr/--fr body regex, --mh/--fh 응답 HEAD의 대소문자 무시 부분 문자열 (--mh 'x-powered-by: php' — body regex는 헤더를 보지 않는다), --extract=REGEX, --ac auto-calibrate |
| Session bindings | --bind-from=FLOW-ID — 캡처된 그 플로우를 먼저 재생해, 응답이 남은 실행 동안 쓸 $NAME 바인딩을 채우게 합니다 |
| Session slot | --slot=NAME — 이 세션 슬롯으로 전송: 그 슬롯의 헤더 오버레이, 그리고 $NAME을 위한 그 슬롯의 바인딩 테이블. --bind-from보다 먼저 적용됩니다 |
| Scope | --allow-unscoped — 프로젝트 스코프 밖으로도 전송. 샌드박스와 명시적 제외 규칙은 매 전송을 여전히 거부합니다 |
| Output | --format (text|json|jsonl), --force, --fail-if-no-matches (매칭이 없으면 종료 코드 3) |
| Evidence | --record-history=none|matched|all — 전송한 각 요청 + 응답을 History에 플로우로 기록(기본 none; matched는 매칭된 행만, all은 매 전송, 5000개 상한). gori run history / get_flow로 다시 읽습니다 |
영구 퍼즈 실행
gori run fuzz …는 여전히 일회성입니다. 같은 소스/옵션 앞에 save 동사를 붙이면 모든 결과를 — 완성된 렌더링 요청, 최종 와이어 요청, 응답 헤드, 응답 본문까지 — 영구 저장합니다:
gori run fuzz save 42 --auto --preset sqli
gori run fuzz save --request request.txt --target https://api.example.com --project acme --payloads a,b
파일/stdin에서 저장할 때는 --project나 --db가 필요합니다. 프로젝트를 지정하지 않은 스윕을 가장 최근 프로젝트에 조용히 써넣지 않기 때문입니다. --record-history는 이와 별개로 남습니다 — 저장된 결과 집합이 아니라 History 플로우를 제어합니다.
| 명령 | 설명 |
|---|---|
fuzz list |
저장된 실행을 최신순으로 나열합니다. --session=ID는 TUI Fuzzer 세션 하나로 좁히고, --offset, --limit, --format text|json이 페이지와 형식을 정합니다 |
fuzz show RUN_ID |
실행 하나의 요약과, 보관된 BLOB을 읽지 않는 스칼라 전용 결과 지표 페이지를 보여 줍니다. --offset, --limit, --matched-only, --format text|json|jsonl을 지원하며, 진행 중인 실행에 --format json을 주면 보관 행 전체를 버퍼링하지 않고 유효한 배열 하나를 스트리밍합니다 |
fuzz show RUN_ID RESULT_INDEX |
보관된 요청/와이어/응답 바이트를 포함해 결과 하나를 정확히 보여 줍니다. 텍스트 출력은 터미널 제어 시퀀스를 무력화하고, JSON은 유효하지 않은 UTF-8을 base64로 내보냅니다. 상세 보기는 text 또는 json을 지원하며, 현재 형식 이전의 불완전한 스냅숏은 실행 메타데이터에 legacy로 표시됩니다 |
fuzz delete RUN_ID --yes |
종료된 실행 하나와 저장된 결과 행 전부를 삭제합니다. 저장이 진행 중이면 거부하며, --force-stale은 죽은 기록자가 남긴 running/saving 행을 지웁니다 — 다른 gori가 저장 중일 때는 절대 쓰면 안 됩니다 |
run mine
gori run mine <flow-id> --locations query,headers --wordlist params.txt
| Option | Description |
|---|---|
--flow, --request, --target, --sni, --http2, -k |
요청 소스와 트랜스포트 |
--locations=LIST |
query, form, multipart, json, headers, cookies (multipart는 기본 꺼짐, 명시해야 켜집니다) |
--wordlist, --bucket=N |
후보 이름과 버킷 크기 |
--concurrency (10), --rate, --throttle, --timeout, --retries (1), --max-requests=N |
속도 제어 |
--no-keep-alive |
연결 재사용 대신 프로브마다 새로 연결 |
--bind-from=FLOW-ID |
캡처된 그 플로우를 먼저 재생해, 응답이 남은 실행 동안 쓸 $NAME 세션 바인딩을 채우게 합니다 |
--slot=NAME |
이 세션 슬롯으로 전송 — 그 슬롯의 헤더 오버레이, 그리고 $NAME을 위한 그 슬롯의 바인딩 테이블. --bind-from보다 먼저 적용되므로 시드가 채우는 슬롯이 곧 실행이 나가는 슬롯입니다 |
--format |
text, json, 또는 jsonl |
기본적으로 연결을 재사용합니다. 마이닝 한 번이 프로브마다가 아니라 워커마다 TCP(https라면 TLS) 핸드셰이크를 한 번씩만 치릅니다. 실행이 끝날 때 나오는 connections · N dialed · M reused 줄에서 대상이 이를 지켰는지 확인할 수 있습니다. 대상이 연결 단위로 동작한다면 --no-keep-alive로 끕니다.
run sequence
토큰의 무작위성을 평가합니다. 라이브: 요청을 리플레이하며 각 응답에서 토큰을 추출합니다. 수동: --tokens로 붙여넣은 목록을 분석합니다(네트워크 없음). 별칭 seq.
gori run sequence 42 --cookie SESSIONID --count 500
gori run sequence --tokens tokens.txt # '-' reads stdin
| Option | Description |
|---|---|
--flow=ID, --request=FILE, stdin |
라이브 리플레이의 요청 소스(또는 맨 앞의 <flow-id>) |
--tokens=FILE |
붙여넣은 토큰 목록 분석(한 줄에 하나, -=stdin), 네트워크 없음 |
| 토큰 위치(하나만 선택) | --cookie=NAME, --header=NAME, --regex=RE, --position=A:B, --jsonpath=EXPR |
--count=N |
목표 토큰 개수(기본값 500) |
--target, --http2, --sni, -k |
트랜스포트(--request/stdin에는 target 필요) |
--concurrency (1), --rate, --throttle, --timeout, --retries, --max-requests=N |
속도 제어(상태 기반 토큰을 위해 concurrency는 1 유지) |
--bind-from=FLOW-ID |
캡처된 그 플로우를 먼저 재생해, 응답이 남은 실행 동안 쓸 $NAME 세션 바인딩을 채우게 합니다 |
--slot=NAME |
이 세션 슬롯으로 전송 — 그 슬롯의 헤더 오버레이, 그리고 $NAME을 위한 그 슬롯의 바인딩 테이블. --bind-from보다 먼저 적용되므로 시드가 채우는 슬롯이 곧 실행이 나가는 슬롯입니다 |
--format |
text, json, jsonl, 또는 markdown(TUI의 Export가 쓰는 리포트) |
run authorize
선택한 플로우를 아이덴티티마다 재전송합니다. 아이덴티티는 관리자 세션, 저권한 사용자, 익명 클라이언트를 대신하는 헤더 오버레이이며, 각 응답을 기준선과 비교합니다. 기준선이 받은 것을 그대로 받는 아이덴티티가 있다면 접근 제어 우회일 가능성이 높습니다. Authorize 탭의 헤드리스 버전입니다.
gori run authorize 12 13
gori run authorize --query 'host:acme.test method:GET' --identities identities.json
| Option | Description |
|---|---|
<flow-id>…, --flow=ID |
재전송할 캡처 플로우(지정한 순서대로, 반복 가능) |
-q, --query=QL |
QL 쿼리에 매칭되는 플로우도 재전송(id 뒤에 이어 붙습니다) |
-n, --limit=N |
--query가 기여할 수 있는 최대 플로우 수(기본값 50). 한 행이 아이덴티티 수만큼의 요청이 됩니다 |
--identities=FILE |
아이덴티티 집합 JSON(-=stdin). 기본값은 프로젝트에 저장된 집합 |
--unsafe-methods |
POST/PUT/PATCH/DELETE도 재전송 — 아이덴티티마다 부수 효과가 다시 실행됩니다 |
--allow-unscoped |
대상이 프로젝트 스코프 밖이어도 전송(샌드박스와 exclude는 그대로 적용) |
--timeout=SEC, -k/--insecure-upstream |
요청당 연결 + 유휴 타임아웃, 업스트림 TLS 검증 생략 |
--project, --db |
읽을 프로젝트 |
--format |
text(기본), json(마지막에 배열 하나), jsonl(스트리밍) |
--identities로 파일을 지정하지 않으면 아이덴티티는 프로젝트, 즉 TUI Authorize 탭의 목록에서 옵니다.
[{"name": "anonymous", "remove": ["Cookie", "Authorization"]},
{"name": "low-priv", "set": [{"name": "Cookie", "value": "session=…"}]}]
set은 헤더를 upsert하고 remove는 제거합니다. 어떤 항목도 "baseline": true를 갖지 않으면 캡처된 그대로의 요청이 기준선입니다. 기준선 외에 최소 한 개의 아이덴티티가 필요하며, 그렇지 않으면 비교할 것이 없습니다.
의미 있게 재전송할 수 없는 플로우는 아무것도 보내기 전에 이유와 함께 STDERR에 나열됩니다(no identity changes them, not a safe method to repeat, never completed, answered by gori, outside project scope, already queued). 선택한 플로우가 전부 건너뛰어지면 실행하지 않고 거부합니다. 모든 전송이 소켓을 열기 전에 거부되면 깨끗한 결과를 보고하는 대신 1로 종료하며 그 사실을 말합니다. 아무것도 보내지 않은 실행은 접근 제어가 동작한다는 증거가 아니기 때문입니다.
run session
프로젝트의 세션 슬롯 — 이름 붙은 신원 각각이 헤더 오버레이 하나와, 그 값을 묶어 주는 extract 규칙들로 이루어집니다. TUI Authorize 탭의 identities 카드가 편집하고 MCP의 *_session_slot 도구가 관리하는 바로 그 목록입니다. Authorize 실행은 모든 슬롯으로 재생하고, 전송은 --slot이 지목한 하나 로 나갑니다.
gori run session # 목록 (값은 [REDACTED])
gori run session show admin --show-values
gori run session add --name admin --set 'Cookie: session=…' --rule SESSION
gori run session edit admin --clear-set --set 'Cookie: session=new'
gori run session baseline as-captured
gori run session rm admin
| 동사 | 옵션 |
|---|---|
list(기본) |
--show-values([REDACTED] 대신 헤더 값 출력), --format text|json |
show <name> |
--show-values, --format text|json |
add |
--name, --set 'Name: value'(반복 가능), --remove NAME(반복 가능), --rule NAME(반복 가능), --baseline |
from-flow <flow-id> |
--name(필수), --baseline, --show-values. 오버레이를 직접 타이핑하는 대신 캡처된 로그인 교환에서 만듭니다 |
edit <name> |
같은 플래그에 --clear-set / --clear-remove / --clear-rules 추가. 컬렉션 플래그는 그 컬렉션 전체를 교체 하고, 생략한 것은 그대로 둡니다 |
rm|delete <name> |
그 슬롯이 주장하던 extract 규칙은 다시 전역 바인딩 테이블에 쓰게 됩니다 |
baseline <name> |
Authorize 기준선 이동(정확히 한 슬롯이 갖습니다) |
모든 동사가 --project=NAME / --db=PATH를 받습니다.
--set 값은 TUI 폼과 같은 헤더 파서를 지납니다. 이름은 RFC 7230 토큰이어야 하고 값에 CR이나 LF가 들어갈 수 없으며, 통과하지 못한 줄은 버려지지 않고 지목되어 거부됩니다.
from-flow는 캡처된 로그인에서 슬롯을 만듭니다. 로그인한 플로우를 지목하면 gori가 그 플로우의 응답 을 읽어 오버레이를 채웁니다. 모든 Set-Cookie의 name=value를 Cookie: 헤더 하나로 접고(속성은 버리며, 응답이 삭제 하는 쿠키는 건너뜁니다), 이어서 응답 자신의 Authorization을, 없으면 JSON 본문 최상위의 access_token / token / id_token 문자열을 Authorization: Bearer <value>로, 그것도 없으면 요청 자신의 Authorization을 씁니다.
gori run session from-flow 4211 --name admin
gori run repeater 900 --slot admin # 플로우 900을 그 신원으로 재전송
오버레이는 리터럴 입니다 — 로그인이 돌려준 바이트 그대로 프로젝트에 저장됩니다. 재인증은 하지 않으므로, 회전하는 토큰(수명 짧은 JWT, 요청마다 바뀌는 CSRF 값)은 extract 규칙 경로가 맞습니다: gori run rewriter extract에 --bind-from FLOW를 더하면 실행마다 값을 새로 발급받습니다. 이름은 플로우를 읽기 전에 검사하므로, 중복된 이름은 "그 플로우는 로그인이 아니다"가 아니라 이름 충돌로 보고됩니다.
session activate는 없습니다. gori run 프로세스는 보내고 끝나므로 활성 포인터가 걸칠 시간이 없고, 저장해 두면 다음 실행에서 비어 있는 바인딩 테이블로 해소되어 $SESSION이 리터럴인 오버레이를 보내게 됩니다. 대신 전송할 때 신원을 지목하세요 — repeater, fuzz, mine, sequence, discover에서 --slot NAME. 실행은 첫 요청 전에 STDERR로 slot: sending as NAME을 찍습니다.
run probe
gori run probe --severity high --category cors
gori run probe -a
--severity는 info|low|medium|high|critical 중 하나입니다. --category는 headers|cookies|tech|infoleak|cors|client|active입니다. 기본적으로 패시브 검사를 수행하며, -a/--active 옵션을 사용하여 액티브 프로브 검사를 포함할 수 있습니다. -q/--query로 QL 필터를 겁니다. --lenient는 없는 필드 이름을 쓴 쿼리를 거절하지 않고 받아들입니다. --in-scope는 프로젝트 스코프 안의 호스트에 대한 이슈만 보고합니다 — TUI의 ⇧S 렌즈로, --active/--allow-unscoped와 무관하게 옵트인이며 모든 플로우는 여전히 스캔됩니다.
--active와 함께: --unsafe는 안전하지 않은 메서드(POST/PUT/PATCH/DELETE)도 프로브하며, 이 재전송은 서버 데이터를 변경할 수 있습니다. --aggressive는 룰별 상한을 높이고 forbidden-bypass 헤더 집합을 넓힙니다(그리고 --unsafe를 함의합니다). 둘 다 --allow-unscoped를 함께 주지 않는 한 스코프 게이트를 따릅니다. 인가된 대상에만 사용하세요.
probe만 쓰면 스캔하고 출력합니다. TUI Probe 탭 뒤에 저장되는 발견 항목은 별개의 표면입니다.
gori run probe issues --severity high # 아래 동사들이 받는 id가 함께 나오는 트리아지 목록
gori run probe promote 12 # 하나를 Issue로 확정
gori run probe dismiss --code missing-hsts # 룰 코드나 --host로 일괄 무시
gori run probe delete --all --yes
gori run probe rules --kind active # 스캔 룰 목록과 무장 여부
gori run probe rules enable <rule-id> # id는 `probe rules`에서
gori run probe mode passive # off | passive | active | aggressive
| Verb | Options |
|---|---|
issues |
-a/--all(무시·확정·해결된 항목 포함), --severity, --category, --host |
dismiss <id> |
또는 --code=CODE / --host=HOST로 일괄 |
promote <id> |
발견 항목을 사람이 확인한 Issue로 승격 |
delete <id> |
또는 --all --yes |
rules [list|enable|disable|add|delete] |
list는 --kind=passive|active|custom. enable/disable/delete는 그 목록의 <rule-id>를 받습니다. add는 -t/--title, -p/--pattern, --description, --side(request|response), --region(whole|header|body), --regex, --exec(--pattern을 프로세스 훅으로 실행: exit 0이면 발견, stdout이 근거), -s/--severity |
mode [off|passive|active|aggressive] |
프로젝트의 스캔 모드를 출력하거나 설정 |
run discover
대상을 스파이더링하고 링크되지 않은 경로를 브루트포스합니다. --no-store가 아니면 결과는 Sitemap으로 반영됩니다. 실제 요청을 무단으로 보내므로 권한이 있는 대상에만 실행하세요.
gori run discover --target https://target.example --max-depth 3 --extensions php,json,bak --format jsonl
| Option | Description |
|---|---|
--target=URL |
탐색할 시드 origin 또는 경로 하위 트리(필수) |
--max-depth=N |
시드로부터의 스파이더 깊이(기본값 4) |
--no-spider / --no-bruteforce |
링크 크롤링 / 디렉터리 브루트포스 비활성화 |
--wordlist=PATH |
내장 목록과 병합할 추가 경로 워드리스트 |
--extensions=LIST |
이 확장자도 프로브(예: php,json,bak) |
-H, --header=HEADER |
모든 프로브에 붙일 커스텀 헤더(반복 가능) |
--containment=MODE |
same-origin | scope-aware(기본) | host+subdomains |
--concurrency (20), --rate, --throttle, --timeout, --retries, --max-requests=N |
속도 제어 |
--no-keep-alive |
origin별 연결 재사용 대신 프로브마다 새로 연결 |
-k, --insecure-upstream |
업스트림 TLS 검증 생략 |
--bind-from=FLOW-ID |
캡처된 그 플로우를 먼저 재생해, 응답이 남은 실행 동안 쓸 $NAME 세션 바인딩을 채우게 합니다 |
--slot=NAME |
이 세션 슬롯으로 전송 — 그 슬롯의 헤더 오버레이, 그리고 $NAME을 위한 그 슬롯의 바인딩 테이블. --bind-from보다 먼저 적용되므로 시드가 채우는 슬롯이 곧 실행이 나가는 슬롯입니다 |
--allow-unscoped |
대상이 프로젝트 스코프 밖이어도 실행. 사전(Layer 1) 검사만 면제되며 Sandbox 모드와 명시적 exclude 룰은 매 전송마다 그대로 거부합니다. 거부 메시지는 둘 중 어느 게이트가 막았는지 이름을 밝힙니다. |
--force |
무제한 실행 안전 게이트 우회 |
--no-store |
결과를 프로젝트에 기록하지 않음 |
--format |
text, json, 또는 jsonl |
기본적으로 origin별로 연결을 재사용합니다. 브루트포스 한 번이 프로브마다가 아니라 워커마다 TCP(https라면 TLS) 핸드셰이크를 한 번씩만 치릅니다. 실행이 끝날 때 나오는 connections · N dialed · M reused 줄에서 대상이 이를 지켰는지 확인할 수 있습니다. 대상이 연결 단위로 동작한다면 --no-keep-alive로 끕니다.
명령줄에서 세션 바인딩 쓰기
세션 바인딩(로그인 응답에서 채워지는 $SESSION 같은 것 — 세션 바인딩 참고)은 그것을 관측한 gori 프로세스의 메모리에만 존재합니다. settings.json에도, 프로젝트 데이터베이스에도 기록되지 않습니다. 복원된 토큰은 이미 낡은 것이고, 다시 추출하는 비용은 요청 한 번이기 때문입니다.
gori run은 호출마다 프로세스 하나이며, 스윕은 의도적으로 추출 소스가 아닙니다(공격 페이로드를 그대로 되비추는 응답이 세션을 그 값으로 바꿔버릴 수 있기 때문입니다). 그래서 선언된 바인딩을 참조하는 헤드리스 fuzz / mine / sequence / discover 템플릿은 그것을 채울 수단이 없어, 전송 전에 거부됩니다.
--bind-from FLOW-ID가 그 빠진 단계입니다. 캡처된 플로우 하나 — 로그인 — 를 의도적 전송 경로로 재생해 그 응답이 바인딩 테이블을 채우게 하고, 같은 프로세스 안에서 스윕을 이어 실행합니다.
gori run fuzz 42 --wordlist ids.txt --bind-from 17
# bind-from: flow #17 replayed → bound $SESS
하나의 stdio 세션에서 gori mcp 도구를 두 번 호출하는 경우도 원래부터 같은 방식으로 동작합니다.
run import
프로젝트의 History로 플로우를 일괄 임포트합니다. TUI의 Import 오버레이에 대응하는 CLI입니다(Proxy & History → 임포트 참고). 소스 플래그는 정확히 하나만 지정해야 하며, 트래픽은 전혀 보내지 않습니다.
gori run import --postman api.postman_collection.json --db ./assessment.db --format json
| Option | Description |
|---|---|
--har=PATH |
브라우저/프록시 HAR(HTTP Archive) 익스포트 — 전체 요청/응답 플로우 |
--urls=PATH |
한 줄에 URL 하나씩 담긴 텍스트 파일(# 주석과 빈 줄은 무시) |
--oas=PATH |
OpenAPI/Swagger 스펙(JSON 또는 YAML) — 오퍼레이션마다 템플릿 하나 |
--postman=PATH |
Postman Collection v2 익스포트(JSON) |
--insomnia=PATH |
Insomnia v4 익스포트(JSON) |
--burp=PATH |
Burp Suite 항목 익스포트(XML) — 요청 과 응답, 바이트 단위 그대로 |
--wsdl=PATH |
WSDL 1.1 서비스 설명서(XML) — 오퍼레이션마다 SOAP 요청 템플릿 하나 |
--project=NAME |
임포트할 프로젝트(기본값: 가장 최근에 사용한 프로젝트) |
--db=PATH |
임포트할 SQLite db 파일을 직접 지정(없으면 생성) |
--format |
text(기본) 또는 json |
임포트는 플로우를 기록하므로 discover와 같은 방식으로 대상을 정합니다. --db를 주면 생성하거나 다시 열고, 주지 않으면 기본 프로젝트를 몰래 만들지 않고 기존 프로젝트에 씁니다.
형식이 잘못된 항목은 파일 전체를 중단시키지 않고 건너뛰며, 결과에 양쪽 개수가 모두 담깁니다({"count": 12, "skipped": 3}). 응답까지 가져오는 것은 --har와 --burp뿐이고, 나머지는 요청 템플릿이라 보내기 전까지 History에서 Pending으로 보입니다.
run sitemap
gori run sitemap --in-scope --format paths
-q/--query=QL는 history와 같은 QL로 엔드포인트를 거릅니다(위치 인자로도 넘길 수 있습니다). -n/--limit=N은 스캔할 엔드포인트 수를 제한합니다(기본값 SITEMAP_MAX). --in-scope는 스코프 내 호스트로 한정하고, --no-group은 id 접기를, --no-fold-query는 쿼리 문자열 접기를 끕니다(서로 다른 축입니다). --format은 text(트리), json, paths 중에서 고릅니다. --lenient는 없는 필드 이름을 쓴 쿼리를 거절하지 않고 받아들입니다.
sitemap tag: 경로 하나에 자유 텍스트 메모를 고정합니다. TUI Sitemap에 보이는 그 메모입니다.
gori run sitemap tag --host api.example.com --path /v1/users --tag "IDOR candidate"
gori run sitemap tag --host api.example.com --path /v1/users --clear
gori run sitemap tag --list
run oast
아웃오브밴드 리스너입니다. listen은 즉석에서 쓰는 저장소 없는 리스너로 페이로드를 등록하고 출력한 뒤 콜백을 스트리밍합니다. list / resume / release는 프로젝트가 저장한 세션 — TUI의 RESUME LISTENER 피커가 보여주는 것과 같은 행 — 을 다룹니다.
gori run oast presets # list built-in public providers
gori run oast listen # interactsh, poll until Ctrl-C
gori run oast listen --provider webhook.site --once --json
presets는 공개 프로바이더를 나열합니다. listen 옵션:
| Option | Description |
|---|---|
--provider=KIND |
interactsh(기본) | custom-http | webhook.site | BOAST | postbin |
--server=URL |
프로바이더 서버 / 베이스 URL(기본값: 프로바이더의 공개 프리셋) |
--token=TOK |
선택적 프로바이더 인증 토큰 |
--interval=SEC |
폴링 간격(기본값 5) |
--once |
한 번만 폴링하고 종료 |
--json |
각 콜백을 JSON 라인으로 출력(MCP와 동일한 형태) |
oast list / resume / release: 프로젝트에 저장된 리스닝 세션입니다(아래의 프로바이더는 어디서 듣는지를, 세션은 그 위의 살아 있는 등록 하나를 뜻합니다). 등록은 그것을 만든 프로세스보다 오래 남고, 그래서 어제 심어둔 페이로드를 오늘도 지켜볼 수 있습니다.
gori run oast list # id, provider, payload host, hits, last poll
gori run oast list --format json
gori run oast resume 7 # 세션 #7 재개 후 콜백 스트리밍
gori run oast resume 7 --once --json # 한 번만 폴링하고 JSON 라인 출력 후 종료
gori run oast release 7 # 서버 측 등록 해제
resume과 release는 세션 id(7, 또는 list가 출력하는 #7)를 받습니다. resume은 서버 측 상태를 다시 살려 이미 심어둔 페이로드가 계속 resolve되게 한 뒤 폴링합니다. 받은 콜백은 모두 프로젝트에 기록되므로 TUI OAST 탭에서 같은 hit를 보게 되고, last_poll_at도 TUI 리스너처럼 갱신됩니다. Ctrl-C는 폴링만 멈추고 등록은 유지합니다. 정리는 release로 명시적으로 하며, 어느 쪽이든 저장된 콜백은 남습니다. 자동으로 재개되는 것은 없습니다.
| Option | Description |
|---|---|
--project=NAME · --db=PATH |
어느 프로젝트의 세션인지(기본값: 가장 최근에 사용한 프로젝트) |
--format=FMT |
list에서: text(기본) 또는 json |
--interval=SEC |
resume에서: 폴링 간격(기본값 5) |
--once |
resume에서: 한 번만 폴링하고 종료 |
--json |
resume에서: 페이로드와 각 콜백을 JSON 라인으로 출력 |
oast providers: 위의 즉석 listen과 달리 프로젝트에 저장되는 프로바이더입니다. 동사: list(기본), add, update, enable, disable, delete(rm).
gori run oast providers # 토큰은 [REDACTED]로 출력
gori run oast providers add --name lab --kind custom-http --host https://oast.lab.internal
gori run oast providers enable p_1
enable, disable, update, delete는 표시 이름이 아니라 프로바이더 id(p_1 또는 그냥 1)를 받습니다. add가 부여한 id를 출력하고, list에도 나옵니다.
| Option | Description |
|---|---|
--name=NAME |
표시 이름. add에서는 필수 |
--kind=KIND |
interactsh(기본) | custom-http | webhook.site | BOAST | postbin |
--host=URL |
서버 / 베이스 URL(기본값: 해당 종류의 공개 프리셋) |
--token=TOK |
프로바이더 인증 토큰 |
--enabled / --disabled |
add / update 시 프로바이더를 켜거나 끔 |
--show-tokens |
list에서 [REDACTED] 대신 토큰을 그대로 출력 |
run jwt
JWT를 디코드, 재서명, 또는 공격 페이로드를 생성합니다. 저장소 없는 계산이며, 토큰은 <token> 인자나 stdin에서 받습니다.
gori run jwt eyJhbGci... # decode (default)
gori run jwt eyJhbGci... --encode --alg HS256 --secret s3cret
gori run jwt eyJhbGci... --encode --set role=admin --secret s3cret
gori run jwt eyJhbGci... --attacks
| Option | Description |
|---|---|
--decode |
header / payload / signature 디코드(기본) |
--encode |
--alg / --secret로 토큰 클레임 재서명 |
--attacks |
테스트 페이로드 생성(alg:none, weak-secret, header injection) |
--alg=ALG |
--encode용 서명 alg: HS256(기본) | HS384 | HS512 | none |
--secret=SECRET |
HS 알고리즘 --encode용 HMAC 시크릿 |
--payload=JSON |
--encode: 재서명 전에 클레임을 통째로 교체(--set과 상호 배타적) |
--set=CLAIM |
--encode: 재서명 전에 클레임 하나를 key=value로 패치, 반복 가능; 값이 JSON으로 파싱되면(true/3) 그 타입, 아니면 문자열 |
--format |
text(기본) 또는 json |
run cookie
서명된 Flask / Rack / Django 세션 쿠키를 디코드, 검증, 브루트포스, 위조합니다. 저장소 없는 계산이며, 쿠키는 <cookie> 인자나 stdin에서 받습니다.
gori run cookie 'eyJ1c2VyIjoi...' # 기본은 decode, 형식은 자동 판별
gori run cookie 'eyJ1c2VyIjoi...' --crack --wordlist secrets.txt
gori run cookie --forge --type flask --secret s3cret --payload '{"user":"admin"}'
| Option | Description |
|---|---|
--decode |
payload / timestamp / signature로 파싱(기본) |
--verify |
--secret으로 서명 검증 |
--crack |
--secrets 또는 --wordlist로 시크릿 브루트포스 |
--forge |
--payload(Rack은 --value)를 --secret으로 재서명 |
--type=T |
flask | rack | django(기본: 자동 판별) |
--secret=S, --secrets=LIST, --wordlist=PATH |
서명 시크릿, 쉼표로 구분한 후보 목록, 또는 줄 단위 파일 |
--payload=JSON |
서명할 세션 JSON(Flask / Django --forge) |
--value=B64 |
base64 Marshal 쿠키 값(Rack --forge, 불투명) |
--salt=SALT |
Flask / Django 서명 솔트 |
--algorithm=ALG |
Django HMAC 알고리즘: sha256(기본) 또는 sha1 |
--timestamp=UNIX |
--forge에 찍을 유닉스 초(기본: 현재) |
--format |
text(기본) 또는 json |
run decoder
값에 대해 Decoder 체인을 실행합니다. 단계는 |, >, ,로 구분합니다.
exec:COMMAND로 쓴 단계는 컨버터가 아니라 외부 프로세스 훅입니다.
현재 값이 COMMAND의 stdin으로 가고 그 stdout이 단계의 출력이 됩니다. 셸 없이 exec되므로 세 구분자는
인자 안에 넣을 수 없습니다.
gori run decoder 'base64-decode | jwt-decode' "$TOKEN"
echo -n secret | gori run decoder 'sha256 | hex-encode'
gori run decoder 'base64-decode > exec:./parse-envelope --json' "$BLOB"
gori run decoder list # every converter (name, category, direction)
| Option | Description |
|---|---|
--input=STR |
변환할 값(없으면 두 번째 위치 인자, 그것도 없으면 stdin) |
-o, --output=MODE |
최종 바이트 렌더링: auto(기본) | text | base64 | hex |
--format |
text(기본) 또는 json(단계별 상세) |
run issues / notes
gori run issues --format markdown --export report.md
gori run issues --format sarif --export issues.sarif # GitHub code scanning / CI 대시보드에 업로드
gori run notes --all
스크립트에서 create / update로 이슈를 작성합니다:
gori run issues create --title "Reflected XSS on /search" --cvss 8.8 --host app.example.com --flow 42
gori run issues update 7 --status confirmed --notes "Verified on staging" --severity critical
gori run issues delete 7
| Option | Description |
|---|---|
--format |
text(기본) | json | markdown | sarif — TUI의 Export가 쓰는 것과 같은 리포트 |
--export=PATH |
STDOUT 대신 PATH에 기록(바이트 그대로. STDOUT은 이스케이프를 제거) |
create |
-t/--title (필수), --cvss (점수 또는 벡터. 미지정 시 severity 자동 산정), -s/--severity (info|low|medium|high|critical), --host, --flow=ID |
update <id> |
-t/--title, --cvss (새 점수/벡터. 빈 문자열로 초기화), -s/--severity, -n/--notes, --status (open|confirmed|false-positive|resolved) |
delete <id> |
이슈와 그 증거 링크를 삭제합니다. 보고서에는 남기고 닫힌 상태로만 표시하려면 update <id> --status=resolved를 쓰세요 |
--format sarif는 SARIF 2.1.0 로그를 씁니다. GitHub code scanning, DefectDojo, Azure DevOps가 그대로 읽는 형식입니다. 이슈 하나가 result 하나가 되며, severity는 SARIF level로 매핑되고(5단계 원본은 rank와 룰의 security-severity에 보존), false-positive/resolved 상태는 suppression으로 나가 정리한 이슈가 다시 열린 것으로 보이지 않습니다. 연결된 플로우는 실제 헤더와 (디코딩·64 KiB 상한) 본문을 담은 webRequest/webResponse로 함께 실립니다.
노트도 읽고 쓸 수 있습니다. 인자 없이 notes를 실행하면 목록을 보여주고(*가 활성 노트), notes <n>은 인덱스로 하나를 출력합니다:
gori run notes # 목록
gori run notes 2 # 2번 노트 출력
gori run notes create --text "SSRF candidate on /fetch"
echo "pasted from a scratchpad" | gori run notes create
gori run notes delete 2
| Option | Description |
|---|---|
list |
--all은 요약 한 줄 대신 모든 노트를 전문으로 출력 |
create |
--text=TEXT, 위치 인자, 또는 STDIN |
delete <n> (rm) |
인덱스 n의 노트 삭제 |
run links
이슈나 노트가 가리키는 증거입니다. 캡처된 플로우, Repeater 세션, Fuzz / Miner 실행이 대상이 됩니다. Markdown 이슈 내보내기는 이미 이 포인터를 해석해 넣고, 여기서는 목록 조회와 편집을 합니다.
gori run links --owner=issue --id=7
gori run links add --owner=issue --id=7 --ref=flow --ref-id=42
gori run links delete --owner=note --id=2 --ref=repeater --ref-id=3
| Option | Description |
|---|---|
--owner=KIND |
소유자 종류: issue (기본값) 또는 note |
--id=N |
소유 이슈 / 노트 id. 필수 |
--ref=KIND |
add / delete의 대상 종류: flow, repeater, fuzz, miner |
--ref-id=M |
add / delete의 대상 id |
--format=FMT |
list에서 text (기본값) 또는 json |
대상이 정리(prune)된 포인터는 사라지지 않고 (stale)로 표시되므로, "증거가 없음"과 "증거가 사라짐"을 구분할 수 있습니다. add는 멱등이며, 양쪽 대상이 모두 존재해야 합니다.
run rewriter
스크립트에서 Match & Replace 규칙을 관리합니다. Rewriter 탭이 편집하는 것과 같은 규칙이며, 실시간 프록시 트래픽에 적용됩니다:
gori run rewriter # 적용 순서대로 규칙 목록
gori run rewriter add --op set_header --target request \
--find X-Forwarded-For --value 127.0.0.1 --host '*.example.com'
gori run rewriter add --op replace --target response --part body \
--match regex --find 'secret=(\w+)' --value 'secret=[redacted]'
gori run rewriter add --op remove_header --target response \
--find Content-Security-Policy --scope global # 모든 프로젝트에 적용
gori run rewriter preview --op replace --part body --find password --value hunter2
gori run rewriter disable 3
gori run rewriter disable 2 --scope global # 이 프로젝트에서만 끄기
gori run rewriter disable 2 --scope global --everywhere # 기본값을 꺼서 모든 곳에 적용
gori run rewriter rm 3
| Option | Description |
|---|---|
--op=OP |
replace(기본값), add_header, set_header, remove_header, short_circuit, pipe |
--target=SIDE |
request(기본값) 또는 response |
--part=PART |
head(기본값), body, 또는 ws(WebSocket 메시지). replace와 pipe에서만 의미가 있음 |
--match=MODE |
literal(기본값) 또는 regex. replace, pipe, short_circuit에 적용됩니다. 정규식 치환은 $1, $2를 쓰고 $$는 리터럴 $ |
--response-file=PATH |
short_circuit: 미리 준비한 응답을 PATH에서 읽음(-는 stdin) |
--body-file=PATH |
short_circuit: PATH를 응답 본문으로 제공하며, 파일이 바뀌면 다시 읽음 |
-f, --find=FIND |
필수. 대상이 되는 리터럴, 패턴, 또는 헤더 이름 |
-v, --value=VALUE |
치환할 텍스트, 헤더 값, 또는 --op=pipe일 때 실행할 명령. 프로세스 훅 참고 |
--host=GLOB |
매칭되는 호스트로 규칙을 한정(부분 문자열, * 와일드카드). 생략하면 전체 적용 |
--name=NAME |
규칙 목록에 표시할 라벨 |
--disabled |
규칙을 만들되 활성화하지 않음 |
--scope=SCOPE |
project(기본값) 또는 global. 전역 규칙은 settings.json에 저장되어 모든 프로젝트에 적용됨 |
--everywhere |
전역 규칙의 enable/disable에서, 이 프로젝트의 오버라이드 대신 규칙 자체의 기본값을 변경 |
preview는 같은 규칙 플래그를 받아, 규칙을 저장하지 않고 저장된 플로우 중 몇 개가 바뀌었을지 보고합니다. rm(delete), enable, disable은 목록의 규칙 id와 함께 --scope도 받습니다. 두 저장소가 규칙 번호를 각자 매기므로 id 하나가 서로 다른 두 규칙을 가리키기 때문입니다. 목록은 범위를 G/P 접두어로 출력하고(G*는 이 프로젝트가 해당 전역 규칙의 기본값을 오버라이드했다는 뜻), 프록시가 적용하는 순서 그대로 전역 규칙을 먼저 보여 줍니다. 전역 규칙과 프로젝트 규칙을 참고하세요.
본문 규칙은 필요에 따라 Content-Length를 다시 맞추고 청크를 해제하며, 활성화된 규칙은 매칭되는 호스트에서 HTTP/1.1을 강제합니다. 대화형 편집기는 Proxy & History를 참고하세요.
rewriter preset: 응답 수정 프리셋 설치 — 평범한 Match & Replace 규칙을 써 주는 이름 붙은 출발점입니다. 동사: list, add <name>.
gori run rewriter preset list
gori run rewriter preset add unhide-hidden-fields
gori run rewriter preset add remove-csp --scope global --disabled
이름은 unhide-hidden-fields, enable-disabled-fields, remove-length-limits, strip-validation, remove-csp, remove-security-headers, disable-sri입니다. add는 --scope=project|global과 --disabled(무장하지 않고 설치해 먼저 검토)를 받습니다. 설치되는 규칙은 rewriter add와 같은 경로를 지나므로 이후에도 목록에 나오고 편집·삭제됩니다. 같은 프리셋을 두 번 설치하면 병합되지 않고 눈에 보이게 중복됩니다.
rewriter extract: 세션 바인딩을 선언하는 규칙입니다. $NAME을 어느 응답의 어디에서 읽을지 정합니다. 동사: list(기본), add, rm(delete), enable, disable.
gori run rewriter extract add --name SESS --kind cookie --selector session --host '*.example.com'
gori run rewriter extract add --name CSRF --kind regex --selector 'name="csrf" value="([^"]+)"'
| Option | Description |
|---|---|
--name=NAME |
$를 뺀 바인딩 이름. 필수 |
--kind=KIND |
cookie(기본), header, regex, position, jsonpath |
--selector=SEL |
쿠키 / 헤더 이름, 정규식, 또는 JSON 경로 |
--range=A:B |
position 전용: 디코드된 본문의 반열린 바이트 범위 |
--when=FILTER |
어떤 메시지를 읽을지, 인터셉트 필터 문법으로(''는 전부) |
--host=GLOB |
호스트 글롭으로 한정(''는 전부) |
--disabled |
규칙을 만들되 활성화하지 않음 |
rewriter bindings: 그 규칙들이 선언한 이름을 나열합니다(--format text|json). 값은 여기에 나오지 않으며, 나올 수도 없습니다. 바인딩 값은 실행 중인 gori의 메모리에만 있고 어디에도 기록되지 않으므로 다른 프로세스가 읽을 것이 없기 때문입니다. 살아 있는 값 테이블은 Rewriter 탭의 bindings 하위 탭에서 봅니다. 헤드리스 스윕에서는 --bind-from이 같은 프로세스 안에서 값을 채웁니다 — 명령줄에서 세션 바인딩 쓰기를 참고하세요.
run grpc
gRPC .proto 렌즈를 명령줄에서: 이 프로젝트가 캡처된 gRPC를 어떤 스키마로 렌더하는지, 각 조각이 어디서 왔는지.
gori run grpc # 무엇이 로드됐는지 (schema가 기본 동사)
gori run grpc schema --format json
gori run grpc reflect https://api.test:443 # ACTIVE: 대상에게 디스크립터를 요청
gori run grpc forget https://api.test:443 # 캐시된 대상 하나 버리기 (`rm`도 받습니다)
gori run grpc forget --all
schema와 forget은 프로젝트 DB 밖을 건드리지 않습니다. 보내는 쪽은 reflect 하나입니다. 대상의 grpc.reflection.v1 서비스에 — 없으면 v1alpha로, 실제 배포된 서버 대부분은 아직 이쪽입니다 — 서비스 목록, 각 서비스를 선언한 파일, 그 파일들의 import 순으로 그래프가 닫힐 때까지 요청해 결과를 프로젝트에 캐시합니다. 다른 액티브 gori run 명령과 같은 스코프 게이트를 지나므로 범위를 벗어난 대상은 다이얼러에 닿기 전에 거부됩니다. 두 리플렉션 버전 모두 응답하지 않는 서버는 조용히 실패하지 않고 그렇다고 말하며, 무엇도 스스로 다시 받아오지 않습니다.
| 옵션 | 설명 |
|---|---|
--format=FMT |
schema와 reflect에서 text(기본) 또는 json |
--allow-unscoped |
reflect: 대상이 프로젝트 스코프 밖이어도 보냅니다 |
-k, --insecure-upstream |
reflect: 대상의 TLS 인증서를 검증하지 않습니다 |
--timeout=SECONDS |
reflect: 작업당 타임아웃(기본값: 프로젝트의 io 타임아웃) |
--all |
forget: 캐시된 리플렉션 대상 전부 버리기 |
디스크립터 셋 파일(Project settings → Proto schema)은 forget으로 내려가지 않습니다. 프로젝트 설정에서 경로를 지우세요. 파일과 리플렉션 페치가 어떤 선언에 대해 어긋나면 그 수는 redefined로 보고되고 대상 자신의 말이 우선합니다.
run colormarker
Colormarker 규칙을 관리합니다. 캡처된 History의 어떤 행을 어떤 방식으로 칠할지 정하는 규칙이며, 표시 전용입니다. 트래픽을 전혀 수정하지 않으므로 Match & Replace 규칙과 달리 잘못 써도 목록이 오해를 부를 뿐, 메시지가 바뀌지는 않습니다.
gori run colormarker # 우선순위 순으로 규칙 목록
gori run colormarker add --when 'status:>=500' --color red --style full --name 'prod 5xx'
gori run colormarker add --when 'host:cdn' --color blue --style strip --scope global
gori run colormarker move 2 --up # 우선순위 올리기
gori run colormarker preview --when 'method:DELETE'
gori run colormarker disable 1 --scope global # 이 프로젝트에서만 끄기
gori run colormarker disable 1 --scope global --everywhere # 모든 프로젝트의 기본값을 끄기
gori run colormarker rm 3
| 옵션 | 설명 |
|---|---|
-w, --when=FILTER |
필수. 플로우가 만족해야 할 조건 (아래 참고) |
--color=NAME |
red, orange, yellow(기본), green, blue, purple. 활성 테마 팔레트로 해석되므로 밝은 테마와 어두운 테마 양쪽에서 제대로 읽힙니다 |
--style=STYLE |
full(기본)은 행 전체 배경을 칠하고, strip은 TIME 앞 좁은 컬럼에 색 셀 하나를 칠합니다 |
--name=NAME |
규칙 목록에 표시할 라벨 |
--disabled |
비활성 상태로 생성 |
--scope=SCOPE |
project(기본) 또는 global. 전역 규칙은 settings.json에 저장되어 모든 프로젝트에 적용됩니다 |
--everywhere |
전역 규칙의 enable/disable 시: 이 프로젝트의 오버라이드가 아니라 규칙 자체의 기본값을 변경 |
--up / --down |
move 시: 우선순위를 올리거나 내림 |
우선순위가 곧 규칙 집합의 의미입니다. Match & Replace 규칙은 합성되어 활성화된 모든 규칙이 순서대로 실행되지만, 색상 규칙은 해석됩니다. 첫 번째로 매칭되는 활성 규칙이 행을 칠하고 나머지는 조회조차 되지 않습니다. move가 rewriter에는 없고 여기에만 있는 이유입니다. 전역 규칙이 프로젝트 규칙보다 먼저 해석되므로, 상시 정책이 로컬 레이어보다 우선합니다.
--when은 조건부 인터셉트 바가 쓰는 것과 같은 불리언 문법입니다. host: path: method: scheme: status: proto:에 AND / OR / NOT, -부정, (그룹)을 더한 형태이며 캡처된 플로우 행에 대해 평가됩니다. 그냥 두면 조용히 실패할 세 가지가 있어, gori는 거부하거나 경고합니다.
body:는 여기서 절대 매칭되지 않습니다. History 행에는 payload가 없습니다. (거부가 아니라 경고 — 문법상 적법한 항이기 때문입니다.)host:는 DNS 레이블 글롭이 아니라 부분문자열입니다.host:alpha.test는xalpha.test도 매칭합니다. (경고)header:/size:/dur:/url:/stub:는 없습니다. 이들은 쿼리가 필요한 History QL 필드이고, 여기는 렌더 경로에서 평가됩니다. 모르는 필드는 거부됩니다. 그냥 두면 조용히 자유 텍스트 검색이 되어 규칙이 영원히 발동하지 않습니다.
모든 플로우에 매칭되는 조건(빈 값이나 입력 중인 host:)도 거부됩니다.
preview는 조건이 최근 플로우 중 몇 개에 매칭되는지와, 실제로 몇 개를 칠하게 되는지를 함께 보고합니다. 앞선 활성 규칙이 이미 그 행을 차지했다면 두 숫자가 달라집니다. rm(delete), enable, disable, move는 목록의 규칙 id와 --scope를 받습니다. 두 저장소가 서로 독립적으로 번호를 매기므로 id만으로는 서로 다른 두 규칙을 가리키기 때문입니다. 목록은 스코프를 G/P 접두사로 출력합니다(G*는 이 프로젝트가 해당 전역 규칙의 기본값을 오버라이드했다는 뜻).
탭은 기본적으로 숨겨져 있습니다. settings:tabs에서 Rewriter 옆에 표시할 수 있습니다. 대화형 편집기는 프록시 & History를 참고하세요.
colormarker color
사용자 색상 팔레트입니다. 내장 6색 위에 얹어 모든 프로젝트의 색상 선택기에 함께 제공되는 이름 있는 색상입니다. 내장 색은 활성 테마를 거쳐 해석되므로 밝은 팔레트와 어두운 팔레트 양쪽에서 제대로 읽히지만, 사용자 색상은 절대 hex 값을 그대로 지니며 테마를 따라가지 않습니다. 팔레트가 주지 않는 색조를 얻는 대신 치르는 대가입니다. 색상은 settings.json(colormarker.colors)에 저장되므로 태생적으로 전역입니다.
gori run colormarker color list
gori run colormarker color add --name hotpink --hex '#ff69b4'
gori run colormarker color update hotpink --hex '#e0559b' # 이름은 두고 색만 변경
gori run colormarker color update hotpink --name fuchsia # 색은 두고 이름만 변경
gori run colormarker color rm fuchsia
gori run colormarker add --when 'method:DELETE' --color hotpink
이름이 곧 식별자입니다. 규칙의 --color에 저장되는 값이자 선택기에 보이는 값이므로 소문자로 정규화되고, 중복될 수 없으며, 내장 색 이름과 같을 수 없습니다. update는 두 옵션 중 하나만 줘도 됩니다.
색상을 지우거나 이름을 바꿔도 그 색을 쓰던 규칙은 의도적으로 고쳐 쓰지 않습니다. 규칙은 옛 이름을 그대로 들고 있다가 눈에 띄는 기본색으로 대체되어 그려지므로, 같은 이름으로 색을 다시 추가하면 원래대로 돌아옵니다. 이 명령에서 모든 프로젝트의 데이터베이스에 손을 뻗을 수는 없고, 절반만 적용된 연쇄 수정은 이름 하나가 붕 뜨는 것보다 나쁩니다. 색상 값만 바꾸는 경우는 다릅니다. 규칙은 색을 이름으로 참조하므로 어디서든 새 hex를 그대로 따라갑니다.
run views
History 뷰를 관리합니다 — History 목록을 좁히는, 이름 붙은 QL 쿼리입니다. 뷰는 렌즈입니다. 다른 필터를 대체하지 않고 그 위에 AND로 얹히므로, gori run history --view History -q 'status:5xx'는 둘 다를 뜻합니다. 기본 뷰 일곱 개가 모든 프로젝트에 들어 있습니다 — 출처 3종 All / History(src:proxy) / History + Repeater(기본값), 그리고 WebSocket·gRPC·SSE·Errors. 저장된 뷰는 컬러 룰과 똑같이 두 저장소에 나뉘어 삽니다.
gori run views # 목록; TUI의 활성 뷰에 ● 표시
gori run views --scope global --format json
gori run views add 'acme errors' -q 'host:api.acme.test status:5xx'
gori run views add 'proxied' -q 'src:proxy' --scope global
gori run views set 'acme errors' -q 'status:>=500' # 이름은 그대로, 쿼리만 교체
gori run views rename 'acme errors' --to 'acme 5xx'
gori run views scope 'acme 5xx' --to global # 두 저장소 사이로 옮기기
gori run views rm 'acme 5xx' --scope global
| Option | Description |
|---|---|
-q, --query=QL |
add와 set에 필수. 뷰의 쿼리이며, 필터 바와 run history -q가 받는 것과 같은 History QL입니다 |
--scope=SCOPE |
project(기본값) 또는 global. 글로벌 뷰는 settings.json에 살며 모든 프로젝트에 나타납니다 |
--to=NAME |
rename에서: 새 이름 |
--to=SCOPE |
scope에서: 옮길 저장소, project 또는 global |
뷰는 이름으로 지목합니다. --view와 피커가 받는 것이 이름이고, id를 두면 한 가지를 두 가지로 부르는 셈이기 때문입니다. 이름은 스코프 안에서 유일하며 스코프끼리는 겹칠 수 있습니다. 그럴 때 --view는 project → global → 기본 제공 순으로 고릅니다. 프로젝트 환경변수와 호스트 오버라이드가 이미 쓰는 것과 같은 우선순위입니다. 모든 변경 명령이 --scope를 받는 이유는 colormarker rm과 같습니다. 두 저장소는 각각 따로 지목되며, 어느 쪽을 뜻했는지 추측하면 엉뚱한 뷰를 고치게 됩니다. 목록은 스코프를 G/P/· 접두사로 찍습니다.
쿼리는 실행할 때가 아니라 저장할 때 검사합니다. 없는 필드를 쓴 쿼리, 깨진 정규식, 그리고 모든 항이 버려질 쿼리는 거절합니다. 마지막 것이 중요합니다 — 아무것도 좁히지 못하는 뷰인데도 v: 칩은 좁히고 있다고 주장하게 되기 때문입니다. 같은 검사가 세 표면 모두에서 돌아가므로, TUI가 거절한 뷰를 CLI가 받아 주는 일은 없습니다.
기본 제공 뷰는 편집도 삭제도 되지 않으며, 저장된 뷰가 기본 뷰의 이름을 가져갈 수도 없습니다 — 가려 버리면 --view로 그 기본 뷰에 다시 닿을 수 없기 때문입니다.
지금 보고 있는 뷰를 지우면 그 프로젝트는 All로 돌아갑니다. 지운 글로벌 뷰를 가리키던 다른 프로젝트의 포인터는 무해하게 남습니다. id는 단조 증가 카운터에서 나오고 재사용되지 않으므로 다른 뷰가 그 자리를 물려받을 수 없습니다. 대화형 피커는 프록시 & History를 보세요.
run project
프로젝트 목록/생성/삭제, 또는 프로젝트 스코프 설정(스코프 규칙, env 변수, 호스트 오버라이드) 관리:
gori run project --format json
gori run project list
gori run project list --all
| Option | Description |
|---|---|
--all |
캡처된 것이 없는 프로젝트까지 모두 출력 |
--format=FMT |
text(기본) 또는 json |
list는 비어 있는 프로젝트(캡처된 flow가 0개)를 숨깁니다. 워크트리나 체크아웃마다 프로젝트를 만들다 보면 수백 개가 쌓여, 정작 트래픽이 든 두세 개가 묻히기 때문입니다. 비어 있는지는 파일 크기가 아니라 행 수로 셉니다. 방금 만든 프로젝트도 3월에 남은 찌꺼기와 크기가 같습니다. 다음 두 개는 아무리 비어 있어도 항상 표시하고 표시자를 붙입니다. ◆는 --project 없이 실행한 gori run이 읽는 프로젝트, ◇는 TUI가 마지막으로 연 프로젝트입니다. --format json에서는 각각 current, tui_active 필드이고 flows 개수가 함께 나옵니다. 몇 개를 숨겼는지는 stderr로 나가므로 JSON 파이프는 깨끗한 배열로 남습니다.
project create
트래픽을 캡처하지 않고 프로젝트를 만듭니다. gori run capture --project=NAME도 필요할 때 만들어 주지만, 이 명령은 요청을 보내지 않으므로 프록시를 띄우기 전에 스코프와 env를 미리 구성할 수 있습니다.
gori run project create "API test"
gori run project create api-test --description="staging sweep"
gori run project create api-test --format json
| Option / subcommand | Description |
|---|---|
<name> |
표시 이름. 공백이 들어가면 따옴표로 감쌉니다 |
--description=TEXT |
프로젝트 설정에 저장됩니다 |
--format=FMT |
text(기본) 또는 json |
이미 있는 이름은 오류가 아니라 그 프로젝트를 다시 여는 것으로 처리하며, --format json은 "created": false로 알려 줍니다. 다시 열 때 저장된 표시 이름은 마지막 create의 대소문자로 갱신되고, --description을 주면 기존 설명을 덮어씁니다.
project delete
프로젝트 디렉터리와 그 안에 캡처된 모든 것(플로우, 이슈, 노트, 스코프, 규칙)을 삭제합니다. 되돌릴 수 없으므로 두 단계로 동작합니다. --yes 없이 실행하면 대상만 출력하고 0이 아닌 코드로 종료합니다.
gori run project delete api-test # preview only, nothing is removed
gori run project delete api-test --format json
gori run project rm api-test --yes # actually delete
| Option / subcommand | Description |
|---|---|
<name> |
짧은 id, id 접두사, 디렉터리 slug, 표시 이름 중 하나로 지정 |
--yes |
실제로 삭제. 없으면 아무것도 지우지 않습니다 |
--format=FMT |
text(기본) 또는 json |
미리보기는 플로우/이슈 개수, 디스크 사용량, 캡처가 살아 있는지를 함께 보여 줍니다. 다른 gori 인스턴스가 캡처 중인 프로젝트는 삭제를 거부하므로, 그 캡처를 먼저 중지해야 합니다.
표시 이름은 유일하지 않습니다(같은 basename을 쓰는 두 워크스페이스는 이름을 공유합니다). 이름이 여러 프로젝트에 걸리면 삭제를 거부하고 각각의 slug를 보여 줍니다. 잘못 고르면 되돌릴 수 없기 때문입니다. slug와 짧은 id는 유일하므로 언제나 하나로 확정됩니다.
project scope
프로젝트의 include/exclude 스코프 규칙을 스크립트에서 관리합니다:
gori run project scope # list rules + enabled state
gori run project scope --format json
gori run project scope add --kind=include --type=host --pattern=api.example.com
gori run project scope add --kind=exclude --type=regex --pattern='.*\.(css|js)$'
gori run project scope delete 3
gori run project scope enable
gori run project scope disable
| Option / subcommand | Description |
|---|---|
| (default) | 규칙 목록; --format은 text 또는 json |
add |
--kind=include|exclude, --type=host|string|regex, --pattern=… |
delete <rule-id> |
id로 규칙 제거 |
enable / disable |
스코프 필터링 적용 여부 토글 |
project sandbox
하드 컨테인먼트 샌드박스 게이트를 조회하거나 설정합니다. TUI Project settings 토글의 헤드리스 등가물입니다. 켜면 캡처 프록시가 스코프가 허용하는 요청만 전달하고 나머지는 모두 차단합니다. 표시 렌즈일 뿐인 project scope enable과는 다릅니다.
gori run project sandbox # show the current state (status is the default)
gori run project sandbox status --format json
gori run project sandbox on # start blocking out-of-scope traffic
gori run project sandbox off # stop blocking
| Option / subcommand | Description |
|---|---|
(default) / status |
게이트 상태 표시; --format은 text 또는 json |
on / enable |
스코프가 허용하지 않는 모든 요청 차단 |
off / disable |
차단 중지 |
include 규칙이 없으면 샌드박스를 켤 때 규칙을 추가하기 전까지 모든 캡처 트래픽이 차단됩니다(
gori run project scope add …). 이 명령은 경고 후 진행하므로 CI에서 컨테인먼트를 부트스트랩할 수 있습니다.
project env
아웃바운드 요청의 $KEY 치환에 쓰이는 프로젝트 env 변수를 관리합니다. 전역 변수는 settings.json / TUI Settings에 있고, 이 명령은 프로젝트 레이어만 다룹니다.
gori run project env # list KEY=value
gori run project env --format json
gori run project env set TOKEN=secret
gori run project env set HOST api.example.com
gori run project env delete TOKEN
| Option / subcommand | Description |
|---|---|
| (default) | 프로젝트 변수 목록; --format은 text 또는 json |
set KEY=value · set KEY value |
프로젝트 변수 upsert (KEY는 [A-Za-z_][A-Za-z0-9_]*) |
delete KEY |
프로젝트 변수 제거 |
project host-override
프로젝트 호스트 오버라이드를 관리합니다. /etc/hosts처럼 호스트명에 대해 dial할 IP만 바꾸고, SNI·인증서 호스트·Host 헤더는 원래 이름을 유지합니다. 충돌 시 프로젝트 항목이 전역 호스트네임 오버라이드보다 우선합니다. 별칭: host-overrides.
gori run project host-override # list
gori run project host-override --format json
gori run project host-override add --host=api.example.com --ip=10.0.0.1
gori run project host-override add 10.0.0.1 api.example.com # /etc/hosts 순서
gori run project host-override update 1 --host=api.example.com --ip=10.0.0.9
gori run project host-override delete 1
| Option / subcommand | Description |
|---|---|
| (default) | 오버라이드 목록; --format은 text 또는 json |
add |
--host=… + --ip=…, 또는 positional IP HOST |
update <id> |
--host=… + --ip=… (둘 다 필수) |
delete <id> |
id로 오버라이드 제거 |
gori mcp
MCP stdio 서버입니다. 도구 세부사항은 MCP 가이드를 참고하세요.
| Option | Description |
|---|---|
--db=PATH |
이 데이터베이스를 제공 (--project보다 우선) |
--project=NAME |
이름이 지정된 프로젝트의 데이터베이스 제공 |
--use-active-project |
Git 워크스페이스 선택을 무시하고 활성 TUI/MRU 프로젝트를 명시적으로 제공 |
--no-project |
Git 워크스페이스 안에서도 unbound로 시작 (에이전트가 list/create/switch로 선택) |
--insecure-upstream |
send_request: 업스트림 TLS 검증 생략 |
--read-only |
액션 도구 비활성화 (send_request, 이슈 생성/수정, fuzz/mine); switch_project(및 unbound 시 create_project)는 유지 |
--install-claude |
Claude Desktop mcpServers 설정 기록 |
--install-claude-code |
Claude Code ~/.claude.json mcpServers 항목 기록 |
--install-codex |
OpenAI Codex ~/.codex/config.toml [mcp_servers.gori] 기록 |
--install-agy |
Antigravity ~/.gemini/antigravity-cli/mcp_config.json 기록 |
--install-grok |
Grok ~/.grok/config.toml [mcp_servers.gori] 기록 |
--install-hermes |
Hermes ~/.hermes/config.yaml mcp_servers.gori 기록 (또는 $HERMES_HOME) |
--install-*은 한 번에 여러 개 지정할 수 있습니다. 클라이언트마다 따로 설정하고 따로 보고하며, 하나가 실패해도 나머지는 그대로 진행됩니다. 커맨드라인의 다른 플래그(--db, --project, --no-project, --use-active-project, --read-only, --insecure-upstream, 전역 --config)는 모두 설치되는 커맨드에 기록되고, 경로는 절대 경로로 바뀝니다. 기존 설정 파일은 제자리에서 갱신됩니다. 다른 항목·테이블·주석은 유지되고, 권한도 보존되며, 교체는 원자적입니다.
gori ca
gori ca
gori ca --pem
gori ca --ca-dir=DIR
gori ca regenerate
gori ca regenerate --yes
gori ca import --cert root.crt.pem --key root.key.pem --yes
gori 루트 CA 인증서의 경로를 출력합니다(최초 사용 시 생성). 브라우저나 시스템 저장소에서 CA를 신뢰시킬 때, 또는 클라이언트에 --cacert를 지정할 때 사용하세요.
| Option | Description |
|---|---|
--ca-dir=DIR |
CA 디렉터리 (기본값 ~/.gori/ca, 또는 $GORI_HOME/ca) |
--pem |
경로 대신 인증서 PEM을 stdout으로 출력 |
동사(verb)를 먼저 쓰고 플래그를 그 뒤에 씁니다 — gori ca --ca-dir=DIR regenerate가 아니라 gori ca regenerate --ca-dir=DIR입니다. 반대 순서는 사용법 오류로 처리합니다. 그러지 않으면 동사가 버려진 채 CA 경로만 출력되어 작업이 수행된 것처럼 보이기 때문입니다. 세 가지 형태 모두 위치 인자를 받지 않습니다.
gori ca는 로드는 되지만 사용할 수 없는 루트 CA — 인증서와 일치하지 않는 개인 키, 또는 gori가 서명에 사용할 수 없는 키 — 도 stderr로 보고합니다. 그렇지 않으면 이 증상은 클라이언트 쪽에서 "unknown CA"나 "bad signature" 핸드셰이크 실패로만 드러나기 때문입니다. 해결책은 regenerate와 import이며, 두 명령은 쌍 중 한 파일만 남은 경우를 포함해 어떤 상태의 CA 디렉터리에서도 동작합니다.
gori ca regenerate
디스크의 루트 CA를 새로 발급한 것으로 교체합니다. 파괴적: 이전 CA를 신뢰하던 모든 클라이언트는 새 인증서를 다시 신뢰해야 합니다. 이미 실행 중인 gori 프로세스는 재시작 전까지 이전 CA를 메모리에 유지합니다.
| Option | Description |
|---|---|
--yes, -y |
대화형 확인 생략 (stdin이 tty가 아닐 때 필수) |
--ca-dir=DIR |
재생성할 CA 디렉터리 |
--yes 없이는 tty에서 프롬프트가 뜨며 regenerate를 입력하도록 요구합니다(TUI 확인과 같은 단어). 스크립트와 CI는 --yes를 전달해야 합니다. 성공하면 새 인증서 경로가 stdout으로 출력됩니다.
gori ca import
외부에서 생성한 루트 CA(인증서 + 일치하는 개인 키, 둘 다 PEM)를 gori 자체 CA 대신 채택합니다. 팀이나 여러 머신에서 하나의 CA를 공유하거나, 조직 CA를 재사용하기 위해서입니다. gori는 호스트별 리프 인증서를 즉석에서 서명하므로 두 파일이 모두 필요합니다. 클라이언트는 인증서만 신뢰합니다. regenerate처럼 파괴적이며, 디스크의 루트를 교체하고 기존 신뢰를 무효화합니다.
| Option | Description |
|---|---|
--cert FILE |
채택할 루트 CA 인증서 PEM (필수) |
--key FILE |
일치하는 개인 키 PEM (필수) |
--yes, -y |
대화형 확인 생략 (stdin이 tty가 아닐 때 필수) |
--ca-dir=DIR |
설치할 CA 디렉터리 |
무엇이든 디스크에 기록하기 전에 쌍을 먼저 검증합니다: 키는 인증서와 일치해야 하고, 인증서는 CA여야 하며(basicConstraints CA:TRUE), gori가 그 키로 리프 인증서를 서명할 수 있어야 합니다. 마지막 검사 때문에 Ed25519 · Ed448 루트는 거부됩니다 — gori는 리프를 SHA-256으로 서명하는데 이 키들은 이를 지원하지 않습니다 — 따라서 EC P-256이나 RSA 루트를 사용하세요. 거부된 쌍은 현재 CA를 건드리지 않고 중단합니다. 만료되었거나 아직 유효하지 않은 인증서는 경고만 남기고 그대로 가져옵니다. tty에서 import를 입력하여 확인하거나 --yes를 전달하세요. 같은 동작을 TUI 팔레트(Import CA certificate)에서도 사용할 수 있습니다.
OpenSSL로 루트를 생성한 뒤 가져옵니다:
openssl ecparam -genkey -name prime256v1 -out root.key.pem
openssl req -x509 -new -key root.key.pem -days 3650 -subj "/CN=my ca" -out root.crt.pem
gori ca import --cert root.crt.pem --key root.key.pem --yes
클라이언트에서는 root.crt.pem만 신뢰하세요. 개인 키는 절대 배포하지 마세요.
gori settings
gori settings # settings.json 경로 출력
gori settings --edit # $EDITOR로 열기
gori settings sections # 최상위 섹션 목록
gori settings export [-o FILE] # 공유 가능한 프로필 출력(기본 stdout)
gori settings import FILE # 프로필의 섹션들을 적용
gori settings tls-fingerprint # 목적지별로 gori가 보내는 JA3/JA4
프로필
export와 import는 설정을 다른 머신으로 옮기거나, 팀과 공유하거나, 재현 가능한 실행을 위해 저장소에 커밋할 때 씁니다. 단위는 최상위 섹션이며 목록은 gori settings sections로 확인합니다.
gori settings export --sections network,scan_rules -o team-profile.json
gori settings import team-profile.json --dry-run # 무엇이 적용될지 미리 보기
gori settings import team-profile.json --sections network
gori settings sections는 gori가 아는 모든 섹션을 나열하고, 이 설치본에 아직 값이 없는 것을 표시합니다:
statusline (can carry commands)
network
editor (can carry commands)
env (holds secrets — excluded unless named; not set — at its default)
scan_rules (can carry commands; not set — at its default)
decoder (holds secrets — excluded unless named; can carry commands; not set — at its default)
rewriter (can carry commands)
not set으로 표시된 섹션도 --sections에 쓸 수 있는 정상적인 이름입니다. export하면 담을 값이 없을 뿐이고(그 사실을 stderr로 알려줍니다), import하면 그 섹션이 처음으로 기록됩니다.
| 플래그 | 대상 | 설명 |
|---|---|---|
--sections a,b |
공통 | 쉼표로 구분한 섹션 이름, 최소 하나. export 기본값은 비밀을 담은 섹션을 제외한 전부, import 기본값은 파일에 있는 전부 |
-o, --out FILE |
export | stdout 대신 파일로 기록 |
--dry-run |
import | 적용될 섹션만 출력하고 아무것도 쓰지 않고 종료 |
--allow-commands |
import | 외부 명령을 실행하는 룰을 적용합니다. 프로필이 그런 룰을 담고 있으면 필수 — 없으면 import는 거부되고 아무것도 쓰이지 않습니다 |
--json |
tls-fingerprint | 리포트를 JSON으로 출력. 분해된 JA3 문자열과 ja4_r가 항상 포함됩니다 |
선택하지 않았거나 프로필에 없는 섹션은 그대로 남습니다. --sections가 고르는 것이 바로 이 경계입니다. 프로필이 실제로 담고 있는 섹션 안에서는:
- 리스트/테이블 섹션은 통째 교체됩니다:
upstream_rules,outbound_tls,listeners,scan_rules,hostname_overrides,tabs등."upstream_rules": []를 담은 프로필은 테이블을 비웁니다 — "규칙 없음"을 그렇게 표현합니다. - 스칼라 오브젝트 섹션은 키 단위로 적용됩니다:
network,editor,probe. 프로필이 생략한 키는 현재 값을 유지하므로,network.upstream_proxy만 지정한 팀 프로필이 언급한 적도 없는bind_port까지 기본값으로 되돌리지 않습니다.
export는 공장 기본값 상태인 섹션을 아예 쓰지 않으므로, 프로필은 설정 전체의 스냅샷이 아니라 적용할 값들의 묶음입니다 — 어떤 값이 기본값인 머신에서 export해도, 그 값이 기본값이 아닌 머신에서 되돌려지지 않습니다. import가 무엇을 건드릴지는 --dry-run으로 확인하세요 — 목록에 넣는 쪽으로 넉넉하게 판단하므로, 거기 없는 섹션은 확실히 아무 변화도 없습니다.
import는 TUI와 동일한 저장 경로를 거치므로 원자적 쓰기가 유지되고, 동시에 실행 중인 gori가 건드리지 않은 섹션에 한 편집이나 삭제를 덮어쓰지 않습니다. 파일에 있는 알 수 없는 섹션은 보고하고 무시합니다 — 실행 중인 설정에도, 파일에도 반영되지 않습니다.
gori가 settings.json을 읽지 못하는 상태 — 파싱 실패, 권한 문제, --config가 열 수 없는 대상을 가리키는 경우 — 라면 export와 import 모두 진행하지 않고 거부합니다. 그 시점의 gori는 모든 섹션이 공장 기본값이므로, import는 프로필이 언급하지 않은 섹션 전부를 기본값으로 디스크에 박고, export는 그 기본값을 원래 설정인 양 파일로 내보내기 때문입니다. 먼저 파일을 고치거나 지우세요 — 파싱되지 않은 원본은 옆에 settings.json.corrupt로 보관됩니다. --dry-run은 예외입니다: 아무것도 쓰지 않으므로 그대로 실행되고, 비교 대상이 기본값이라는 사실을 stderr로 알려줍니다.
env와 decoder는 export에서 기본 제외됩니다 — env는 토큰 값을, decoder는 마지막 입력과 저장된 세션을 담기 때문입니다. 명시적으로 이름을 적는 것(--sections env)이 포함에 대한 동의입니다. upstream_rules는 공유해도 안전합니다 — 사용자명과 환경변수 이름만 저장하고 비밀번호는 담지 않습니다.
-o가 실제 사용 중인 settings.json을 가리키면 거부됩니다. export는 스냅샷이 아니라 — 기본값 상태인 섹션은 모두 빠지고, env와 decoder는 이름을 지정하지 않는 한 빠집니다 — 그것을 원본 파일에 되쓰면 해당 섹션이 갱신되는 게 아니라 삭제됩니다.
export가 실제로 그런 섹션을 담게 되면 -o FILE은 0600으로 생성되고, gori가 파일에 무엇이 들어 있는지 이름을 대며 알려줍니다. 자격증명을 export하는 데 동의한 것이 그것을 누구나 읽을 수 있게 두는 데 동의한 것은 아닙니다. 일반 export는 0644로 남고, env 변수가 하나도 없는 설치에서 env를 지정한 export도 일반 export입니다 — 권한은 타이핑한 내용이 아니라 문서에 실제로 담긴 것을 따릅니다.
명령을 담은 프로필
다섯 섹션은 데이터가 아니라 명령을 담을 수 있습니다. 다른 설정과 똑같이 export됩니다 — 팀이 같은 재서명 훅을 표준으로 쓰는 것이야말로 훅이 존재하는 이유니까요. 대신 양쪽 끝에서 파일에 무엇이 들었는지 말해줍니다.
| 섹션 | 무엇이 담나 | 어떻게 실행되나 |
|---|---|---|
rewriter |
op: pipe인 룰 |
argv, 셸 없음 — 매치되는 프록시 트래픽마다 |
scan_rules |
kind: exec인 항목 |
argv, 셸 없음 — 분석되는 플로우마다 |
decoder |
exec:…로 쓴 chains 스텝 |
argv, 셸 없음 — 체인을 실행할 때 |
statusline |
command |
/bin/sh -c — interval초마다 |
editor |
command |
argv — gori settings --edit와 TUI의 ^E에서 |
앞의 셋은 프로세스 훅입니다. 다섯 중 가장 날카로운 건 statusline입니다 — argv exec이 아니라 완전한 셸이고, 같은 섹션에 자기 enabled를 들고 있어 프로필 하나로 바로 무장되며, 트래픽 없이 타이머만으로 실행됩니다. editor는 프로필이 값을 지정했을 때만 보고합니다 — 비어 있으면 gori는 받는 쪽의 $VISUAL/$EDITOR/vi로 넘어갑니다.
export는 개수를 stderr로 알리고, stdout의 프로필은 깨끗하게 둡니다:
note: 5 entries in this profile run a local command (2 rewriter pipe, 1 scan_rules exec, 1 statusline sh -c, 1 editor exec) — whoever imports it runs them with their own privileges
import는 argv까지 한 줄씩 나열하고, 확인을 받기 전까지 쓰지 않습니다. --dry-run도 같은 목록을 출력하며 어느 쪽이든 아무것도 쓰지 않습니다:
$ gori settings import team-profile.json
5 entries in this profile run a local command here, with your privileges:
rewriter pipe resign body ./resign.sh --key $TOKEN
rewriter pipe (unnamed) /usr/local/bin/hmac [disabled]
scan_rules exec leak detector ./detect.py
statusline sh -c command gori-status --project
editor exec command nvim
importing them is the same trust decision as running the author's script
gori settings import: refused — the 5 entries listed above run a local command with your privileges. Read them, then pass --allow-commands. Nothing was written.
명령을 읽고 나서 --allow-commands를 주세요. 대화형 프롬프트가 없으므로 스크립트에서 실행하는 import는 그대로 스크립트로 남습니다 — 그 플래그 자체가 확인 절차입니다. 프로필이 담고는 있지만 꺼둔 항목은 [disabled]로 표시됩니다. 누군가 켜기 전까지는 아무것도 실행하지 않지만, 파일에는 여전히 들어 있습니다. --sections로 범위를 좁히면 이 판단도 함께 좁아집니다 — network만 적용하는 import는 아무것도 무장시키지 않으므로 항목을 나열하지도, 플래그를 요구하지도 않습니다.
gori settings tls-fingerprint
gori가 실제로 보내는 ClientHello의 JA3/JA4 지문을 목적지별로 출력합니다 — Cloudflare·Akamai·DataDome·PerimeterX 같은 안티봇이 챌린지를 띄울지 판단할 때 읽는 바로 그 offer입니다. outbound_tls의 지문 필드를 검증하는 수단입니다. OpenSSL은 협상 결과만 알려줄 뿐 무엇을 제안했는지는 알려주지 않으므로, 이 명령이 없으면 설정이 먹혔는지 확인할 방법이 없습니다.
gori settings tls-fingerprint # 모든 규칙 + 규칙 없음 기본값
gori settings tls-fingerprint shop.example.com # 그 호스트가 실제로 받는 정책 하나
gori settings tls-fingerprint --json # 원본 목록까지 담은 기계 판독용 출력
# …그리고 per-send 오버라이드가 대신 무엇을 보낼지. Repeater 탭의 ␣T나 `--tls-preset`
# 실행이 적용하는 것과 같은 좁히기를, settings.json을 건드리지 않고 미리 봅니다:
gori settings tls-fingerprint shop.example.com --preset curl
shop.example.com (matched rule "shop.example.com")
preset chrome
groups X25519:P-256:P-384
…
tunnelled (gori offers h2) — ALPN h2, http/1.1
JA3 c99e92e692ba483e2602b38b3c0a5645
771,4865-4866-…,65281-0-11-10-35-5-16-22-13-43-45-51-21,29-23-24,0
JA4 t13d1513h2_8daaf6152771_afafd945c4ab
t13d1513h2_002f,0035,…_0005,000a,…_0403,0804,…
정책마다 두 개의 leg를 보여주며, 둘의 차이는 실재합니다. gori는 복호화하는 MITM 연결에서는 h2를 제안하고, 자신이 HTTP/1.1을 말하게 될 leg(포워드 프록시 dial, Repeater, WebSocket)에서는 제안에서 h2를 빼며, alpn을 설정하지 않았다면 ALPN 확장 자체를 보내지 않습니다 — 그래서 두 leg의 ClientHello가 다릅니다. 각 다이제스트 아래 줄은 그 다이제스트가 해시한 목록입니다. 어떤 필드가 움직였는지는 거기서만 보이고, 브라우저와 비교할 가치가 있는 쪽도 이쪽입니다.
리포트가 읽는 컨텍스트는 실제 dial이 만드는 것과 같은 객체이므로, gori가 하지 않는 핸드셰이크를 설명할 수 없습니다. 이 OpenSSL이 거부하는 groups/sigalgs 문자열은 해당 규칙에 대해 stderr로 보고하고 나머지는 계속 출력합니다.
--preset NAME은 보고되는 모든 정책을 per-send 오버라이드와 똑같이 좁혀서 출력합니다(전송 단위 TLS 지문 참고). 이미 chrome 규칙이 걸린 호스트에 --tls-preset curl이 실제로 무엇을 실어 보낼지 확인할 때 씁니다. 클라이언트 인증서·프로토콜 범위·permissive는 목적지 것이 그대로 남고, ClientHello 모양만 교체됩니다. 모르는 이름은 빈 hello로 보고하지 않고 거부합니다.
전송 단위 TLS 지문
outbound_tls는 목적지 호스트로만 키가 잡힙니다. 상시 정책에는 맞지만, 지문 기능이 존재하는 이유인 질문 — 이 엔드포인트가 chrome일 때와 curl일 때 다르게 답하나? — 에는 맞지 않습니다. 그건 같은 호스트에 대한 A/B이고, 전송 사이에 전역 규칙을 고쳐서 하면 두 전송이 비교 불가능해질 뿐 아니라 그 호스트로 가는 다른 모든 탭과 백그라운드 캡처의 핸드셰이크까지 바뀝니다.
per-send 오버라이드는 목적지 테이블을 건드리지 않고 전송 하나 또는 실행 하나에 대해 프리셋을 지정하며, dial 시점에 해석됩니다:
gori run repeater 42 --tls-preset chrome # 캡처한 플로우를 Chrome처럼 재전송
gori run repeater 42 --tls-preset curl # …그리고 curl로 한 번 더, 비교 가능하게
gori run repeater send 7 --tls-preset firefox # 저장된 세션을 이번 전송만 덮어쓰기
gori run repeater create --tls-preset chrome … # 세션에 저장
gori run fuzz --flow 42 --auto --tls-preset chrome # 스윕 전체를 한 핸드셰이크로
TUI에서는 Repeater 탭의 ␣T(TARGET 밴드의 ␣T:… 칩)이고, 탭과 함께 영속화되므로 다시 연 탭은 이전에 보낸 지문 그대로 보냅니다. Fuzzer는 ^O 고급 카드의 TLS fingerprint 행입니다. MCP는 send_request{tls_preset}와 fuzz_start{tls_preset}이며, 결과 세트가 어느 핸드셰이크에서 나왔는지 말할 수 있도록 그대로 되돌려 줍니다.
오버라이드는 목적지 정책을 통째로 갈아치우는 게 아니라 좁힙니다:
| 필드 | 오버라이드 하에서 |
|---|---|
preset, groups, sigalgs, ciphers, ciphersuites, alpn, session_tickets, ocsp_stapling |
지정한 프리셋 것으로 교체 — 이것이 ClientHello 모양이고, 병합하면 목적지 자신의 값이 계속 이기게 됩니다 |
client_cert, client_key |
유지 — 오버라이드는 hello가 어떻게 보일지를 말하지 gori가 누구인지를 말하지 않습니다. 인증서를 떨어뜨리면 "chrome vs curl"이 "인증됨 vs 익명"이 됩니다 |
min_version, max_version |
유지 — 버전 범위는 그 목적지의 도달 가능성에 대한 사실입니다 |
permissive |
유지 — 오버라이드는 security level 0을 줄 수도 뺏을 수도 없습니다 |
오버라이드만 다른 두 전송은 서로 다른 SSL 컨텍스트를 dial하므로 정말로 두 개의 핸드셰이크입니다. https 전용입니다 — 평문 leg는 ClientHello를 보내지 않고, gori는 보내지 않은 것을 보고하지 않습니다. 목적지 단위 프리셋과 마찬가지로 이것들은 근사치입니다(확장 순서와 GREASE 배치는 OpenSSL의 것). 가정하지 말고 gori settings tls-fingerprint HOST --preset NAME으로 확인하세요.
--config PATH
--config는 이번 실행에 쓸 설정 파일을 지정합니다. 서브커맨드 앞에 옵니다.
gori --config ./ci-profile.json run capture --target https://api.example.com
gori --config ~/profiles/corp.json # 다른 설정으로 TUI 실행
우선순위는 --config → $GORI_CONFIG → $GORI_HOME/settings.json입니다.
이 플래그는 의도적으로 GORI_HOME과 직교합니다 — 읽고 쓸 설정 파일만 바꾸고 CA, 프로젝트 DB, 테마, 워드리스트는 그대로 둡니다. 이전에는 설정을 바꾸려면 트리 전체를 옮기는 방법밖에 없었습니다.
gori wizard
gori wizard
대화형 설정(전역 프록시 바인드 기본값, 테마, 그다음 Miss Ring 마스코트)을 실행합니다. 최초 실행 시에도 자동으로 실행됩니다. 바인드 단계는 공유 settings.json 기본값을 기록하며, 선택한 포트에 이미 다른 프로세스가 열려 있으면 경고합니다(Enter를 한 번 더 누르면 그대로 유지). 프로젝트는 Project 탭에서 자체 주소를 고정할 수 있으며, --listen / --port는 이번 실행에 한해서만 오버라이드합니다. Esc를 두 번 누르면 마법사를 건너뛰고, 각 행은 클릭으로도 선택할 수 있습니다.
gori tutorial
gori tutorial
목업 UI에서 TUI를 대화형으로 둘러봅니다: 탭/패널 탐색, 커맨드 팔레트(Ctrl-P), 스페이스 메뉴(Space), READ/INS 편집 모드. 각 레슨은 동작을 시연하고 키를 직접 눌러 보도록 안내하며, 마지막 연습 단계는 완료 전에 네 가지를 모두 요구한 뒤 첫 실제 세션으로 안내합니다. gori wizard 끝에서 제공되고, 세션 안에서는 팔레트 명령 Guided tour(Ctrl-P)로 열 수 있으며 끝나면 원래 화면으로 돌아옵니다. 실제 프록시 세션 없이도 언제든 안전하게 다시 실행할 수 있습니다. 빠른 시작을 참고하세요.
gori update
gori update
gori update --exec # Homebrew/Snap: run the package-manager command
이 gori 바이너리가 어떻게 설치되었는지 감지하여 그에 맞게 업데이트합니다:
| Install channel | Behavior |
|---|---|
독립 실행 바이너리 (curl 설치, 수동 다운로드, 워크스페이스 빌드, 또는 어떤 패키지 관리자도 소유하지 않은 /usr/bin으로의 수동 복사) |
이 OS/arch에 맞는 최신 GitHub 릴리스 자산을 내려받아 바이너리를 교체 (macOS는 전용 디렉터리의 형제 lib/도 갱신) |
| Homebrew | brew upgrade gori 출력 (--exec로 실행; brew 관리 경로는 절대 덮어쓰지 않음) |
| Snap | snap refresh gori 출력 (--exec로 실행) |
| pacman / AUR | yay / paru / pacman 안내 출력 |
| deb (dpkg) | apt 업그레이드 안내 출력 |
| rpm | dnf / yum / zypper 안내 출력 |
| Nix (스토어 경로) | nix profile upgrade / 플레이크 업데이트 안내 출력; 스토어가 읽기 전용이므로 아무것도 내려받지 않음 |
스토어 경로란 /nix/store/…뿐 아니라 옮겨진 스토어도 포함합니다. 루트 없이 설치하면 스토어가 ~/.local/share/nix/root 아래에 놓이고, NIX_STORE_DIR로 어디로든 옮길 수 있습니다. 기본 접두사를 벗어나면 스토어 해시의 형태로 판별하므로, 사용자가 우연히 nix/store라고 이름 붙인 디렉터리는 그대로 일반 바이너리 설치로 분류됩니다.
/usr/bin 또는 /bin 아래 경로는 패키지 소유권(pacman -Qo, dpkg-query -S, rpm -qf)으로 분류됩니다. 관리자가 파일을 소유하면 gori는 절대 덮어쓰지 않습니다. 프로브가 소유자를 찾지 못하면 바이너리 채널이 자체 업데이트합니다. 패키지 도구가 전혀 없으면 /etc/os-release(ID / ID_LIKE)로 Arch 계열 / Debian 계열 / RHEL 계열 안내를 폴백으로 고릅니다.
릴리스 자산 이름은 설치 가이드와 일치합니다(gori-v*-linux-* 순수 바이너리, gori-v*-osx-*.tar.gz 아카이브). macOS 아카이브 업데이트는 전용 레이아웃(예: curl 설치 프로그램의 PREFIX/opt/gori)을 요구하여 번들된 lib/가 /usr/local/lib 같은 공유 루트 아래에 절대 기록되지 않도록 합니다. 아직 릴리스 자산이 없으면 명령은 릴리스 페이지를 가리키는 명확한 오류로 종료합니다. 조용히 아무 동작도 하지 않는 것이 아닙니다.