feat(08-03): add 20 Shodan dorks for exposed LLM infrastructure
- frontier.yaml: 6 dorks (OpenAI/Anthropic proxies, Azure OpenAI certs, AWS Bedrock, LiteLLM) - infrastructure.yaml: 14 dorks (Ollama, vLLM, LocalAI, LM Studio, text-generation-webui, Open WebUI, Triton, TGI, LangServe, FastChat, OpenRouter/Portkey/Helicone gateways) - Real Shodan query syntax: http.title, http.html, ssl.cert.subject.cn, product, port, http.component - Dual-located: pkg/dorks/definitions/shodan/ + dorks/shodan/
This commit is contained in:
270
pkg/dorks/github_test.go
Normal file
270
pkg/dorks/github_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package dorks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sampleDork() Dork {
|
||||
return Dork{
|
||||
ID: "openai-github-envfile",
|
||||
Name: "OpenAI key in .env",
|
||||
Source: "github",
|
||||
Query: "sk-proj- extension:env",
|
||||
}
|
||||
}
|
||||
|
||||
func newTestExecutor(token, baseURL string) *GitHubExecutor {
|
||||
return &GitHubExecutor{
|
||||
Token: token,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
MaxRetries: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_Source(t *testing.T) {
|
||||
g := NewGitHubExecutor("x")
|
||||
if g.Source() != "github" {
|
||||
t.Fatalf("expected source %q, got %q", "github", g.Source())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_MissingTokenReturnsErrMissingAuth(t *testing.T) {
|
||||
// Use a server that would fail the test if it were ever hit — the
|
||||
// executor must short-circuit before issuing an HTTP request.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("server should not be hit when token is empty; got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("", srv.URL)
|
||||
_, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty token")
|
||||
}
|
||||
if !errors.Is(err, ErrMissingAuth) {
|
||||
t.Fatalf("expected ErrMissingAuth, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "GITHUB_TOKEN") {
|
||||
t.Fatalf("expected setup instructions in error, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_SuccessfulSearchParsesMatches(t *testing.T) {
|
||||
body := `{
|
||||
"total_count": 2,
|
||||
"items": [
|
||||
{
|
||||
"name": ".env",
|
||||
"path": "backend/.env",
|
||||
"html_url": "https://github.com/acme/leaky/blob/main/backend/.env",
|
||||
"repository": {"full_name": "acme/leaky"},
|
||||
"text_matches": [{"fragment": "OPENAI_API_KEY=sk-proj-AAA"}]
|
||||
},
|
||||
{
|
||||
"name": "config.env",
|
||||
"path": "infra/config.env",
|
||||
"html_url": "https://github.com/acme/other/blob/main/infra/config.env",
|
||||
"repository": {"full_name": "acme/other"},
|
||||
"text_matches": [{"fragment": "SECRET=sk-proj-BBB"}]
|
||||
}
|
||||
]
|
||||
}`
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer test-token" {
|
||||
t.Errorf("expected Bearer auth, got %q", got)
|
||||
}
|
||||
if got := r.Header.Get("Accept"); !strings.Contains(got, "text-match") {
|
||||
t.Errorf("expected text-match Accept header, got %q", got)
|
||||
}
|
||||
if r.URL.Path != "/search/code" {
|
||||
t.Errorf("expected /search/code, got %s", r.URL.Path)
|
||||
}
|
||||
if q := r.URL.Query().Get("q"); q != "sk-proj- extension:env" {
|
||||
t.Errorf("expected query to be url-decoded to original, got %q", q)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("test-token", srv.URL)
|
||||
matches, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("expected 2 matches, got %d", len(matches))
|
||||
}
|
||||
m := matches[0]
|
||||
if m.DorkID != "openai-github-envfile" {
|
||||
t.Errorf("DorkID = %q", m.DorkID)
|
||||
}
|
||||
if m.Source != "github" {
|
||||
t.Errorf("Source = %q", m.Source)
|
||||
}
|
||||
if m.URL != "https://github.com/acme/leaky/blob/main/backend/.env" {
|
||||
t.Errorf("URL = %q", m.URL)
|
||||
}
|
||||
if m.Path != "acme/leaky/backend/.env" {
|
||||
t.Errorf("Path = %q", m.Path)
|
||||
}
|
||||
if m.Snippet != "OPENAI_API_KEY=sk-proj-AAA" {
|
||||
t.Errorf("Snippet = %q", m.Snippet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_LimitCapsResults(t *testing.T) {
|
||||
// Build a response with 10 items; executor must return only 5.
|
||||
var items []string
|
||||
for i := 0; i < 10; i++ {
|
||||
items = append(items, fmt.Sprintf(`{
|
||||
"name": "f%d.env",
|
||||
"path": "dir/f%d.env",
|
||||
"html_url": "https://github.com/acme/repo/blob/main/dir/f%d.env",
|
||||
"repository": {"full_name": "acme/repo"},
|
||||
"text_matches": [{"fragment": "sk-proj-%d"}]
|
||||
}`, i, i, i, i))
|
||||
}
|
||||
body := fmt.Sprintf(`{"total_count": 10, "items": [%s]}`, strings.Join(items, ","))
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("per_page"); got != "5" {
|
||||
t.Errorf("expected per_page=5, got %q", got)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("test-token", srv.URL)
|
||||
matches, err := g.Execute(context.Background(), sampleDork(), 5)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(matches) != 5 {
|
||||
t.Fatalf("expected 5 matches after cap, got %d", len(matches))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_RetryAfterSleepsAndRetries(t *testing.T) {
|
||||
var hits int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n := atomic.AddInt32(&hits, 1)
|
||||
if n == 1 {
|
||||
w.Header().Set("Retry-After", "1")
|
||||
w.Header().Set("X-RateLimit-Remaining", "0")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`{"message":"rate limit"}`))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"total_count":1,"items":[{
|
||||
"name":"a.env","path":"a.env","html_url":"https://github.com/x/y/blob/main/a.env",
|
||||
"repository":{"full_name":"x/y"},
|
||||
"text_matches":[{"fragment":"sk-proj-ZZ"}]}]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("test-token", srv.URL)
|
||||
start := time.Now()
|
||||
matches, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
t.Fatalf("expected 1 match after retry, got %d", len(matches))
|
||||
}
|
||||
if atomic.LoadInt32(&hits) != 2 {
|
||||
t.Fatalf("expected 2 server hits, got %d", hits)
|
||||
}
|
||||
if elapsed < 900*time.Millisecond {
|
||||
t.Fatalf("expected to sleep ~1s from Retry-After, only waited %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_RateLimitExhaustedReturnsError(t *testing.T) {
|
||||
var hits int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&hits, 1)
|
||||
w.Header().Set("Retry-After", "0")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
_, _ = w.Write([]byte(`{"message":"secondary rate limit"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("test-token", srv.URL)
|
||||
g.MaxRetries = 1
|
||||
_, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected rate limit error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "rate limit") {
|
||||
t.Fatalf("expected rate limit in error, got %q", err.Error())
|
||||
}
|
||||
if atomic.LoadInt32(&hits) != 2 {
|
||||
t.Fatalf("expected 2 hits (initial + 1 retry), got %d", hits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_UnauthorizedMapsToMissingAuth(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"message":"Bad credentials"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("bad-token", srv.URL)
|
||||
_, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !errors.Is(err, ErrMissingAuth) {
|
||||
t.Fatalf("expected ErrMissingAuth wrap, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubExecutor_UnprocessableEntityReturnsDescriptiveError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
_, _ = w.Write([]byte(`{"message":"Validation Failed"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := newTestExecutor("test-token", srv.URL)
|
||||
_, err := g.Execute(context.Background(), sampleDork(), 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "422") {
|
||||
t.Fatalf("expected status code in error, got %q", err.Error())
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Validation Failed") {
|
||||
t.Fatalf("expected server message in error, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetryAfter(t *testing.T) {
|
||||
cases := map[string]time.Duration{
|
||||
"": time.Second,
|
||||
"0": time.Second,
|
||||
"1": time.Second,
|
||||
"5": 5 * time.Second,
|
||||
"huh?": time.Second,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := parseRetryAfter(in); got != want {
|
||||
t.Errorf("parseRetryAfter(%q) = %s, want %s", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user