Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-20 19:31:31 +02:00
parent 6e673f5c11
commit 44b3c67a37
6 changed files with 349 additions and 270 deletions
+2
View File
@@ -8,6 +8,7 @@
hooks = { hooks = {
gofmt.enable = true; gofmt.enable = true;
govet.enable = true; govet.enable = true;
stylua.enable = true;
gomod2nix = { gomod2nix = {
enable = true; enable = true;
@@ -53,6 +54,7 @@ in
go go
python3 python3
doctoc doctoc
stylua
trufflehog trufflehog
gomod2nixPkgs.gomod2nix gomod2nixPkgs.gomod2nix
] ]
+16 -16
View File
@@ -1,6 +1,6 @@
Plugin = { Plugin = {
name = "Inject Header", name = "Inject Header",
description = [[ description = [[
Inject custom headers into every intercepted request. Inject custom headers into every intercepted request.
**Config** (YAML): **Config** (YAML):
@@ -9,26 +9,26 @@ headers:
- "X-My-Header: myvalue" - "X-My-Header: myvalue"
``` ```
]], ]],
on_request = { sync = true }, on_request = { sync = true },
} }
local headers = {} local headers = {}
function on_config() function on_config()
headers = {} headers = {}
local cfg = get_config() local cfg = get_config()
if cfg and cfg.headers then if cfg and cfg.headers then
for _, line in ipairs(cfg.headers) do for _, line in ipairs(cfg.headers) do
local name, value = line:match("^([^:]+):%s*(.+)$") local name, value = line:match("^([^:]+):%s*(.+)$")
if name and value then if name and value then
table.insert(headers, { name = name, value = value }) table.insert(headers, { name = name, value = value })
end end
end end
end end
end end
function on_request(req) function on_request(req)
for _, h in ipairs(headers) do for _, h in ipairs(headers) do
req:set_header(h.name, h.value) req:set_header(h.name, h.value)
end end
end end
+46 -44
View File
@@ -1,6 +1,6 @@
Plugin = { Plugin = {
name = "IP Filter (Whitelist/Blacklist)", name = "IP Filter (Whitelist/Blacklist)",
description = [[ description = [[
Checks that the proxy's outbound IP is in an allowed list on startup. Checks that the proxy's outbound IP is in an allowed list on startup.
**Config** (YAML): **Config** (YAML):
@@ -11,61 +11,63 @@ ips:
``` ```
- If no IPs are configured, the check is skipped. - If no IPs are configured, the check is skipped.
]], ]],
on_start = { sync = false }, on_start = { sync = false },
disable_by_default = true, disable_by_default = true,
} }
local whitelist = {} local whitelist = {}
local blacklist = {} local blacklist = {}
function on_config() function on_config()
whitelist, blacklist = {}, {} whitelist, blacklist = {}, {}
local cfg = get_config() local cfg = get_config()
if cfg and cfg.ips then if cfg and cfg.ips then
for _, entry in ipairs(cfg.ips) do for _, entry in ipairs(cfg.ips) do
local trimmed = entry:match("^%s*(.-)%s*$") local trimmed = entry:match("^%s*(.-)%s*$")
if trimmed ~= "" then if trimmed ~= "" then
if trimmed:sub(1, 1) == "!" then if trimmed:sub(1, 1) == "!" then
local ip = trimmed:sub(2):match("^%s*(.-)%s*$") local ip = trimmed:sub(2):match("^%s*(.-)%s*$")
if ip ~= "" then table.insert(blacklist, ip) end if ip ~= "" then
else table.insert(blacklist, ip)
table.insert(whitelist, trimmed) end
end else
end table.insert(whitelist, trimmed)
end end
end end
end
end
end end
function on_start() function on_start()
if #whitelist == 0 and #blacklist == 0 then if #whitelist == 0 and #blacklist == 0 then
return return
end end
local result, err = shell_pipe("curl -sf https://api.ipify.org 2>/dev/null") local result, err = shell_pipe("curl -sf https://api.ipify.org 2>/dev/null")
result = result and result:match("^%s*(.-)%s*$") or nil result = result and result:match("^%s*(.-)%s*$") or nil
if err or not result or result == "" then if err or not result or result == "" then
log("could not determine outbound IP, skipping check") log("could not determine outbound IP, skipping check")
notif("IP Filter", "Could not determine outbound IP, skipping check", "warning") notif("IP Filter", "Could not determine outbound IP, skipping check", "warning")
return return
end end
for _, ip in ipairs(blacklist) do for _, ip in ipairs(blacklist) do
if result == ip then if result == ip then
notif("IP Filter", "Outbound IP " .. result .. " is blacklisted!", "error") notif("IP Filter", "Outbound IP " .. result .. " is blacklisted!", "error")
return return
end end
end end
if #whitelist == 0 then if #whitelist == 0 then
return return
end end
for _, ip in ipairs(whitelist) do for _, ip in ipairs(whitelist) do
if result == ip then if result == ip then
return return
end end
end end
notif("IP Filter", "Outbound IP " .. result .. " is not in the whitelist!", "error") notif("IP Filter", "Outbound IP " .. result .. " is not in the whitelist!", "error")
end end
+68 -50
View File
@@ -1,6 +1,6 @@
Plugin = { Plugin = {
name = "Scopes", name = "Scopes",
description = [[ description = [[
Auto-forward requests and exclude them from history based on patterns. Auto-forward requests and exclude them from history based on patterns.
**Config** (YAML): **Config** (YAML):
@@ -34,70 +34,88 @@ patterns:
- "h:^$" - "h:^$"
``` ```
]], ]],
priority = 100, priority = 100,
on_request = { sync = true }, on_request = { sync = true },
on_response = { sync = true }, on_response = { sync = true },
on_history_entry = { sync = true }, on_history_entry = { sync = true },
} }
local blacklist = {} local blacklist = {}
local whitelist = {} local whitelist = {}
local blacklist_req = {} local blacklist_req = {}
local whitelist_req = {} local whitelist_req = {}
local blacklist_hist = {} local blacklist_hist = {}
local whitelist_hist = {} local whitelist_hist = {}
function on_config() function on_config()
blacklist, whitelist = {}, {} blacklist, whitelist = {}, {}
blacklist_req, whitelist_req = {}, {} blacklist_req, whitelist_req = {}, {}
blacklist_hist, whitelist_hist = {}, {} blacklist_hist, whitelist_hist = {}, {}
local cfg = get_config() local cfg = get_config()
if not cfg or not cfg.patterns then return end if not cfg or not cfg.patterns then
for _, line in ipairs(cfg.patterns) do return
local trimmed = line:match("^%s*(.-)%s*$") end
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then for _, line in ipairs(cfg.patterns) do
local scope = trimmed:match("^([rh]):") local trimmed = line:match("^%s*(.-)%s*$")
local rest = scope and trimmed:sub(3) or trimmed if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
local is_bl = rest:sub(1, 1) == "!" local scope = trimmed:match("^([rh]):")
local pattern = is_bl and rest:sub(2) or rest local rest = scope and trimmed:sub(3) or trimmed
if scope == "r" then local is_bl = rest:sub(1, 1) == "!"
table.insert(is_bl and blacklist_req or whitelist_req, pattern) local pattern = is_bl and rest:sub(2) or rest
elseif scope == "h" then if scope == "r" then
table.insert(is_bl and blacklist_hist or whitelist_hist, pattern) table.insert(is_bl and blacklist_req or whitelist_req, pattern)
else elseif scope == "h" then
table.insert(is_bl and blacklist or whitelist, pattern) table.insert(is_bl and blacklist_hist or whitelist_hist, pattern)
end else
end table.insert(is_bl and blacklist or whitelist, pattern)
end end
end
end
end end
local function check_skip(url, bl_extra, wl_extra) local function check_skip(url, bl_extra, wl_extra)
for _, p in ipairs(blacklist) do for _, p in ipairs(blacklist) do
if url:match(p) then return true end if url:match(p) then
end return true
for _, p in ipairs(bl_extra) do end
if url:match(p) then return true end end
end for _, p in ipairs(bl_extra) do
local wl = {} if url:match(p) then
for _, p in ipairs(whitelist) do wl[#wl + 1] = p end return true
for _, p in ipairs(wl_extra) do wl[#wl + 1] = p end end
if #wl > 0 then end
for _, p in ipairs(wl) do local wl = {}
if url:match(p) then return false end for _, p in ipairs(whitelist) do
end wl[#wl + 1] = p
return true end
end for _, p in ipairs(wl_extra) do
return false wl[#wl + 1] = p
end
if #wl > 0 then
for _, p in ipairs(wl) do
if url:match(p) then
return false
end
end
return true
end
return false
end end
function on_request(req) function on_request(req)
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end if check_skip(req.url, blacklist_req, whitelist_req) then
return "forward"
end
end end
function on_response(req) function on_response(req)
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end if check_skip(req.url, blacklist_req, whitelist_req) then
return "forward"
end
end end
function on_history_entry(entry) function on_history_entry(entry)
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then return "skip" end if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then
return "skip"
end
end end
+166 -120
View File
@@ -1,70 +1,91 @@
Plugin = { Plugin = {
name = "Secret Scan", name = "Secret Scan",
description = [[ description = [[
Scans HTML, JavaScript and JSON content (requests and responses) for hardcoded Scans HTML, JavaScript and JSON content (requests and responses) for hardcoded
secrets by matching common secret key names followed by a non-trivial value. secrets by matching common secret key names followed by a non-trivial value.
Uses `grep -E` (available on all Unix systems, no extra dependencies). Uses `grep -E` (available on all Unix systems, no extra dependencies).
]], ]],
on_request = { sync = false }, on_request = { sync = false },
on_response = { sync = false }, on_response = { sync = false },
disable_by_default = true, disable_by_default = true,
} }
local CONTENT_TYPES = { local CONTENT_TYPES = {
"text/html", "text/html",
"text/javascript", "text/javascript",
"application/javascript", "application/javascript",
"application/json", "application/json",
} }
-- Key name alternation (case-insensitive via grep -i) -- Key name alternation (case-insensitive via grep -i)
-- Suffixes are required (no bare generic keyword alone). -- Suffixes are required (no bare generic keyword alone).
local KEYS = { local KEYS = {
"access(_key|_token)", "accessid_secret", "account(_key|_sid)", "access(_key|_token)",
"admin_pass(word)?", "admin_user", "accessid_secret",
"(algolia|aws|gcp|azure|heroku|firebase|github|gitlab|slack|datadog|stripe|twilio|vercel|supabase|sendgrid|cloudinary|cloudflare|bitbucket|npm|netlify|auth0|okta|sentry)(_?(api|secret|access)(_?(key|token|id|sid|secret))?|_?(key|token|id|sid|secret))", "account(_key|_sid)",
"ansible_vault_password", "aos_key", "admin_pass(word)?",
"api(_key|_secret|_token)", "admin_user",
"app_(id|key|secret)", "application(_key|_id|_secret)", "(algolia|aws|gcp|azure|heroku|firebase|github|gitlab|slack|datadog|stripe|twilio|vercel|supabase|sendgrid|cloudinary|cloudflare|bitbucket|npm|netlify|auth0|okta|sentry)(_?(api|secret|access)(_?(key|token|id|sid|secret))?|_?(key|token|id|sid|secret))",
"auth(_token|_secret|orization)", "authkey", "authsecret", "ansible_vault_password",
"bearer_?token", "aos_key",
"bucket(_password|_key)", "api(_key|_secret|_token)",
"cert_?pass(word)?", "certificate_password", "app_(id|key|secret)",
"client(_id|_secret)", "application(_key|_id|_secret)",
"codecov_token", "consumer_(key|secret)", "auth(_token|_secret|orization)",
"connection_?string", "credentials?", "crypt(_key|_secret)", "authkey",
"db_(password|passwd|user(name)?)", "authsecret",
"deploy(_key|_password|_token)", "bearer_?token",
"docker_?pass(word)?", "dockerhub_?password", "bucket(_password|_key)",
"encryption_(key|password)", "cert_?pass(word)?",
"jwt_secret", "json_web_token", "certificate_password",
"keycloak_secret", "kubernetes_token", "client(_id|_secret)",
"ldap_(password|bindpw)", "login(_password|_token)", "codecov_token",
"mail_?password", "mail_smtp_pass", "consumer_(key|secret)",
"mysql_password", "mongo_password", "connection_?string",
"netlify_token", "npm(_token|_auth_token)", "credentials?",
"oauth(_token|_secret)", "crypt(_key|_secret)",
"openai_(api_key|secret)", "db_(password|passwd|user(name)?)",
"pass(word)?", "passwd", "deploy(_key|_password|_token)",
"private(_key|_token)", "docker_?pass(word)?",
"rds_password", "dockerhub_?password",
"s3(_key|_secret|_access_key_id)", "encryption_(key|password)",
"secret(_key|_token|_id)", "security_token", "jwt_secret",
"sendgrid_api_key", "json_web_token",
"ses_(smtp|access|secret)", "keycloak_secret",
"service(_account|_key|_token)", "kubernetes_token",
"smtp_pass(word)?", "smtp_secret", "ldap_(password|bindpw)",
"sonar_token", "login(_password|_token)",
"ssh(_key|_private_key|_rsa)", "mail_?password",
"supabase(_anon|_service)?_key", "mail_smtp_pass",
"symfony_secret", "mysql_password",
"telegram_bot_token", "mongo_password",
"token", "netlify_token",
"travis_token", "npm(_token|_auth_token)",
"vault(_token|_secret)", "oauth(_token|_secret)",
"webhook(_secret|_token)", "openai_(api_key|secret)",
"zapier_webhook_token", "pass(word)?",
"passwd",
"private(_key|_token)",
"rds_password",
"s3(_key|_secret|_access_key_id)",
"secret(_key|_token|_id)",
"security_token",
"sendgrid_api_key",
"ses_(smtp|access|secret)",
"service(_account|_key|_token)",
"smtp_pass(word)?",
"smtp_secret",
"sonar_token",
"ssh(_key|_private_key|_rsa)",
"supabase(_anon|_service)?_key",
"symfony_secret",
"telegram_bot_token",
"token",
"travis_token",
"vault(_token|_secret)",
"webhook(_secret|_token)",
"zapier_webhook_token",
} }
-- Built once at load time. -- Built once at load time.
@@ -74,93 +95,118 @@ local KEYS = {
-- [[:space:]]*[:=] REQUIRED: actual = or : assignment operator -- [[:space:]]*[:=] REQUIRED: actual = or : assignment operator
-- [[:space:]]*"? optional whitespace + opening quote -- [[:space:]]*"? optional whitespace + opening quote
-- [a-zA-Z0-9+/=_.-]{8,} the secret value, at least 8 chars -- [a-zA-Z0-9+/=_.-]{8,} the secret value, at least 8 chars
local KEY_PAT = "(" .. table.concat(KEYS, "|") .. ")" local KEY_PAT = "(" .. table.concat(KEYS, "|") .. ")"
local FULL_PAT = KEY_PAT .. '[a-z0-9._-]{0,20}[^=:a-zA-Z0-9_]{0,3}[[:space:]]*[:=][[:space:]]*"?[a-zA-Z0-9+/=_.-]{8,}' local FULL_PAT = KEY_PAT .. '[a-z0-9._-]{0,20}[^=:a-zA-Z0-9_]{0,3}[[:space:]]*[:=][[:space:]]*"?[a-zA-Z0-9+/=_.-]{8,}'
local GREP_CMD = "grep -Eoni '" .. FULL_PAT .. "'" local GREP_CMD = "grep -Eoni '" .. FULL_PAT .. "'"
local function is_relevant(ct) local function is_relevant(ct)
if not ct or ct == "" then return false end if not ct or ct == "" then
ct = ct:lower() return false
for _, t in ipairs(CONTENT_TYPES) do end
if ct:find(t, 1, true) then return true end ct = ct:lower()
end for _, t in ipairs(CONTENT_TYPES) do
return false if ct:find(t, 1, true) then
return true
end
end
return false
end end
local function build_context(lines, linenum) local function build_context(lines, linenum)
local lo = math.max(1, linenum - 6) local lo = math.max(1, linenum - 6)
local hi = math.min(#lines, linenum + 6) local hi = math.min(#lines, linenum + 6)
local before, after = {}, {} local before, after = {}, {}
for i = lo, linenum - 1 do for i = lo, linenum - 1 do
local l = lines[i] or "" local l = lines[i] or ""
if #l > 120 then l = l:sub(1, 120) .. "..." end if #l > 120 then
table.insert(before, l) l = l:sub(1, 120) .. "..."
end end
for i = linenum + 1, hi do table.insert(before, l)
local l = lines[i] or "" end
if #l > 120 then l = l:sub(1, 120) .. "..." end for i = linenum + 1, hi do
table.insert(after, l) local l = lines[i] or ""
end if #l > 120 then
l = l:sub(1, 120) .. "..."
end
table.insert(after, l)
end
local matched_line = lines[linenum] or "" local matched_line = lines[linenum] or ""
if #matched_line > 200 then matched_line = matched_line:sub(1, 200) .. "..." end if #matched_line > 200 then
matched_line = matched_line:sub(1, 200) .. "..."
end
local parts = {} local parts = {}
if #before > 0 then if #before > 0 then
table.insert(parts, "```\n" .. table.concat(before, "\n") .. "\n```") table.insert(parts, "```\n" .. table.concat(before, "\n") .. "\n```")
end end
table.insert(parts, "> **`" .. matched_line .. "`**") table.insert(parts, "> **`" .. matched_line .. "`**")
if #after > 0 then if #after > 0 then
table.insert(parts, "```\n" .. table.concat(after, "\n") .. "\n```") table.insert(parts, "```\n" .. table.concat(after, "\n") .. "\n```")
end end
return table.concat(parts, "\n\n") return table.concat(parts, "\n\n")
end end
local function scan(label, ct, body, host, path) local function scan(label, ct, body, host, path)
if not is_relevant(ct) then return end if not is_relevant(ct) then
if not body or body == "" then return end return
end
if not body or body == "" then
return
end
local out, err = shell_pipe(GREP_CMD, body) local out, err = shell_pipe(GREP_CMD, body)
if err and err ~= "" then if err and err ~= "" then
log("grep error on " .. label .. " for " .. host .. path .. ": " .. err) log("grep error on " .. label .. " for " .. host .. path .. ": " .. err)
return return
end end
if not out or out == "" then return end if not out or out == "" then
return
end
local lines = {} local lines = {}
for line in (body .. "\n"):gmatch("([^\n]*)\n") do for line in (body .. "\n"):gmatch("([^\n]*)\n") do
table.insert(lines, line) table.insert(lines, line)
end end
for entry in out:gmatch("[^\n]+") do for entry in out:gmatch("[^\n]+") do
local linenum_str, matched = entry:match("^(%d+):(.+)$") local linenum_str, matched = entry:match("^(%d+):(.+)$")
if linenum_str then if linenum_str then
local linenum = tonumber(linenum_str) local linenum = tonumber(linenum_str)
matched = matched:match("^%s*(.-)%s*$") matched = matched:match("^%s*(.-)%s*$")
if matched ~= "" then if matched ~= "" then
local display = matched local display = matched
if #display > 200 then display = display:sub(1, 200) .. "..." end if #display > 200 then
local ctx = build_context(lines, linenum) display = display:sub(1, 200) .. "..."
create_finding({ end
title = "Potential secret in " .. label .. " (" .. host .. ")", local ctx = build_context(lines, linenum)
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n**Match:** `" .. display .. "`\n\n" .. ctx, create_finding({
key = host .. "|" .. path .. "|" .. label .. "|" .. matched, title = "Potential secret in " .. label .. " (" .. host .. ")",
severity = "high", description = "**Host:** `"
}) .. host
end .. "` \n**Path:** `"
end .. path
end .. "`\n\n**Match:** `"
.. display
.. "`\n\n"
.. ctx,
key = host .. "|" .. path .. "|" .. label .. "|" .. matched,
severity = "high",
})
end
end
end
end end
function on_request(req) function on_request(req)
scan("request", req.headers["Content-Type"] or "", req:get_body(), req.host, req.path) scan("request", req.headers["Content-Type"] or "", req:get_body(), req.host, req.path)
end end
function on_response(req, res) function on_response(req, res)
local ct = "" local ct = ""
if res.headers then if res.headers then
ct = res.headers["Content-Type"] or "" ct = res.headers["Content-Type"] or ""
end end
scan("response", ct, res:get_body(), req.host, req.path) scan("response", ct, res:get_body(), req.host, req.path)
end end
+51 -40
View File
@@ -1,6 +1,6 @@
Plugin = { Plugin = {
name = "TruffleHog", name = "TruffleHog",
description = [[ description = [[
Scans request and response bodies for secrets using [TruffleHog](https://github.com/trufflesecurity/trufflehog). Scans request and response bodies for secrets using [TruffleHog](https://github.com/trufflesecurity/trufflehog).
Requires `trufflehog` v3+ to be installed and available in PATH. Requires `trufflehog` v3+ to be installed and available in PATH.
@@ -8,54 +8,65 @@ Requires `trufflehog` v3+ to be installed and available in PATH.
Each finding is stored on the **Findings** page with the matched detector output. 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. Findings are deduplicated per host+path+body content so repeated requests do not create duplicates.
]], ]],
on_start = { sync = false }, on_start = { sync = false },
on_request = { sync = false }, on_request = { sync = false },
on_response = { sync = false }, on_response = { sync = false },
disable_by_default = true, disable_by_default = true,
} }
function on_start() function on_start()
local result, _ = shell_pipe("command -v trufflehog 2>/dev/null") local result, _ = shell_pipe("command -v trufflehog 2>/dev/null")
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")
return false return false
end end
end end
local function scan(label, content, host, path) local function scan(label, content, host, path)
if not content or content == "" then return end if not content or content == "" then
local out, err = shell_pipe("f=$(mktemp) && cat > \"$f\" && trufflehog filesystem --no-color \"$f\"; rc=$?; rm -f \"$f\"; exit $rc", content) return
if err and err ~= "" then end
log("trufflehog error on " .. label .. ": " .. err) local out, err = shell_pipe(
return 'f=$(mktemp) && cat > "$f" && trufflehog filesystem --no-color "$f"; rc=$?; rm -f "$f"; exit $rc',
end content
if not out or out == "" then return end )
local blocks = {} if err and err ~= "" then
local current = nil log("trufflehog error on " .. label .. ": " .. err)
for line in out:gmatch("[^\n]+") do return
if line:match("^Found ") then end
if current then table.insert(blocks, current) end if not out or out == "" then
current = line return
elseif current then end
current = current .. "\n" .. line local blocks = {}
end local current = nil
end for line in out:gmatch("[^\n]+") do
if current then table.insert(blocks, current) end if line:match("^Found ") then
for _, block in ipairs(blocks) do if current then
create_finding({ table.insert(blocks, current)
title = "Secret detected in " .. label .. " (" .. host .. ")", end
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```", current = line
key = host .. "|" .. path .. "|" .. label .. "|" .. block, elseif current then
severity = "high", current = current .. "\n" .. line
}) end
end end
if current then
table.insert(blocks, current)
end
for _, block in ipairs(blocks) do
create_finding({
title = "Secret detected in " .. label .. " (" .. host .. ")",
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```",
key = host .. "|" .. path .. "|" .. label .. "|" .. block,
severity = "high",
})
end
end end
function on_request(req) function on_request(req)
scan("request", req:get_body(), req.host, req.path) scan("request", req:get_body(), req.host, req.path)
end end
function on_response(req, res) function on_response(req, res)
scan("response", res:get_body(), req.host, req.path) scan("response", res:get_body(), req.host, req.path)
end end