Files
sif/internal/modules/management_api_exposure_test.go
TigahandGitHub 3ed9ea4b6f feat(modules): add kong, jolokia and nats api exposure modules (#215)
modules/recon/kong-api-exposure.yaml flags an exposed Kong admin api that grants
full control of the gateway, keyed on the available plugins map paired with the
admin listen address that the node reports, then extracts the kong version.

modules/recon/jolokia-api-exposure.yaml flags an exposed Jolokia agent that
bridges http to jmx, keyed on the agent and protocol fields of its version
response, then extracts the agent version.

modules/recon/nats-api-exposure.yaml flags an exposed NATS monitoring endpoint
that leaks the server topology, keyed on the server id paired with the max
payload, then extracts the server version.

internal/modules/management_api_exposure_test.go drives the three modules end to
end through ExecuteHTTPModule and asserts the leak alongside the near misses a
strict review wants pinned: each service with one keying field missing, a generic
version response, a plain 200 and a 404.

verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
2026-06-22 16:49:45 -07:00

156 lines
5.5 KiB
Go

package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runMgmtModule(t *testing.T, file string, status int, body string) *modules.Result {
t.Helper()
def, err := modules.ParseYAMLModule(file)
if err != nil {
t.Fatalf("parse %s: %v", file, err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}))
defer srv.Close()
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
Timeout: 5 * time.Second,
Threads: 2,
})
if err != nil {
t.Fatalf("execute %s: %v", file, err)
}
return res
}
func mgmtExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestManagementAPIExposureModules(t *testing.T) {
const kong = "../../modules/recon/kong-api-exposure.yaml"
const jolokia = "../../modules/recon/jolokia-api-exposure.yaml"
const nats = "../../modules/recon/nats-api-exposure.yaml"
kongRoot := `{"version":"3.4.0","tagline":"Welcome to kong","hostname":"kong-node","node_id":"abc",` +
`"lua_version":"LuaJIT 2.1.0","plugins":{"available_on_server":{}},` +
`"configuration":{"database":"postgres","admin_listen":["0.0.0.0:8001"]}}`
jolokiaVersion := `{"request":{"type":"version"},"value":{"agent":"1.7.2","protocol":"7.2",` +
`"config":{"agentType":"servlet"},"info":{"product":"tomcat"}},"status":200,"timestamp":1694598949}`
natsVarz := `{"server_id":"NDABC","server_name":"NDABC","version":"2.10.1","proto":1,"go":"go1.21.1",` +
`"host":"0.0.0.0","port":4222,"max_connections":65536,"max_payload":1048576,"connections":3,"total_connections":10}`
t.Run("an exposed kong admin api is flagged and versioned", func(t *testing.T) {
res := runMgmtModule(t, kong, 200, kongRoot)
if len(res.Findings) == 0 {
t.Fatal("expected a kong finding")
}
if v := mgmtExtract(res, "kong_version"); v != "3.4.0" {
t.Errorf("kong_version=%q, want 3.4.0", v)
}
})
t.Run("an exposed jolokia agent is flagged and versioned", func(t *testing.T) {
res := runMgmtModule(t, jolokia, 200, jolokiaVersion)
if len(res.Findings) == 0 {
t.Fatal("expected a jolokia finding")
}
if v := mgmtExtract(res, "jolokia_agent_version"); v != "1.7.2" {
t.Errorf("jolokia_agent_version=%q, want 1.7.2", v)
}
})
t.Run("an exposed nats monitor is flagged and versioned", func(t *testing.T) {
res := runMgmtModule(t, nats, 200, natsVarz)
if len(res.Findings) == 0 {
t.Fatal("expected a nats finding")
}
if v := mgmtExtract(res, "nats_version"); v != "2.10.1" {
t.Errorf("nats_version=%q, want 2.10.1", v)
}
})
t.Run("an available plugins map without an admin listen is not flagged", func(t *testing.T) {
body := `{"plugins":{"available_on_server":{}},"version":"3.4.0"}`
if res := runMgmtModule(t, kong, 200, body); len(res.Findings) > 0 {
t.Errorf("an available plugins map alone should not match kong, got %d findings", len(res.Findings))
}
})
t.Run("an admin listen without an available plugins map is not flagged", func(t *testing.T) {
body := `{"configuration":{"admin_listen":["0.0.0.0:8001"]},"version":"1.0"}`
if res := runMgmtModule(t, kong, 200, body); len(res.Findings) > 0 {
t.Errorf("an admin listen alone should not match kong, got %d findings", len(res.Findings))
}
})
t.Run("a jolokia agent without a protocol is not flagged", func(t *testing.T) {
body := `{"value":{"agent":"1.7.2"}}`
if res := runMgmtModule(t, jolokia, 200, body); len(res.Findings) > 0 {
t.Errorf("an agent alone should not match jolokia, got %d findings", len(res.Findings))
}
})
t.Run("a jolokia protocol without an agent is not flagged", func(t *testing.T) {
body := `{"value":{"protocol":"7.2"},"info":{}}`
if res := runMgmtModule(t, jolokia, 200, body); len(res.Findings) > 0 {
t.Errorf("a protocol alone should not match jolokia, got %d findings", len(res.Findings))
}
})
t.Run("a nats server id without a max payload is not flagged", func(t *testing.T) {
body := `{"server_id":"NDABC","version":"2.10.1"}`
if res := runMgmtModule(t, nats, 200, body); len(res.Findings) > 0 {
t.Errorf("a server id alone should not match nats, got %d findings", len(res.Findings))
}
})
t.Run("a max payload without a nats server id is not flagged", func(t *testing.T) {
body := `{"max_payload":1048576,"port":4222}`
if res := runMgmtModule(t, nats, 200, body); len(res.Findings) > 0 {
t.Errorf("a max payload alone should not match nats, got %d findings", len(res.Findings))
}
})
t.Run("a generic version json is not a management api", func(t *testing.T) {
body := `{"version":"1.0.0","name":"app"}`
for _, file := range []string{kong, jolokia, nats} {
if res := runMgmtModule(t, file, 200, body); len(res.Findings) > 0 {
t.Errorf("%s: a generic version should not match, got %d findings", file, len(res.Findings))
}
}
})
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
for _, file := range []string{kong, jolokia, nats} {
if res := runMgmtModule(t, file, 200, "ok"); len(res.Findings) > 0 {
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
}
}
})
t.Run("a 404 is not a leak", func(t *testing.T) {
for _, file := range []string{kong, jolokia, nats} {
if res := runMgmtModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}