|
| 1 | +# TruffleHog TUI |
| 2 | + |
| 3 | +The TUI is a Bubble Tea program that lets the user configure a scan or an |
| 4 | +analyzer interactively. When it exits, control hands back to `kingpin` (for |
| 5 | +scans) or to `pkg/analyzer.Run` (for analyzer flows). |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +Three layers, nothing else: |
| 10 | + |
| 11 | +1. **`app` — the router.** Owns the page stack, global key handling, the |
| 12 | + shared style sheet and key bindings, and the parent-process handoff |
| 13 | + (argv for kingpin, SecretInfo for the analyzer). Pages know nothing |
| 14 | + about each other; they emit navigation messages and the router |
| 15 | + reacts. |
| 16 | + |
| 17 | +2. **`pages/*` — one directory per page.** Every page implements |
| 18 | + `app.Page`, receives `app.ResizeMsg` for layout, and communicates |
| 19 | + upward with `app.PushMsg` / `app.PopMsg` / `app.ReplaceMsg` / |
| 20 | + `app.ExitMsg` / `app.RunAnalyzerMsg`. Layout is kebab-case |
| 21 | + (`pages/source-picker/`, `pages/analyzer-form/`, …). |
| 22 | + |
| 23 | +3. **`components/form` + `sources` — declarative configuration.** |
| 24 | + `sources.Definition` is a registry entry for each scan source: |
| 25 | + title, description, `form.FieldSpec`s, validators, optional |
| 26 | + `BuildArgs` override. `form.Form` renders the fields, validates |
| 27 | + them, and produces a kingpin-style `[]string` arg vector. The few |
| 28 | + sources with irreducibly complex arg-emission rules (elasticsearch, |
| 29 | + jenkins, postman) provide a `BuildArgs` hook. |
| 30 | + |
| 31 | +```mermaid |
| 32 | +graph TD |
| 33 | + subgraph Runtime |
| 34 | + main[main.go] -->|argv| tui[pkg/tui.Run] |
| 35 | + end |
| 36 | +
|
| 37 | + subgraph pkg/tui |
| 38 | + tui -->|builds| Model[app.Model router] |
| 39 | + Model --> Pages[pages/*] |
| 40 | + Pages -->|uses| Form[components/form] |
| 41 | + Pages -->|uses| Registry[sources registry] |
| 42 | + end |
| 43 | +
|
| 44 | + Model -->|ExitMsg| kingpin |
| 45 | + Model -->|RunAnalyzerMsg| analyzer.Run |
| 46 | +``` |
| 47 | + |
| 48 | +### Window resize |
| 49 | + |
| 50 | +`app.Model` is the only code that subscribes to `tea.WindowSizeMsg`. On |
| 51 | +resize it computes the content rectangle (terminal size minus chrome) and |
| 52 | +forwards an `app.ResizeMsg{Width,Height}` to the active page so pages can't |
| 53 | +drift on their own frame math. Terminals smaller than 40×10 render a |
| 54 | +"terminal too small" placeholder. |
| 55 | + |
| 56 | +## Workflows |
| 57 | + |
| 58 | +### Entry |
| 59 | + |
| 60 | +```mermaid |
| 61 | +flowchart LR |
| 62 | + start([main.go]) --> argv{argv} |
| 63 | + argv -->|no args| wizard[wizard] |
| 64 | + argv -->|analyze| picker[analyzer-picker] |
| 65 | + argv -->|analyze known| form[analyzer-form] |
| 66 | + argv -->|analyze unknown| picker |
| 67 | +``` |
| 68 | + |
| 69 | +### Source scan |
| 70 | + |
| 71 | +```mermaid |
| 72 | +sequenceDiagram |
| 73 | + participant U as user |
| 74 | + participant W as wizard |
| 75 | + participant SP as source-picker |
| 76 | + participant SC as source-config |
| 77 | + participant R as router |
| 78 | + participant K as kingpin |
| 79 | +
|
| 80 | + U->>W: choose "Scan a source" |
| 81 | + W->>R: PushMsg(source-picker) |
| 82 | + U->>SP: pick source |
| 83 | + SP->>R: PushMsg(source-config, SourceID) |
| 84 | + U->>SC: fill source tab → tab |
| 85 | + U->>SC: fill trufflehog tab → tab |
| 86 | + U->>SC: enter on run tab |
| 87 | + SC->>R: ExitMsg(argv) |
| 88 | + R->>K: argv |
| 89 | +``` |
| 90 | + |
| 91 | +### Analyzer |
| 92 | + |
| 93 | +```mermaid |
| 94 | +sequenceDiagram |
| 95 | + participant U as user |
| 96 | + participant AP as analyzer-picker |
| 97 | + participant AF as analyzer-form |
| 98 | + participant R as router |
| 99 | + participant A as analyzer.Run |
| 100 | +
|
| 101 | + U->>AP: pick analyzer type |
| 102 | + AP->>R: PushMsg(analyzer-form, KeyType) |
| 103 | + U->>AF: fill fields |
| 104 | + AF->>R: RunAnalyzerMsg(type, info) |
| 105 | + R->>A: dispatch |
| 106 | +``` |
| 107 | + |
| 108 | +## Adding a new source |
| 109 | + |
| 110 | +1. Create `pkg/tui/sources/<id>/` with a `Definition()` returning a |
| 111 | + `sources.Definition`. |
| 112 | +2. Declare fields with `form.FieldSpec` — pick the appropriate `Kind` |
| 113 | + (`KindText`, `KindSecret`, `KindCheckbox`, `KindSelect`) and |
| 114 | + `EmitMode` (`EmitPositional`, `EmitLongFlagEq`, `EmitPresence`, |
| 115 | + `EmitRepeatedLongFlagEq`, `EmitConstant`, …). Add validators from |
| 116 | + `form` (`Required`, `Integer`, `OneOf`) and cross-field |
| 117 | + `Constraint`s (`XOrGroup`) as needed. |
| 118 | +3. If the arg-emission logic isn't expressible declaratively, set |
| 119 | + `Definition.BuildArgs` to a `func(values map[string]string) []string`. |
| 120 | +4. Add `func init() { sources.Register(Definition()) }` and blank-import |
| 121 | + the package from `pkg/tui/tui.go` so the registry picks it up. |
| 122 | +5. Add a test case to `pkg/tui/sources/sources_test.go` covering the |
| 123 | + emitted arg vector. |
| 124 | + |
| 125 | +## Adding a new page |
| 126 | + |
| 127 | +1. Create `pkg/tui/pages/<kebab-name>/` with a `Page` type satisfying |
| 128 | + `app.Page`. |
| 129 | +2. Export an `ID` constant re-declaring the appropriate `app.PageID` (or |
| 130 | + add a new one to `pkg/tui/app/page.go`). |
| 131 | +3. Register a factory in `pkg/tui/tui.go`'s `registerPages`. |
| 132 | +4. Emit `app.PushMsg` / `app.PopMsg` / `app.ExitMsg` / |
| 133 | + `app.RunAnalyzerMsg` from the page as appropriate; never touch the |
| 134 | + stack directly. |
0 commit comments