11/ 12

트리아지와 리포트

지금쯤이면 발견들이 Probe 탭, Repeater 탭, 그리고 머릿속에 흩어져 있을 겁니다. 이 플레이북은 그것들을 넘겨줄 수 있는 형태로 모읍니다. 심각도가 붙은 이슈, 그것을 증명하는 diff, 그리고 gori 없이도 동료가 읽을 수 있는 리포트로요. 약 10분이 걸립니다. 여기서는 새 트래픽을 거의 보내지 않습니다. 이미 캡처한 것으로 작업합니다.

시작하기 전에. 리포트로 쓸 만한 캡처·테스트 트래픽이 담긴 엔게이지먼트가 필요합니다. 앞선 플레이북을 먼저 끝내거나, 자기 프로젝트를 가져오세요. 예시는 api.example.com을 대역으로 씁니다.

1. 패시브 발견 훑어보기

Probe는 캡처되는 모든 플로우와 모든 Repeater 전송에 대해 패시브 검사를 돌립니다. 추가 비용이 없습니다(이 발견을 만들려고 요청이 머신을 떠나지 않습니다). 그래서 여기가 가장 값싼 출발점입니다. Probe 탭을 여세요. 발견은 검사와 호스트로 묶여 있어, 한 행이 수십 건의 히트를 대표할 수 있습니다(관대한 CORS, 누락된 보안 헤더, 쿠키 위생, URL 속 시크릿 등).

행을 열면 AFFECTED URLS 목록이 증거입니다. /로 훑고, Enter로 그 URL이 캡처된 플로우를 History와 같은 상세 뷰로 열며, r로 Repeater에 보내 더 파고듭니다.

같은 집합을 헤드리스로 읽을 수 있고, 이 역시 스스로는 아무것도 보내지 않습니다:

gori run probe                       # 패시브 발견만
gori run probe --severity high       # high 심각도 행만
gori run probe --category cors       # 단일 카테고리

체크포인트. Probe 탭에 발견이 심각도와 카테고리로 묶여 나열되고, 각 항목은 그것이 나온 플로우로 열립니다. 그리고 이를 만드느라 History의 요청 수는 움직이지 않았습니다.

2. 이슈 파일링

Issues는 결국 리포트에 넘길 트리아지 목록입니다. History 플로우나 Repeater 전송에서 Shift-F를 눌러 하나를 파일링하고, Probe 발견은 Probe 탭에서 이슈로 승격합니다. 심각도(info부터 critical까지)와 상태(open, confirmed, false-positive, resolved)를 부여하세요. 파일링한 플로우가 증거로 링크되므로 이슈가 스스로 증거를 담습니다. 이슈에서 Enter를 누르면 그 교환으로 바로 돌아갑니다.

gori Issues tab listing triaged findings with severity, status, host and title columns, one row selected and its linked evidence flow shown
Issues 탭: 승격한 모든 발견이 심각도와 상태를 갖고, 그것을 증명하는 증거 플로우로 다시 링크됩니다.

스크립트에서는 이슈를 직접 파일링·갱신할 수 있습니다. --flow가 같은 동작에서 증거를 링크합니다:

gori run issues create --title "IDOR on /v1/users/{id}" --severity high --host api.example.com --flow 42
gori run issues update 7 --status confirmed --notes "Verified on staging"
gori run probe promote 12            # Probe 발견을 Issues로 확정

체크포인트. Issues 탭에 심각도와 함께 이슈가 보이고, 그것을 열면 증거 플로우로 점프합니다.

3. Comparer로 증명하기

발견은 이전과 이후를 나란히 놓으면 더 강하게 꽂힙니다. 인증 없는 403 옆에 인증된 200, 또는 패치된 응답 옆에 취약한 응답처럼요. Comparer는 두 메시지 A와 B를 담아 diff합니다.

