// Package sitemap строит иерархическую карту сайта из перехваченного // трафика (аналог Target / Site Map в Burp) и управляет scope — // набором regex-правил include/exclude, которыми фильтруется, что // попадает в карту. // // Пакет намеренно не зависит от internal/proxy — на вход Build // принимает свои Record, а не proxy.Entry, чтобы sitemap оставался // переиспользуемым независимо от того, откуда взялись перехваченные // запросы (сейчас это Proxy, но с тем же успехом это мог бы быть, // например, лог из файла). package sitemap import ( "fmt" "regexp" ) // Rule — одно правило scope: regex-паттерн и его тип (include/exclude). type Rule struct { Pattern string Exclude bool } type compiledRule struct { Rule re *regexp.Regexp } // Scope — упорядоченный набор regex-правил. Порядок добавления не влияет // на результат сопоставления (см. Matches), но сохраняется для // отображения в UI в том виде, в котором пользователь их вводил. type Scope struct { rules []compiledRule } // NewScope создаёт пустой scope — по умолчанию всё считается в scope, // пока не добавлено хотя бы одно include-правило. func NewScope() *Scope { return &Scope{} } // Add компилирует pattern и добавляет как include- или exclude-правило. func (s *Scope) Add(pattern string, exclude bool) error { re, err := regexp.Compile(pattern) if err != nil { return fmt.Errorf("некорректный regex %q: %w", pattern, err) } s.rules = append(s.rules, compiledRule{Rule: Rule{Pattern: pattern, Exclude: exclude}, re: re}) return nil } // RemoveAt удаляет правило по индексу (в порядке добавления, том же, // что возвращает Rules). Индекс вне диапазона игнорируется. func (s *Scope) RemoveAt(idx int) { if idx < 0 || idx >= len(s.rules) { return } s.rules = append(s.rules[:idx], s.rules[idx+1:]...) } // Rules возвращает текущие правила в порядке добавления — для отображения в UI. func (s *Scope) Rules() []Rule { out := make([]Rule, len(s.rules)) for i, r := range s.rules { out[i] = r.Rule } return out } // Matches решает, попадает ли url в scope: // - если url совпал с любым exclude-правилом — не в scope, безоговорочно; // - иначе, если есть хотя бы одно include-правило — url должен совпасть // хотя бы с одним из них; // - если include-правил нет вовсе — по умолчанию всё в scope // (кроме того, что попало под exclude). func (s *Scope) Matches(url string) bool { hasInclude := false includeMatched := false for _, r := range s.rules { if r.Exclude { if r.re.MatchString(url) { return false } continue } hasInclude = true if r.re.MatchString(url) { includeMatched = true } } if hasInclude { return includeMatched } return true }