diff --git a/docs/plugins.md b/docs/plugins.md index 27849db..3868f44 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -31,14 +31,14 @@ Plugin = { ### Hook reference -| Hook | When called | Sync/async | Return value (sync only) | +| Hook | When called | Sync/async | Return value | | ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- | | `on_config(config_text)` | At startup and on config save | always sync | ignored | -| `on_start()` | Once at startup, after `on_config` | configurable | ignored | +| `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored | | `on_quit()` | When the app exits | always sync | ignored | -| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` | -| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` | -| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) | +| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (sync only) | +| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (sync only) | +| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) -- sync only | ## Request and response objects @@ -141,6 +141,27 @@ Each plugin gets a **config textarea** on the Plugins page. The raw text is pass ### Return values for sync hooks +**`on_start`:** + +| Return value | Effect | +| ------------ | -------------------------------------------------------------------------------------------- | +| `false` | The plugin is disabled immediately and the state is persisted (equivalent to toggling it off). | +| anything else | Ignored. | + +This is useful for prerequisite checks (binary not found, config invalid, etc.) so the plugin does not silently run in a broken state: + +```lua +function on_start() + local h = io.popen("command -v mytool 2>/dev/null") + local result = h and h:read("*a") or "" + if h then h:close() end + if result:match("^%s*$") then + notif("MyPlugin", "mytool not found, plugin disabled", "error") + return false + end +end +``` + **`on_request` and `on_response`:** | Return value | Effect | diff --git a/internal/plugins/lua.go b/internal/plugins/lua.go index bb6de9a..922626b 100644 --- a/internal/plugins/lua.go +++ b/internal/plugins/lua.go @@ -292,22 +292,19 @@ func pushEntry(L *lua.LState, e db.Entry) *lua.LTable { return t } -func callHook(p *Plugin, hookName string, args ...lua.LValue) (string, error) { +func callHook(p *Plugin, hookName string, args ...lua.LValue) (lua.LValue, error) { fn := p.L.GetGlobal(hookName) if fn == lua.LNil { - return "", nil + return lua.LNil, nil } if err := p.L.CallByParam(lua.P{ Fn: fn, NRet: 1, Protect: true, }, args...); err != nil { - return "", err + return lua.LNil, err } ret := p.L.Get(-1) p.L.Pop(1) - if s, ok := ret.(lua.LString); ok { - return string(s), nil - } - return "", nil + return ret, nil } diff --git a/internal/plugins/manager.go b/internal/plugins/manager.go index 10efba1..2a115e7 100644 --- a/internal/plugins/manager.go +++ b/internal/plugins/manager.go @@ -191,17 +191,31 @@ func (m *Manager) TogglePlugin(name string) { if !ok { return } + disableIfFalse := func(p *Plugin, ret lua.LValue) { + if ret == lua.LFalse { + p.Enabled = false + if m.db != nil { + _ = m.db.SavePluginState(p.Name, false, p.ConfigText) + } + } + } if hc.Sync { found.mu.Lock() - if _, err := callHook(found, "on_start"); err != nil { + ret, err := callHook(found, "on_start") + if err != nil { log.Printf("plugin %s on_start: %v", found.Name, err) + } else { + disableIfFalse(found, ret) } found.mu.Unlock() } else { go func() { found.mu.Lock() - if _, err := callHook(found, "on_start"); err != nil { + ret, err := callHook(found, "on_start") + if err != nil { log.Printf("plugin %s on_start: %v", found.Name, err) + } else { + disableIfFalse(found, ret) } found.mu.Unlock() }() @@ -264,17 +278,31 @@ func (m *Manager) RunOnStart() { if !ok { continue } + disableIfFalse := func(p *Plugin, ret lua.LValue) { + if ret == lua.LFalse { + p.Enabled = false + if m.db != nil { + _ = m.db.SavePluginState(p.Name, false, p.ConfigText) + } + } + } if hc.Sync { p.mu.Lock() - if _, err := callHook(p, "on_start"); err != nil { + ret, err := callHook(p, "on_start") + if err != nil { log.Printf("plugin %s on_start: %v", p.Name, err) + } else { + disableIfFalse(p, ret) } p.mu.Unlock() } else { go func(p *Plugin) { p.mu.Lock() - if _, err := callHook(p, "on_start"); err != nil { + ret, err := callHook(p, "on_start") + if err != nil { log.Printf("plugin %s on_start: %v", p.Name, err) + } else { + disableIfFalse(p, ret) } p.mu.Unlock() }(p) @@ -316,11 +344,13 @@ func (m *Manager) runSyncDecisionForPlugins(hookName string, argsFor func(*Plugi log.Printf("plugin %s %s: %v", p.Name, hookName, err) continue } - switch result { - case "drop": - return intercept.Drop - case "forward": - return intercept.Forward + if s, ok := result.(lua.LString); ok { + switch string(s) { + case "drop": + return intercept.Drop + case "forward": + return intercept.Forward + } } } return intercept.Intercept @@ -388,7 +418,7 @@ func (m *Manager) RunSyncOnHistoryEntry(e db.Entry) bool { log.Printf("plugin %s on_history_entry: %v", p.Name, err) continue } - if result == "skip" { + if s, ok := result.(lua.LString); ok && string(s) == "skip" { return false } } diff --git a/plugins/trufflehog.lua b/plugins/trufflehog.lua index b336f1a..2521830 100644 --- a/plugins/trufflehog.lua +++ b/plugins/trufflehog.lua @@ -8,11 +8,23 @@ Requires `trufflehog` v3+ to be installed and available in PATH. Each finding is stored on the **Findings** page with the matched detector output. Findings are deduplicated per host+path+body content so repeated requests do not create duplicates. ]], + on_start = { sync = false }, on_request = { sync = false }, on_response = { sync = false }, disable_by_default = true, } +function on_start() + local handle = io.popen("command -v trufflehog 2>/dev/null") + local result = handle and handle:read("*a") or "" + if handle then handle:close() end + if not result or result:match("^%s*$") then + log("trufflehog is not installed or not in PATH") + notif("TruffleHog", "trufflehog is not installed or not in PATH, plugin disabled", "error") + return false + end +end + local function scan(label, content, host, path) if not content or content == "" then return end local out, err = shell_pipe("f=$(mktemp) && cat > \"$f\" && trufflehog filesystem --no-color \"$f\"; rc=$?; rm -f \"$f\"; exit $rc", content)