요청과 응답이 있는 곳이면 어디서든 슬롯을 채웁니다. History에서 첫 플로우를 선택하고 SpaceSend to Comparer를 누르면 슬롯 A에 들어갑니다. 두 번째도 같은 방식으로 보내 슬롯 B를 채웁니다. Repeater 전송이나 Fuzzer 결과 행도 같은 방식으로 들어가며, 둘 다 캡처된 플로우를 남기지 않으므로, 이것이 그들이 diff로 들어가는 유일한 경로입니다. Comparer 탭에서는 a / b로 캡처된 플로우를 각 슬롯에 바로 골라 넣습니다. 이 경로는 활성 Scope 렌즈를 거치므로, 스코프 밖 대조군이 필요하면 렌즈를 꺼야 합니다.

두 컬럼 사이의 구분선은 한 줄을 읽기도 전에 A→B 델타를 알려 줍니다. 403 → 200이면 대개 그게 답 전부입니다. /로 요청 diff와 응답 diff를 오가고, 변경된 행에서는 실제로 다른 바이트만 빨강·초록으로 켜지므로, 뒤바뀐 값 하나가 줄을 읽지 않아도 눈에 띕니다.

gori run compare 41 42 --pane response --changes-only

체크포인트. diff가 발견을 증명하는 변화를 강조합니다. 403 → 200 같은 상태 전환이나, 움직인 그 값 하나를요.

3b. 지난 엔게이지먼트 대비 리테스트

Comparer는 메시지 하나가 바뀌었음을 증명합니다. 리테스트는 같은 질문을 표면 전체에 던집니다. 지난번 이후 뭐가 새로 생겼고, 사라졌고, 다르게 응답하나? 이전 평가가 별도의 gori 프로젝트에 남아 있다면 명령 하나면 됩니다.

gori run diff --from q1-audit --to q3-retest --format md

엔드포인트 키는 Sitemap이 그리는 폴딩 템플릿(/users/{uuid})을 그대로 씁니다. 그래서 각 엔게이지먼트가 우연히 캡처한 식별자 때문에 모든 행이 added/removed 쌍으로 갈라지지 않습니다. changed 판정도 바이트 동일성이 아니라 허용 밴드로 내리므로, 길이가 조금 흔들리는 페이지는 발견이 아닙니다. 대화형으로는 Target → Diff입니다. a로 기준을 고르고, 행에서 를 누르면 양쪽 캡처가 Comparer로 넘어가 바이트 단위로 보여줍니다.

판정은 문자 그대로 읽으세요. gone은 새 캡처가 요청했고 404를 받았다는 뜻이고, not seen은 아예 요청하지 않았다는 뜻입니다. 이번 리테스트의 커버리지 공백이지 수정된 게 아닙니다. 리포트 끝에는 아직 열려 있는 이슈들과 그 이슈가 걸려 있던 엔드포인트의 현재 상태가 붙습니다. 요청은 보내지 않습니다.

체크포인트. --format md는 리테스트 산출물에 그대로 붙여 넣을 수 있는 섹션을 만들어 주고, 개수 위에 양쪽의 커버리지가 함께 적힙니다.

모든 것이 이슈는 아닙니다. Notes는 자유 형식의 프로젝트별 Markdown 문서입니다(프로젝트당 여러 개). 시도한 것, 통한 페이로드, 나중에 다시 볼 단서를 적는 기록장이죠. Notes 탭에서 만들고 편집합니다.

