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 }