burterm/internal/sitemap/tree.go
2026-09-14 10:55:07 +03:00

110 lines
3.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sitemap
import (
"encoding/json"
"fmt"
"net/url"
"os"
"strings"
)
// Record — минимальное описание одного перехваченного запроса,
// достаточное для построения карты. Отделено от proxy.Entry намеренно
// (см. doc пакета) — конвертация делается на стороне вызывающего кода.
type Record struct {
ID int
Method string
URL string
StatusCode int
Err string // непусто, если запрос завершился с ошибкой, а не HTTP-ответом
}
// Endpoint — один запрос, привязанный к конкретному узлу дерева (то есть
// к конкретному пути). На одном узле может быть несколько Endpoint —
// например, GET и POST на один и тот же путь, или один и тот же запрос,
// перехваченный повторно с другим статусом ответа.
type Endpoint struct {
Method string `json:"method"`
StatusCode int `json:"status_code,omitempty"`
URL string `json:"url"`
EntryID int `json:"entry_id"`
Err string `json:"error,omitempty"`
}
// Node — один сегмент пути в дереве (либо хост на верхнем уровне).
type Node struct {
Name string `json:"name"`
Children map[string]*Node `json:"children,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
}
func newNode(name string) *Node {
return &Node{Name: name, Children: map[string]*Node{}}
}
// Tree — карта сайта: набор деревьев путей, по одному на каждый
// перехваченный хост.
type Tree struct {
Hosts map[string]*Node `json:"hosts"`
}
// Build строит дерево из записей, отфильтрованных scope. Записи,
// не проходящие Matches, в дерево не попадают вовсе — это соответствует
// поведению Target scope в Burp: то, что вне scope, не засоряет карту.
func Build(records []Record, scope *Scope) *Tree {
t := &Tree{Hosts: map[string]*Node{}}
for _, r := range records {
if scope != nil && !scope.Matches(r.URL) {
continue
}
u, err := url.Parse(r.URL)
if err != nil || u.Host == "" {
continue
}
host, ok := t.Hosts[u.Host]
if !ok {
host = newNode(u.Host)
t.Hosts[u.Host] = host
}
cur := host
for _, seg := range strings.Split(strings.Trim(u.Path, "/"), "/") {
if seg == "" {
continue
}
child, ok := cur.Children[seg]
if !ok {
child = newNode(seg)
cur.Children[seg] = child
}
cur = child
}
cur.Endpoints = append(cur.Endpoints, Endpoint{
Method: r.Method,
StatusCode: r.StatusCode,
URL: r.URL,
EntryID: r.ID,
Err: r.Err,
})
}
return t
}
// ExportJSON сериализует дерево и пишет его в path. encoding/json сам
// сортирует ключи map[string]*Node при маршалинге, так что вывод
// детерминирован без дополнительной сортировки перед записью.
func (t *Tree) ExportJSON(path string) error {
data, err := json.MarshalIndent(t, "", " ")
if err != nil {
return fmt.Errorf("сериализация карты сайта: %w", err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("запись файла %s: %w", path, err)
}
return nil
}