/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
// genericDefaultPanelTypes are the sqlAdminPaths entries with no product-specific
// case in isAdminPanel, so they fall through to the default keyword branch.
var genericDefaultPanelTypes = []string{
"SQL Interface", "Database Interface", "Database Admin", "MySQL Admin",
"SQL Manager", "WebSQL", "SQLWeb", "MongoDB Interface", "Redis Interface",
}
// an ordinary javascript page is not a database admin panel. "query" used to
// match the default branch via jQuery/querySelector, flagging every js site.
func TestIsAdminPanel_GenericJSPageNotFlagged(t *testing.T) {
pages := []struct{ name, body string }{
{"jquery script tag", ``},
{"querySelector call", ``},
{"jquery invocation", ``},
{"search query word", "
"},
{"graphql query const", ``},
}
for _, p := range pages {
for _, pt := range genericDefaultPanelTypes {
if isAdminPanel(p.body, pt) {
t.Errorf("%s wrongly flagged as %q admin panel", p.name, pt)
}
}
}
}
// dropping "query" must not reduce recall: real db interfaces still match via
// the sibling keywords (database/sql/mysql/postgresql/mongodb).
func TestIsAdminPanel_RealGenericPanelsStillDetected(t *testing.T) {
cases := []struct{ name, body string }{
{"database manager", "Database Manager"},
{"sql console", "SQL Console
"},
{"mysql admin", "MySQL Administration"},
{"postgresql browser", "PostgreSQL database browser
"},
{"mongodb express", "mongodb express"},
{"sql query interface", "SQL Query Interface
"},
}
for _, c := range cases {
if !isAdminPanel(c.body, "Database Interface") {
t.Errorf("%s should still be detected as a database interface", c.name)
}
}
}
// the precise change: a lone "query" no longer triggers, but "query" alongside
// a db keyword still does, carried by the sibling.
func TestIsAdminPanel_QueryRemovalPrecise(t *testing.T) {
if isAdminPanel("Query Console", "Database Interface") {
t.Error(`lone "query" should no longer trigger the default branch`)
}
if !isAdminPanel("SQL Query Tool", "Database Interface") {
t.Error(`"query" with "sql" should still detect via "sql"`)
}
}
// the default-branch change must not disturb the product-specific cases.
func TestIsAdminPanel_ExplicitCasesUnaffected(t *testing.T) {
cases := []struct {
panelType string
body string
want bool
}{
{"phpMyAdmin", "phpMyAdmin", true},
{"phpMyAdmin", "", true},
{"phpMyAdmin", "Home", false},
{"Adminer", "Adminer", true},
{"Adminer", "nothing relevant", false},
{"pgAdmin", "pgAdmin 4", true},
{"phpPgAdmin", "phpPgAdmin
", true},
{"RockMongo", "RockMongo", true},
{"Redis Commander", "Redis Commander", true},
{"phpRedisAdmin", "phpRedisAdmin
", true},
{"phpMyAdmin", ``, false},
}
for _, c := range cases {
if got := isAdminPanel(c.body, c.panelType); got != c.want {
t.Errorf("isAdminPanel(%q, %q) = %v, want %v", c.body, c.panelType, got, c.want)
}
}
}
// end to end: a catch-all that serves a jquery page at every path (the common
// soft-404-as-200 case) must not yield any admin-panel finding.
func TestSQL_JQueryCatchAllNotReported(t *testing.T) {
jq := `
Welcome
`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(jq))
}))
defer srv.Close()
result, err := SQL(srv.URL, 5*time.Second, 4, "", false)
if err != nil {
t.Fatalf("SQL: %v", err)
}
// SQL returns a nil result when nothing is found, which is the pass case here.
if result != nil && len(result.AdminPanels) != 0 {
t.Errorf("jquery catch-all produced %d admin-panel finding(s): %+v",
len(result.AdminPanels), result.AdminPanels)
}
}
// end to end: a real phpMyAdmin install is still reported.
func TestSQL_RealPhpMyAdminReported(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/phpmyadmin/" {
_, _ = w.Write([]byte("phpMyAdmin"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
result, err := SQL(srv.URL, 5*time.Second, 4, "", false)
if err != nil {
t.Fatalf("SQL: %v", err)
}
if result == nil {
t.Fatal("expected a phpMyAdmin finding, got nil result")
}
found := false
for _, p := range result.AdminPanels {
if p.Type == "phpMyAdmin" {
found = true
}
}
if !found {
t.Errorf("real phpMyAdmin not reported; panels=%+v", result.AdminPanels)
}
}
// end to end: a genuine generic db interface (db-topical body at a db path) is
// still reported, so the change did not over-tighten the default branch.
func TestSQL_RealGenericPanelReported(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/db/" {
_, _ = w.Write([]byte("Database ManagerMySQL server status"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
result, err := SQL(srv.URL, 5*time.Second, 4, "", false)
if err != nil {
t.Fatalf("SQL: %v", err)
}
if result == nil || len(result.AdminPanels) == 0 {
t.Error("a real database interface at /db/ should still be reported")
}
}