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

335 lines
9.8 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 tui
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/r3g1tpr0cs/burterm/internal/fuzzer"
)
// fuzzerModel — вкладка Fuzzer: путь к целевому бинарнику, аргументы
// (с плейсхолдером "@@" — заменяется на файл с мутированным вводом,
// это отдельный от Intruder-маркеров §...§ синтаксис), seed-файлы, число
// воркеров и таймаут на запуск. Живой лог крашей пополняется по мере
// поступления; у фаззинга, как и у краулинга, нет заранее известного
// "total" — вместо процента честный спиннер + счётчик execs/сек.
type fuzzerModel struct {
targetInput textinput.Model
argsInput textinput.Model
seedsInput textinput.Model
workersInput textinput.Model
timeoutInput textinput.Model
useStdin bool
viewport viewport.Model
spinner spinner.Model
running bool
startedAt time.Time
execs int64
crashes int
crashDir string
lines []string
errMsg string
statusMsg string
focus int // 0 target, 1 args, 2 seeds, 3 workers, 4 timeout, 5 viewport
}
func newFuzzerView() fuzzerModel {
target := textinput.New()
target.Placeholder = "/путь/к/цели"
target.CharLimit = 512
target.Focus()
args := textinput.New()
args.Placeholder = "@@ — заменяется на файл с мутированным вводом (ctrl+i — вместо этого через stdin)"
args.CharLimit = 512
seeds := textinput.New()
seeds.Placeholder = "seed1.bin,seed2.bin — через запятую"
seeds.CharLimit = 1024
workers := textinput.New()
workers.Placeholder = "4"
workers.CharLimit = 4
timeout := textinput.New()
timeout.Placeholder = "2"
timeout.CharLimit = 4
sp := spinner.New()
sp.Spinner = spinner.Dot
return fuzzerModel{
targetInput: target,
argsInput: args,
seedsInput: seeds,
workersInput: workers,
timeoutInput: timeout,
viewport: viewport.New(0, 0),
spinner: sp,
}
}
func (m *fuzzerModel) SetSize(width, height int) {
if width < 0 {
width = 0
}
if height < 0 {
height = 0
}
m.viewport.Width = width
// 20 фиксированных строк вокруг viewport (построчный подсчёт по
// View(): 5 полей × (метка+поле+разделитель) = 15, плюс разделитель
// перед статусом (1), строка статуса (1), разделитель перед viewport
// (1), разделитель+футер(две строки) (3) — итого 20. Раньше стояло
// -12 — критически недосчитано с самого начала, безотносительно
// разбиения футера. Оговорка: это минимум для "чистого" состояния —
// если показаны ещё и "Краши сохраняются в:"/ошибка/статус (обычное
// дело после первого запуска), реальная высота на 1-3 строки больше
// этого минимума.
vh := height - 20
if vh < 3 {
vh = 3
}
m.viewport.Height = vh
}
// Reset очищает лог крашей и счётчики перед новым запуском.
func (m *fuzzerModel) Reset() {
m.lines = nil
m.crashes = 0
m.execs = 0
m.running = true
m.startedAt = time.Now()
m.viewport.SetContent("")
}
func (m *fuzzerModel) Finish() {
m.running = false
}
// AddCrash дописывает находку в лог.
func (m *fuzzerModel) AddCrash(c fuzzer.Crash) {
m.crashes++
var b strings.Builder
b.WriteString(fmt.Sprintf("#%-3d %-10s %s\n", m.crashes, c.Signal, c.Path))
if c.Stderr != "" {
b.WriteString(" stderr: " + firstLine(c.Stderr) + "\n")
}
m.lines = append(m.lines, b.String())
m.viewport.SetContent(strings.Join(m.lines, "\n"))
m.viewport.GotoBottom()
}
func firstLine(s string) string {
if idx := strings.IndexByte(s, '\n'); idx >= 0 {
return s[:idx]
}
return s
}
func (m fuzzerModel) Update(msg tea.Msg) (fuzzerModel, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "tab":
m.focus = (m.focus + 1) % 6
m.targetInput.Blur()
m.argsInput.Blur()
m.seedsInput.Blur()
m.workersInput.Blur()
m.timeoutInput.Blur()
switch m.focus {
case 0:
m.targetInput.Focus()
case 1:
m.argsInput.Focus()
case 2:
m.seedsInput.Focus()
case 3:
m.workersInput.Focus()
case 4:
m.timeoutInput.Focus()
}
return m, nil
case "ctrl+i":
m.useStdin = !m.useStdin
return m, nil
case "ctrl+r":
if m.running {
// Фаззер уже идёт — второй запуск игнорируем, как и в Spider:
// параллельный прогон по тому же корпусу не даёт пользы,
// только путает счётчики execs/крашей.
return m, nil
}
m.errMsg = ""
return m, m.startCmd()
}
}
var fieldCmd tea.Cmd
switch m.focus {
case 0:
m.targetInput, fieldCmd = m.targetInput.Update(msg)
case 1:
m.argsInput, fieldCmd = m.argsInput.Update(msg)
case 2:
m.seedsInput, fieldCmd = m.seedsInput.Update(msg)
case 3:
m.workersInput, fieldCmd = m.workersInput.Update(msg)
case 4:
m.timeoutInput, fieldCmd = m.timeoutInput.Update(msg)
}
var vpCmd tea.Cmd
m.viewport, vpCmd = m.viewport.Update(msg)
return m, tea.Batch(fieldCmd, vpCmd)
}
// startCmd валидирует конфигурацию, грузит seed-файлы и запускает
// fuzzer.Fuzzer. Директория для крашей создаётся автоматически под
// ~/.burterm/fuzz-crashes/<unix-время>/ — не нужно отдельное поле
// ввода ради этого.
func (m fuzzerModel) startCmd() tea.Cmd {
target := strings.TrimSpace(m.targetInput.Value())
argsRaw := strings.TrimSpace(m.argsInput.Value())
seedsRaw := strings.TrimSpace(m.seedsInput.Value())
useStdin := m.useStdin
workersStr := m.workersInput.Value()
timeoutStr := m.timeoutInput.Value()
return func() tea.Msg {
if target == "" {
return fuzzErrMsg{fmt.Errorf("укажи путь к целевому исполняемому файлу")}
}
if seedsRaw == "" {
return fuzzErrMsg{fmt.Errorf("укажи хотя бы один seed-файл")}
}
var args []string
if argsRaw != "" {
args = strings.Fields(argsRaw)
}
seedPaths := strings.Split(seedsRaw, ",")
for i := range seedPaths {
seedPaths[i] = strings.TrimSpace(seedPaths[i])
}
seeds, err := fuzzer.LoadSeeds(seedPaths)
if err != nil {
return fuzzErrMsg{err}
}
workers, _ := strconv.Atoi(workersStr)
if workers <= 0 {
workers = 4
}
timeoutSec, _ := strconv.Atoi(timeoutStr)
if timeoutSec <= 0 {
timeoutSec = 2
}
crashDir, err := defaultFuzzCrashDir()
if err != nil {
return fuzzErrMsg{err}
}
f, err := fuzzer.New(fuzzer.Config{
Target: target,
Args: args,
UseStdin: useStdin,
Timeout: time.Duration(timeoutSec) * time.Second,
Workers: workers,
CrashDir: crashDir,
}, seeds)
if err != nil {
return fuzzErrMsg{err}
}
ctx, cancel := context.WithCancel(context.Background())
ch := f.Run(ctx)
return fuzzStartedMsg{ch: ch, cancel: cancel, crashDir: crashDir}
}
}
func defaultFuzzCrashDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("не удалось определить домашнюю директорию: %w", err)
}
dir := filepath.Join(home, ".burterm", "fuzz-crashes", fmt.Sprintf("%d", time.Now().Unix()))
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", fmt.Errorf("не удалось создать %s: %w", dir, err)
}
return dir, nil
}
func (m fuzzerModel) View() string {
focusMark := func(i int) string {
if m.focus == i {
return "▸ "
}
return " "
}
var b strings.Builder
b.WriteString(focusMark(0) + "Цель\n")
b.WriteString(m.targetInput.View())
b.WriteString("\n")
b.WriteString(focusMark(1) + fmt.Sprintf("Аргументы (ввод: %s, ctrl+i — переключить)\n", inputModeLabel(m.useStdin)))
b.WriteString(m.argsInput.View())
b.WriteString("\n")
b.WriteString(focusMark(2) + "Seed-файлы\n")
b.WriteString(m.seedsInput.View())
b.WriteString("\n")
b.WriteString(focusMark(3) + "Воркеры\n")
b.WriteString(m.workersInput.View())
b.WriteString("\n")
b.WriteString(focusMark(4) + "Таймаут, с\n")
b.WriteString(m.timeoutInput.View())
b.WriteString("\n\n")
status := "готов"
if m.running {
elapsed := time.Since(m.startedAt)
rate := 0.0
if elapsed.Seconds() > 0 {
rate = float64(m.execs) / elapsed.Seconds()
}
status = fmt.Sprintf("%s фаззинг… execs: %d (%.0f/с) · крашей: %d (esc — остановить)", m.spinner.View(), m.execs, rate, m.crashes)
}
b.WriteString(fmt.Sprintf("Статус: %s\n", status))
if m.crashDir != "" {
b.WriteString("Краши сохраняются в: " + m.crashDir + "\n")
}
if m.errMsg != "" {
b.WriteString("Ошибка: " + m.errMsg + "\n")
}
if m.statusMsg != "" {
b.WriteString(m.statusMsg + "\n")
}
b.WriteString("\n" + focusMark(5) + m.viewport.View())
b.WriteString("\n\ntab — между полями · ctrl+r — запустить · ctrl+i — файл/stdin · ctrl+c — выход\n")
b.WriteString("f1-f12, pgup — вкладки\n")
return b.String()
}
func inputModeLabel(useStdin bool) string {
if useStdin {
return "stdin"
}
return "файл (@@)"
}