summaryrefslogtreecommitdiff
path: root/internal/plugin/api.go
diff options
context:
space:
mode:
authorspiperac <[email protected]>2026-06-15 06:42:13 +0200
committerspiperac <[email protected]>2026-06-15 06:42:13 +0200
commit0a477fd288d064d265395c88dbd0f261b723804e (patch)
tree9b129f8f41c907285b89ed8385679934bb59c14f /internal/plugin/api.go
Initial release
Diffstat (limited to 'internal/plugin/api.go')
-rw-r--r--internal/plugin/api.go313
1 files changed, 313 insertions, 0 deletions
diff --git a/internal/plugin/api.go b/internal/plugin/api.go
new file mode 100644
index 0000000..986c5d2
--- /dev/null
+++ b/internal/plugin/api.go
@@ -0,0 +1,313 @@
+package plugin
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ lua "github.com/yuin/gopher-lua"
+
+ "github.com/shiv/internal/store"
+)
+
+// registerAPI registers all Lua API namespaces on the given VM.
+// logFn is called when the Lua script calls log() — the engine routes
+// these lines onto the bus after each hook call.
+// Called once per plugin at load time.
+func registerAPI(L *lua.LState, st *store.Store, logFn func(string)) {
+ registerLog(L, logFn)
+ registerHTTPClient(L)
+ registerDB(L, st)
+}
+
+// ── log ───────────────────────────────────────────────────────────────────────
+
+func registerLog(L *lua.LState, logFn func(string)) {
+ L.SetGlobal("log", L.NewFunction(func(L *lua.LState) int {
+ msg := L.CheckString(1)
+ logFn(msg)
+ return 0
+ }))
+}
+
+// ── http client ───────────────────────────────────────────────────────────────
+
+var httpClient = &http.Client{Timeout: 15 * time.Second}
+
+func registerHTTPClient(L *lua.LState) {
+ tbl := L.NewTable()
+
+ // http.get(url) → {status, headers, body} or nil, err
+ L.SetField(tbl, "get", L.NewFunction(func(L *lua.LState) int {
+ url := L.CheckString(1)
+ resp, err := httpClient.Get(url)
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ defer resp.Body.Close()
+ return pushResponse(L, resp)
+ }))
+
+ // http.post(url, body, content_type) → {status, headers, body} or nil, err
+ L.SetField(tbl, "post", L.NewFunction(func(L *lua.LState) int {
+ url := L.CheckString(1)
+ body := L.CheckString(2)
+ ct := L.OptString(3, "application/json")
+ resp, err := httpClient.Post(url, ct, strings.NewReader(body))
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ defer resp.Body.Close()
+ return pushResponse(L, resp)
+ }))
+
+ // http.request(method, url, body, headers_table) → {status, headers, body} or nil, err
+ L.SetField(tbl, "request", L.NewFunction(func(L *lua.LState) int {
+ method := L.CheckString(1)
+ url := L.CheckString(2)
+ body := L.OptString(3, "")
+ headersTbl := L.OptTable(4, nil)
+
+ var bodyReader io.Reader
+ if body != "" {
+ bodyReader = strings.NewReader(body)
+ }
+
+ req, err := http.NewRequest(method, url, bodyReader)
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+
+ if headersTbl != nil {
+ headersTbl.ForEach(func(k, v lua.LValue) {
+ req.Header.Set(k.String(), v.String())
+ })
+ }
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ defer resp.Body.Close()
+ return pushResponse(L, resp)
+ }))
+
+ L.SetGlobal("http", tbl)
+}
+
+func pushResponse(L *lua.LState, resp *http.Response) int {
+ body, _ := io.ReadAll(resp.Body)
+
+ tbl := L.NewTable()
+ L.SetField(tbl, "status", lua.LNumber(resp.StatusCode))
+ L.SetField(tbl, "body", lua.LString(body))
+
+ headers := L.NewTable()
+ for k, vals := range resp.Header {
+ L.SetField(headers, k, lua.LString(strings.Join(vals, ", ")))
+ }
+ L.SetField(tbl, "headers", headers)
+
+ L.Push(tbl)
+ return 1
+}
+
+// ── db ────────────────────────────────────────────────────────────────────────
+
+func registerDB(L *lua.LState, st *store.Store) {
+ tbl := L.NewTable()
+
+ // db.history(limit) → array of transaction tables
+ L.SetField(tbl, "history", L.NewFunction(func(L *lua.LState) int {
+ limit := L.OptInt(1, 100)
+ txs, err := st.TransactionsPage(0, store.TransactionFilter{ShowOutScope: true})
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ if limit > 0 && len(txs) > limit {
+ txs = txs[:limit]
+ }
+ arr := L.NewTable()
+ for i, tx := range txs {
+ L.RawSetInt(arr, i+1, transactionToTable(L, tx))
+ }
+ L.Push(arr)
+ return 1
+ }))
+
+ // db.history_filter(host, method, status) → array of transaction tables
+ L.SetField(tbl, "history_filter", L.NewFunction(func(L *lua.LState) int {
+ host := L.OptString(1, "")
+ method := L.OptString(2, "")
+ _ = L.OptInt(3, 0) // status filtering done client-side below
+
+ filter := store.TransactionFilter{
+ Host: host,
+ ShowOutScope: true,
+ }
+ if method != "" {
+ filter.Search = method
+ }
+
+ txs, err := st.TransactionsPage(0, filter)
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ arr := L.NewTable()
+ idx := 1
+ for _, tx := range txs {
+ L.RawSetInt(arr, idx, transactionToTable(L, tx))
+ idx++
+ }
+ L.Push(arr)
+ return 1
+ }))
+
+ // db.scope_get() → array of {id, host}
+ L.SetField(tbl, "scope_get", L.NewFunction(func(L *lua.LState) int {
+ entries, err := st.AllScopeEntries()
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ arr := L.NewTable()
+ for i, e := range entries {
+ row := L.NewTable()
+ L.SetField(row, "id", lua.LNumber(e.ID))
+ L.SetField(row, "host", lua.LString(e.Host))
+ L.RawSetInt(arr, i+1, row)
+ }
+ L.Push(arr)
+ return 1
+ }))
+
+ // db.scope_add(host) → true or nil, err
+ L.SetField(tbl, "scope_add", L.NewFunction(func(L *lua.LState) int {
+ host := L.CheckString(1)
+ if err := st.AddScopeEntry(host); err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ L.Push(lua.LTrue)
+ return 1
+ }))
+
+ // db.scope_remove(id) → true or nil, err
+ L.SetField(tbl, "scope_remove", L.NewFunction(func(L *lua.LState) int {
+ id := L.CheckInt64(1)
+ if err := st.DeleteScopeEntry(id); err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ L.Push(lua.LTrue)
+ return 1
+ }))
+
+ // db.loot_get() → array of loot tables
+ L.SetField(tbl, "loot_get", L.NewFunction(func(L *lua.LState) int {
+ entries, err := st.AllLoot()
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ arr := L.NewTable()
+ for i, e := range entries {
+ row := L.NewTable()
+ L.SetField(row, "id", lua.LNumber(e.ID))
+ L.SetField(row, "title", lua.LString(e.Title))
+ L.SetField(row, "severity", lua.LString(e.Severity))
+ L.SetField(row, "notes", lua.LString(e.Notes))
+ L.SetField(row, "raw_request", lua.LString(e.RawRequest))
+ L.SetField(row, "raw_response", lua.LString(e.RawResponse))
+ L.RawSetInt(arr, i+1, row)
+ }
+ L.Push(arr)
+ return 1
+ }))
+
+ // db.loot_add(title, severity, notes, raw_request, raw_response) → id or nil, err
+ L.SetField(tbl, "loot_add", L.NewFunction(func(L *lua.LState) int {
+ title := L.CheckString(1)
+ severity := L.CheckString(2)
+ notes := L.OptString(3, "")
+ rawReq := L.OptString(4, "")
+ rawResp := L.OptString(5, "")
+ id, err := st.AddLoot(store.LootEntry{
+ Title: title,
+ Severity: severity,
+ Notes: notes,
+ RawRequest: rawReq,
+ RawResponse: rawResp,
+ })
+ if err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ L.Push(lua.LNumber(id))
+ return 1
+ }))
+
+ // db.loot_delete(id) → true or nil, err
+ L.SetField(tbl, "loot_delete", L.NewFunction(func(L *lua.LState) int {
+ id := L.CheckInt64(1)
+ if err := st.DeleteLoot(id); err != nil {
+ L.Push(lua.LNil)
+ L.Push(lua.LString(err.Error()))
+ return 2
+ }
+ L.Push(lua.LTrue)
+ return 1
+ }))
+
+ L.SetGlobal("db", tbl)
+}
+
+// ── helpers ───────────────────────────────────────────────────────────────────
+
+func transactionToTable(L *lua.LState, tx store.Transaction) *lua.LTable {
+ tbl := L.NewTable()
+ L.SetField(tbl, "id", lua.LNumber(tx.ID))
+ L.SetField(tbl, "host", lua.LString(tx.Host))
+ L.SetField(tbl, "method", lua.LString(tx.Method))
+ L.SetField(tbl, "url", lua.LString(tx.URL))
+ L.SetField(tbl, "proto", lua.LString(tx.Proto))
+ L.SetField(tbl, "status", lua.LNumber(tx.StatusCode))
+ L.SetField(tbl, "duration_ms", lua.LNumber(tx.DurationMs))
+ L.SetField(tbl, "tls", lua.LBool(tx.TLS))
+ L.SetField(tbl, "in_scope", lua.LBool(tx.InScope))
+ L.SetField(tbl, "req_body", lua.LString(tx.ReqBody))
+ L.SetField(tbl, "resp_body", lua.LString(tx.RespBody))
+ L.SetField(tbl, "timestamp", lua.LString(tx.Timestamp.UTC().Format(time.RFC3339)))
+
+ reqH := L.NewTable()
+ for k, vals := range tx.ReqHeaders {
+ L.SetField(reqH, k, lua.LString(strings.Join(vals, ", ")))
+ }
+ L.SetField(tbl, "req_headers", reqH)
+
+ respH := L.NewTable()
+ for k, vals := range tx.RespHeaders {
+ L.SetField(respH, k, lua.LString(strings.Join(vals, ", ")))
+ }
+ L.SetField(tbl, "resp_headers", respH)
+
+ return tbl
+}