- Stats, keys, providers, scan, recon, dorks, config endpoints - JSON response wrappers with proper tags for all entities - Filtering, pagination, 404/204/202 status codes - SSE hub stub (full impl in task 2) - Resolved merge conflict in schema.sql - 16 passing tests covering all endpoints
330 lines
8.6 KiB
Go
330 lines
8.6 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/salvacybersec/keyhunter/pkg/dorks"
|
|
"github.com/salvacybersec/keyhunter/pkg/providers"
|
|
"github.com/salvacybersec/keyhunter/pkg/recon"
|
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testServer creates a Server with in-memory DB and minimal registries for testing.
|
|
func testServer(t *testing.T) (*Server, []byte) {
|
|
t.Helper()
|
|
db, err := storage.Open(":memory:")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
encKey := []byte("0123456789abcdef0123456789abcdef") // 32-byte test key
|
|
|
|
provReg := providers.NewRegistryFromProviders([]providers.Provider{
|
|
{Name: "openai", DisplayName: "OpenAI", Tier: 1, Keywords: []string{"sk-"}},
|
|
{Name: "anthropic", DisplayName: "Anthropic", Tier: 1, Keywords: []string{"sk-ant-"}},
|
|
})
|
|
|
|
dorkReg := dorks.NewRegistryFromDorks([]dorks.Dork{
|
|
{ID: "gh-openai-1", Name: "OpenAI GitHub", Source: "github", Category: "frontier", Query: "sk-proj- in:file", Description: "Find OpenAI keys on GitHub"},
|
|
})
|
|
|
|
reconEng := recon.NewEngine()
|
|
|
|
s := NewServer(ServerConfig{
|
|
DB: db,
|
|
EncKey: encKey,
|
|
Providers: provReg,
|
|
Dorks: dorkReg,
|
|
ReconEngine: reconEng,
|
|
})
|
|
|
|
return s, encKey
|
|
}
|
|
|
|
// seedFinding inserts a test finding and returns its ID.
|
|
func seedFinding(t *testing.T, db *storage.DB, encKey []byte, provider string) int64 {
|
|
t.Helper()
|
|
id, err := db.SaveFinding(storage.Finding{
|
|
ProviderName: provider,
|
|
KeyValue: "sk-test1234567890abcdefghijklmnop",
|
|
KeyMasked: "sk-test1...mnop",
|
|
Confidence: "high",
|
|
SourcePath: "/tmp/test.py",
|
|
SourceType: "file",
|
|
LineNumber: 42,
|
|
}, encKey)
|
|
require.NoError(t, err)
|
|
return id
|
|
}
|
|
|
|
func TestAPIStats(t *testing.T) {
|
|
s, encKey := testServer(t)
|
|
seedFinding(t, s.cfg.DB, encKey, "openai")
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
|
|
|
var body map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
|
assert.Contains(t, body, "totalKeys")
|
|
assert.Contains(t, body, "totalProviders")
|
|
assert.Contains(t, body, "reconSources")
|
|
}
|
|
|
|
func TestAPIListKeys(t *testing.T) {
|
|
s, encKey := testServer(t)
|
|
seedFinding(t, s.cfg.DB, encKey, "openai")
|
|
seedFinding(t, s.cfg.DB, encKey, "anthropic")
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/keys", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var keys []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &keys))
|
|
assert.Len(t, keys, 2)
|
|
|
|
// Keys should be masked (no raw key value exposed)
|
|
for _, k := range keys {
|
|
val, ok := k["keyValue"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "", val, "API must not expose raw key values")
|
|
}
|
|
}
|
|
|
|
func TestAPIListKeysFilterByProvider(t *testing.T) {
|
|
s, encKey := testServer(t)
|
|
seedFinding(t, s.cfg.DB, encKey, "openai")
|
|
seedFinding(t, s.cfg.DB, encKey, "anthropic")
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/keys?provider=openai", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var keys []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &keys))
|
|
assert.Len(t, keys, 1)
|
|
}
|
|
|
|
func TestAPIGetKey(t *testing.T) {
|
|
s, encKey := testServer(t)
|
|
id := seedFinding(t, s.cfg.DB, encKey, "openai")
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/keys/"+itoa(id), nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var body map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
|
assert.Equal(t, "openai", body["providerName"])
|
|
}
|
|
|
|
func TestAPIGetKeyNotFound(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/keys/99999", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestAPIDeleteKey(t *testing.T) {
|
|
s, encKey := testServer(t)
|
|
id := seedFinding(t, s.cfg.DB, encKey, "openai")
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/keys/"+itoa(id), nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNoContent, w.Code)
|
|
}
|
|
|
|
func TestAPIDeleteKeyNotFound(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/keys/99999", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestAPIListProviders(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/providers", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var provs []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &provs))
|
|
assert.Len(t, provs, 2)
|
|
}
|
|
|
|
func TestAPIGetProvider(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/providers/openai", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var body map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
|
assert.Equal(t, "openai", body["name"])
|
|
}
|
|
|
|
func TestAPIGetProviderNotFound(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/providers/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestAPIScan(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
body := `{"path":"/tmp/test","verify":false,"workers":2}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/scan", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code)
|
|
var resp map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, "started", resp["status"])
|
|
}
|
|
|
|
func TestAPIRecon(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
body := `{"query":"openai","sources":["github"],"stealth":false}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code)
|
|
var resp map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, "started", resp["status"])
|
|
}
|
|
|
|
func TestAPIListDorks(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dorks", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var d []map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &d))
|
|
assert.Len(t, d, 1)
|
|
}
|
|
|
|
func TestAPIAddDork(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
body := `{"dorkId":"custom-1","name":"Custom Dork","source":"github","category":"custom","query":"custom query","description":"test"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/dorks", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
var resp map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Contains(t, resp, "id")
|
|
}
|
|
|
|
func TestAPIGetConfig(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
func TestAPIUpdateConfig(t *testing.T) {
|
|
s, _ := testServer(t)
|
|
|
|
r := chi.NewRouter()
|
|
s.mountAPI(r)
|
|
|
|
body := `{"scan.workers":"8"}`
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/config", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|