110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
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
|
||
}
|