mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 17:52:33 +02:00
fix: log silent errors, harden proxy auth, optimize db and render pipeline
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -41,6 +41,7 @@ type Config struct {
|
|||||||
DefaultInterceptEnabled bool `mapstructure:"default_intercept_enabled"`
|
DefaultInterceptEnabled bool `mapstructure:"default_intercept_enabled"`
|
||||||
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
||||||
AutoForwardRegex []string `mapstructure:"auto_forward_regex"`
|
AutoForwardRegex []string `mapstructure:"auto_forward_regex"`
|
||||||
|
QueueSize int `mapstructure:"queue_size"`
|
||||||
} `mapstructure:"intercept"`
|
} `mapstructure:"intercept"`
|
||||||
|
|
||||||
Replay struct {
|
Replay struct {
|
||||||
@@ -82,7 +83,7 @@ func WriteDefaultConfig(path string) error {
|
|||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
return fmt.Errorf("create config dir: %w", err)
|
return fmt.Errorf("create config dir: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, defaultConfig, 0o644); err != nil {
|
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
|
||||||
return fmt.Errorf("write config: %w", err)
|
return fmt.Errorf("write config: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ app:
|
|||||||
project_dir: ~/.local/share/spilltea
|
project_dir: ~/.local/share/spilltea
|
||||||
plugins_dir: ~/.config/spilltea/plugins
|
plugins_dir: ~/.config/spilltea/plugins
|
||||||
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
||||||
proxy_auth: "" # require basic auth to use the proxy, format: user:pass (empty = disabled)
|
proxy_auth: "" # require basic auth to use the proxy, format: user:pass (empty = disabled). Also run: chmod 600 ~/.config/spilltea/config.yaml
|
||||||
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
|
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
|
||||||
external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait)
|
external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait)
|
||||||
|
|
||||||
intercept:
|
intercept:
|
||||||
default_intercept_enabled: true
|
default_intercept_enabled: true
|
||||||
default_capture_response: false
|
default_capture_response: false
|
||||||
|
queue_size: 64 # max pending intercepted requests/responses before the proxy blocks
|
||||||
auto_forward_regex:
|
auto_forward_regex:
|
||||||
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
conn *sql.DB
|
conn *sql.DB
|
||||||
|
roConn *sql.DB
|
||||||
path string
|
path string
|
||||||
dedupMu sync.Mutex
|
dedupMu sync.Mutex
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,17 @@ func Open(path string) (*DB, error) {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
roConn, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
roConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.roConn = roConn
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +106,7 @@ func (d *DB) Close() error {
|
|||||||
if d == nil {
|
if d == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
_ = d.roConn.Close()
|
||||||
return d.conn.Close()
|
return d.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-12
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,11 @@ func (d *DB) InsertEntry(e Entry, body string) (Entry, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
e.ID, _ = res.LastInsertId()
|
var idErr error
|
||||||
|
e.ID, idErr = res.LastInsertId()
|
||||||
|
if idErr != nil {
|
||||||
|
log.Printf("db: LastInsertId: %v", idErr)
|
||||||
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,19 +118,11 @@ func (d *DB) SearchEntries(term string) ([]Entry, error) {
|
|||||||
|
|
||||||
// QueryEntries runs a WHERE expression supplied by the user against the entries
|
// QueryEntries runs a WHERE expression supplied by the user against the entries
|
||||||
// table (e.g. "status_code = 404" or "host LIKE '%example.com%'").
|
// table (e.g. "status_code = 404" or "host LIKE '%example.com%'").
|
||||||
// It opens a dedicated read-only connection so that any DML or DDL in the
|
// Uses the persistent read-only connection (PRAGMA query_only=ON) so that any
|
||||||
// user-supplied expression is rejected by SQLite before it can execute.
|
// DML or DDL in the user-supplied expression is rejected by SQLite before it executes.
|
||||||
func (d *DB) QueryEntries(where string) ([]Entry, error) {
|
func (d *DB) QueryEntries(where string) ([]Entry, error) {
|
||||||
roConn, err := sql.Open("sqlite", d.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer roConn.Close()
|
|
||||||
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where)
|
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where)
|
||||||
rows, err := roConn.Query(q)
|
rows, err := d.roConn.Query(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,13 @@ func (b *Broker) SetOnNewEntry(cb func(db.Entry)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBroker() *Broker {
|
func NewBroker() *Broker {
|
||||||
|
size := config.Global.Intercept.QueueSize
|
||||||
|
if size <= 0 {
|
||||||
|
size = 64
|
||||||
|
}
|
||||||
b := &Broker{
|
b := &Broker{
|
||||||
Incoming: make(chan *PendingRequest, 64),
|
Incoming: make(chan *PendingRequest, size),
|
||||||
IncomingResponse: make(chan *PendingResponse, 64),
|
IncomingResponse: make(chan *PendingResponse, size),
|
||||||
}
|
}
|
||||||
b.SetAutoForwardRegex(config.Global.Intercept.AutoForwardRegex)
|
b.SetAutoForwardRegex(config.Global.Intercept.AutoForwardRegex)
|
||||||
return b
|
return b
|
||||||
|
|||||||
@@ -182,7 +182,9 @@ func (m *Manager) TogglePlugin(name string) {
|
|||||||
configText := found.ConfigText
|
configText := found.ConfigText
|
||||||
found.mu.Unlock()
|
found.mu.Unlock()
|
||||||
if m.db != nil {
|
if m.db != nil {
|
||||||
_ = m.db.SavePluginState(name, enabled, configText)
|
if err := m.db.SavePluginState(name, enabled, configText); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return
|
return
|
||||||
@@ -195,7 +197,9 @@ func (m *Manager) TogglePlugin(name string) {
|
|||||||
if ret == lua.LFalse {
|
if ret == lua.LFalse {
|
||||||
p.Enabled = false
|
p.Enabled = false
|
||||||
if m.db != nil {
|
if m.db != nil {
|
||||||
_ = m.db.SavePluginState(p.Name, false, p.ConfigText)
|
if err := m.db.SavePluginState(p.Name, false, p.ConfigText); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +245,9 @@ func (m *Manager) SaveConfig(name, configText string) {
|
|||||||
_, hasOnConfig := found.hooks["on_config"]
|
_, hasOnConfig := found.hooks["on_config"]
|
||||||
found.mu.Unlock()
|
found.mu.Unlock()
|
||||||
if m.db != nil {
|
if m.db != nil {
|
||||||
_ = m.db.SavePluginState(name, enabled, configText)
|
if err := m.db.SavePluginState(name, enabled, configText); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !hasOnConfig {
|
if !hasOnConfig {
|
||||||
return
|
return
|
||||||
@@ -282,7 +288,9 @@ func (m *Manager) RunOnStart() {
|
|||||||
if ret == lua.LFalse {
|
if ret == lua.LFalse {
|
||||||
p.Enabled = false
|
p.Enabled = false
|
||||||
if m.db != nil {
|
if m.db != nil {
|
||||||
_ = m.db.SavePluginState(p.Name, false, p.ConfigText)
|
if err := m.db.SavePluginState(p.Name, false, p.ConfigText); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -46,7 +47,6 @@ func (a *interceptAddon) Request(f *goproxy.Flow) {
|
|||||||
switch a.plugins.RunSyncOnRequest(f) {
|
switch a.plugins.RunSyncOnRequest(f) {
|
||||||
case intercept.Drop:
|
case intercept.Drop:
|
||||||
f.Response = dropResponse()
|
f.Response = dropResponse()
|
||||||
go a.plugins.RunAsyncOnRequest(f)
|
|
||||||
return
|
return
|
||||||
case intercept.Forward:
|
case intercept.Forward:
|
||||||
go a.plugins.RunAsyncOnRequest(f)
|
go a.plugins.RunAsyncOnRequest(f)
|
||||||
@@ -133,7 +133,9 @@ func Start(broker *intercept.Broker, mgr *plugins.Manager) error {
|
|||||||
wantUser, wantPass := parts[0], parts[1]
|
wantUser, wantPass := parts[0], parts[1]
|
||||||
p.SetAuthProxy(func(res http.ResponseWriter, req *http.Request) (bool, error) {
|
p.SetAuthProxy(func(res http.ResponseWriter, req *http.Request) (bool, error) {
|
||||||
user, pass, ok := parseBasicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
user, pass, ok := parseBasicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
||||||
if !ok || user != wantUser || pass != wantPass {
|
userOK := subtle.ConstantTimeCompare([]byte(user), []byte(wantUser))
|
||||||
|
passOK := subtle.ConstantTimeCompare([]byte(pass), []byte(wantPass))
|
||||||
|
if !ok || userOK&passOK != 1 {
|
||||||
res.Header().Set("Proxy-Authenticate", `Basic realm="spilltea"`)
|
res.Header().Set("Proxy-Authenticate", `Basic realm="spilltea"`)
|
||||||
return false, fmt.Errorf("invalid credentials")
|
return false, fmt.Errorf("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-6
@@ -28,6 +28,12 @@ type Styles struct {
|
|||||||
|
|
||||||
PagerDotActive string
|
PagerDotActive string
|
||||||
PagerDotInactive string
|
PagerDotInactive string
|
||||||
|
|
||||||
|
methodGet lipgloss.Style
|
||||||
|
methodPost lipgloss.Style
|
||||||
|
methodPutPatch lipgloss.Style
|
||||||
|
methodDelete lipgloss.Style
|
||||||
|
methodDefault lipgloss.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
var S *Styles
|
var S *Styles
|
||||||
@@ -46,6 +52,7 @@ func Init(cfg *config.Config) {
|
|||||||
primary := lipgloss.Color("#" + c.Base0D) // Accent: primary
|
primary := lipgloss.Color("#" + c.Base0D) // Accent: primary
|
||||||
purple := lipgloss.Color("#" + c.Base0E) // Purple: editing
|
purple := lipgloss.Color("#" + c.Base0E) // Purple: editing
|
||||||
|
|
||||||
|
methodBase := lipgloss.NewStyle().Bold(true).Width(7)
|
||||||
S = &Styles{
|
S = &Styles{
|
||||||
Primary: primary,
|
Primary: primary,
|
||||||
Success: success,
|
Success: success,
|
||||||
@@ -74,6 +81,12 @@ func Init(cfg *config.Config) {
|
|||||||
|
|
||||||
PagerDotActive: lipgloss.NewStyle().Foreground(primary).SetString("•").String(),
|
PagerDotActive: lipgloss.NewStyle().Foreground(primary).SetString("•").String(),
|
||||||
PagerDotInactive: lipgloss.NewStyle().Foreground(subtle).SetString("•").String(),
|
PagerDotInactive: lipgloss.NewStyle().Foreground(subtle).SetString("•").String(),
|
||||||
|
|
||||||
|
methodGet: methodBase.Foreground(success),
|
||||||
|
methodPost: methodBase.Foreground(warning),
|
||||||
|
methodPutPatch: methodBase.Foreground(primary),
|
||||||
|
methodDelete: methodBase.Foreground(errCol),
|
||||||
|
methodDefault: methodBase.Foreground(text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,17 +103,16 @@ func NewHelp() help.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Styles) Method(method string) lipgloss.Style {
|
func (s *Styles) Method(method string) lipgloss.Style {
|
||||||
base := lipgloss.NewStyle().Bold(true).Width(7)
|
|
||||||
switch method {
|
switch method {
|
||||||
case "GET":
|
case "GET":
|
||||||
return base.Foreground(s.Success)
|
return s.methodGet
|
||||||
case "POST":
|
case "POST":
|
||||||
return base.Foreground(s.Warning)
|
return s.methodPost
|
||||||
case "PUT", "PATCH":
|
case "PUT", "PATCH":
|
||||||
return base.Foreground(s.Primary)
|
return s.methodPutPatch
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return base.Foreground(s.Error)
|
return s.methodDelete
|
||||||
default:
|
default:
|
||||||
return base.Foreground(s.Text)
|
return s.methodDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,7 @@ func (m *Model) renderList() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.findings))
|
start, end := util.PageBounds(m.pager, len(m.findings))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, f := range m.findings[start:end] {
|
for i, f := range m.findings[start:end] {
|
||||||
|
|||||||
@@ -96,13 +96,7 @@ func (m *Model) renderList() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.entries))
|
start, end := util.PageBounds(m.pager, len(m.entries))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, e := range m.entries[start:end] {
|
for i, e := range m.entries[start:end] {
|
||||||
|
|||||||
@@ -109,13 +109,7 @@ func (m *Model) renderList() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
s := style.S
|
||||||
start, end := m.pager.GetSliceBounds(len(m.queue))
|
start, end := util.PageBounds(m.pager, len(m.queue))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, req := range m.queue[start:end] {
|
for i, req := range m.queue[start:end] {
|
||||||
@@ -165,13 +159,7 @@ func (m *Model) renderResponseList() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
s := style.S
|
||||||
start, end := m.responsePager.GetSliceBounds(len(m.responseQueue))
|
start, end := util.PageBounds(m.responsePager, len(m.responseQueue))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, resp := range m.responseQueue[start:end] {
|
for i, resp := range m.responseQueue[start:end] {
|
||||||
|
|||||||
@@ -143,13 +143,7 @@ func (m *Model) renderList() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.filtered))
|
start, end := util.PageBounds(m.pager, len(m.filtered))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, p := range m.filtered[start:end] {
|
for i, p := range m.filtered[start:end] {
|
||||||
|
|||||||
@@ -88,13 +88,7 @@ func (m *Model) renderList() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
s := style.S
|
||||||
start, end := m.pager.GetSliceBounds(len(m.entries))
|
start, end := util.PageBounds(m.pager, len(m.entries))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, e := range m.entries[start:end] {
|
for i, e := range m.entries[start:end] {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
@@ -27,7 +28,9 @@ func OpenExternalEditor(content string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tmpPath := f.Name()
|
tmpPath := f.Name()
|
||||||
_, _ = f.WriteString(content)
|
if _, err := f.WriteString(content); err != nil {
|
||||||
|
log.Printf("editor: writing temp file: %v", err)
|
||||||
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
return tea.ExecProcess(exec.Command(editor, tmpPath), func(err error) tea.Msg {
|
return tea.ExecProcess(exec.Command(editor, tmpPath), func(err error) tea.Msg {
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/paginator"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,18 @@ func CenterLines(lines ...string) string {
|
|||||||
return strings.Join(centered, "\n")
|
return strings.Join(centered, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageBounds returns clamped start/end indices for rendering a paginated list.
|
||||||
|
func PageBounds(p paginator.Model, total int) (start, end int) {
|
||||||
|
start, end = p.GetSliceBounds(total)
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if end < start {
|
||||||
|
end = start
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// InferScheme returns "http" for port 80, "https" otherwise.
|
// InferScheme returns "http" for port 80, "https" otherwise.
|
||||||
func InferScheme(host string) string {
|
func InferScheme(host string) string {
|
||||||
if strings.HasSuffix(host, ":80") {
|
if strings.HasSuffix(host, ":80") {
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ Findings are deduplicated per host+path+body content so repeated requests do not
|
|||||||
}
|
}
|
||||||
|
|
||||||
function on_start()
|
function on_start()
|
||||||
local handle = io.popen("command -v trufflehog 2>/dev/null")
|
local result, _ = shell_pipe("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
|
if not result or result:match("^%s*$") then
|
||||||
log("trufflehog is not installed or not in PATH")
|
log("trufflehog is not installed or not in PATH")
|
||||||
notif("TruffleHog", "trufflehog is not installed or not in PATH, plugin disabled", "error")
|
notif("TruffleHog", "trufflehog is not installed or not in PATH, plugin disabled", "error")
|
||||||
|
|||||||
Reference in New Issue
Block a user