흩어진 증거를 하나로 묶으려면 History, Repeater, Fuzzer, Miner에서 Space → **Link…**를 누르세요. 하나의 카드에 모든 이슈 모든 노트가 나열되고, 위에 + New issue… / + New note…가 고정됩니다. 지금 보고 있는 것을 기존 이슈에 붙이는 것과, 이미 링크된 새 이슈를 파일링하는 것이 같은 키 입력입니다. 입력한 글자는 제목·호스트·상태로 필터링하고, 생성 행에 닿으면 새 이슈의 제목이 됩니다. 링크하려는 대상에 아직 아무것도 파일링된 적이 없으면 커서는 + New issue…에서 열리고(가장 흔한 첫 파일링), 링크가 이미 있으면 첫 기존 이슈에서 열립니다. 이슈를 고르면 바이트까지 함께 남습니다. 다음 Repeater 전송이나 보존 정리가 가져가 버릴, 취약점을 확인해 준 그 응답이 같은 단계에서 변경 불가 복사본으로 동결됩니다. 이슈의 RELATED 카드에 FROZEN 행으로 표시되고 모든 내보내기에 SHA-256과 함께 실립니다. 노트는 포인터만 받고, 아직 교환이 없는 대상(대기 중 플로우, 보낸 적 없는 탭)도 링크는 됩니다. 카드의 토큰이 누르기 전에 이유를 말하고(↵ link — nothing to freeze: …), 누른 뒤에는 토스트가 같은 말을 반복합니다.

손으로 새 이슈를 파일링하면 그 이슈가 바로 열립니다. 폼이 커밋되면 Issues 상세가 새 이슈 위에 뜨고, 토스트 끝에 돌아가는 길이 적힙니다 — issue #21 created and linked · frozen as evidence #5 · esc returns to History. 그 esc는 드릴인과 커서까지 그대로, 있던 자리로 되돌립니다. (Diff에서 줄줄이 파일링하는 재검증 스윕은 의도적으로 그 자리에 머뭅니다.)

gori run notes create --text "SSRF candidate on /fetch, needs OAST to confirm"
gori run notes --all

체크포인트. Notes 탭에 노트가 담기고, 링크한 이슈 아래에 증거 플로우나 세션이 나열됩니다.

5. 리포트 내보내기

이슈 트리아지가 끝나면, gori 없이도 동료가 읽을 수 있는 단일 Markdown 문서로 내보냅니다:

gori run issues --format markdown --export report.md

TUI에서는 Issues 탭의 ⇧E가 같은 리포트입니다 — 목록의 힌트 줄에 이 키가 적혀 있습니다. 형식을 고르고(↵ export), 저장 경로를 고르면 됩니다(↵ write, 파일이 이미 있으면 ↵ overwrite).

리포트를 사람이 아니라 기계가 읽는다면 SARIF로 내보내세요. GitHub code scanning, DefectDojo, Azure DevOps가 그대로 읽는 형식입니다:

gori run issues --format sarif --export issues.sarif
gh api -X POST /repos/OWNER/REPO/code-scanning/sarifs \
  -f commit_sha="$(git rev-parse HEAD)" -f ref=refs/heads/main \
  -f sarif="$(gzip -c issues.sarif | base64 | tr -d '\n')"

이슈 하나가 result 하나로, URL과 심각도를 싣고 도착합니다. 플로우를 링크해 두었다면 실제 요청·응답이 webRequest/webResponse로 함께 갑니다. false-positiveresolved로 트리아지한 이슈는 SARIF suppression으로 나가므로, gori에서 정리한 발견은 대시보드에서도 정리된 상태로 남고 다시 열리지 않습니다.

발견 뒤의 원본 트래픽까지(요약본만이 아니라) 넘기려면, History 쿼리를 하나의 HAR 로그로 내보내세요. STDOUT으로 쓰이고, Burp·Charles·브라우저 네트워크 패널로 불러들일 수 있으며, gori로 그대로 다시 임포트됩니다:

gori run history -q 'host:api.example.com status:200' --format har > evidence.har

체크포인트. report.md가 디스크에 존재하고 심각도 순으로 정렬된 이슈 목록으로 읽히며, evidence.har가 그 뒤의 플로우들을 담습니다. SARIF로 내보냈다면 jq '.runs[0].results | length' issues.sarif가 이슈 개수와 일치합니다.

다음 단계