diff options
Diffstat (limited to 'internal/plugin/api.go')
| -rw-r--r-- | internal/plugin/api.go | 313 |
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 +} |
