---
phase: 08-dork-engine
plan: 05
type: execute
wave: 2
depends_on: [08-01]
files_modified:
- pkg/dorks/github.go
- pkg/dorks/github_test.go
autonomous: true
requirements:
- DORK-02
must_haves:
truths:
- "GitHubExecutor.Source() returns \"github\""
- "GitHubExecutor.Execute runs GitHub Code Search against api.github.com and returns []Match"
- "Missing token returns ErrMissingAuth with setup instructions"
- "Retry-After header is honored (sleep + retry once) for 403/429"
- "Response items mapped to Match with URL, Path, Snippet (text_matches)"
artifacts:
- path: "pkg/dorks/github.go"
provides: "GitHubExecutor implementing Executor interface"
contains: "type GitHubExecutor struct"
- path: "pkg/dorks/github_test.go"
provides: "httptest server exercising success/auth/rate-limit paths"
contains: "httptest.NewServer"
key_links:
- from: "pkg/dorks/github.go"
to: "https://api.github.com/search/code"
via: "net/http client"
pattern: "api.github.com/search/code"
- from: "pkg/dorks/github.go"
to: "pkg/dorks/executor.go Executor interface"
via: "interface satisfaction"
pattern: "Execute\\(ctx"
---
Implement the live GitHub Code Search executor — the only source that actually
runs in Phase 8 (all other executors stay stubbed with ErrSourceNotImplemented).
Hits `GET https://api.github.com/search/code?q={query}`, authenticated via
GITHUB_TOKEN env var / viper config. Honors rate-limit response codes. Maps
response items to pkg/dorks.Match entries consumable by the engine pipeline in
downstream phases.
Purpose: Satisfies the "GitHub live" slice of DORK-02 and unblocks `keyhunter
dorks run --source=github` in Plan 08-06.
Output: Working pkg/dorks.GitHubExecutor + httptest-backed test suite.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/phases/08-dork-engine/08-CONTEXT.md
@.planning/phases/08-dork-engine/08-01-PLAN.md
@pkg/dorks/executor.go
```go
type Executor interface {
Source() string
Execute(ctx context.Context, d Dork, limit int) ([]Match, error)
}
type Match struct {
DorkID string
Source string
URL string
Snippet string
Path string
}
var ErrMissingAuth = errors.New("dork source requires auth credentials")
```
Task 1: GitHubExecutor with net/http + Retry-After handling
pkg/dorks/github.go, pkg/dorks/github_test.go
- Test: Execute with empty token returns ErrMissingAuth (wrapped) without hitting HTTP
- Test: Execute with httptest server returning 200 + items parses response into []Match with URL/Path/Snippet
- Test: limit=5 caps returned Match count at 5 even if API returns 10
- Test: 403 with X-RateLimit-Remaining=0 and Retry-After=1 sleeps and retries once, then succeeds
- Test: 401 returns ErrMissingAuth (token rejected)
- Test: 422 (invalid query) returns a descriptive error containing the status code
- Test: Source() returns "github"
Create pkg/dorks/github.go:
```go
package dorks
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
type GitHubExecutor struct {
Token string
BaseURL string // default "https://api.github.com", overridable for tests
HTTPClient *http.Client
MaxRetries int // default 1
}
func NewGitHubExecutor(token string) *GitHubExecutor {
return &GitHubExecutor{
Token: token,
BaseURL: "https://api.github.com",
HTTPClient: &http.Client{Timeout: 30 * time.Second},
MaxRetries: 1,
}
}
func (g *GitHubExecutor) Source() string { return "github" }
type ghSearchResponse struct {
TotalCount int `json:"total_count"`
Items []struct {
Name string `json:"name"`
Path string `json:"path"`
HTMLURL string `json:"html_url"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
TextMatches []struct {
Fragment string `json:"fragment"`
} `json:"text_matches"`
} `json:"items"`
}
func (g *GitHubExecutor) Execute(ctx context.Context, d Dork, limit int) ([]Match, error) {
if g.Token == "" {
return nil, fmt.Errorf("%w: set GITHUB_TOKEN env var or `keyhunter config set dorks.github.token ` (needs public_repo scope)", ErrMissingAuth)
}
if limit <= 0 || limit > 100 {
limit = 30
}
url := fmt.Sprintf("%s/search/code?q=%s&per_page=%d", g.BaseURL, urlQueryEscape(d.Query), limit)
var resp *http.Response
for attempt := 0; attempt <= g.MaxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { return nil, err }
req.Header.Set("Accept", "application/vnd.github.v3.text-match+json")
req.Header.Set("Authorization", "Bearer "+g.Token)
req.Header.Set("User-Agent", "keyhunter-dork-engine")
r, err := g.HTTPClient.Do(req)
if err != nil { return nil, fmt.Errorf("github search: %w", err) }
if r.StatusCode == http.StatusOK {
resp = r
break
}
body, _ := io.ReadAll(r.Body)
r.Body.Close()
switch r.StatusCode {
case http.StatusUnauthorized:
return nil, fmt.Errorf("%w: github token rejected (401)", ErrMissingAuth)
case http.StatusForbidden, http.StatusTooManyRequests:
if attempt < g.MaxRetries {
sleep := parseRetryAfter(r.Header.Get("Retry-After"))
select {
case <-time.After(sleep):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, fmt.Errorf("github rate limit: %d %s", r.StatusCode, string(body))
default:
return nil, fmt.Errorf("github search failed: %d %s", r.StatusCode, string(body))
}
}
defer resp.Body.Close()
var parsed ghSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, fmt.Errorf("decoding github response: %w", err)
}
out := make([]Match, 0, len(parsed.Items))
for _, it := range parsed.Items {
snippet := ""
if len(it.TextMatches) > 0 {
snippet = it.TextMatches[0].Fragment
}
out = append(out, Match{
DorkID: d.ID,
Source: "github",
URL: it.HTMLURL,
Path: it.Repository.FullName + "/" + it.Path,
Snippet: snippet,
})
if len(out) >= limit { break }
}
return out, nil
}
func parseRetryAfter(v string) time.Duration {
if v == "" { return time.Second }
if secs, err := strconv.Atoi(v); err == nil {
return time.Duration(secs) * time.Second
}
return time.Second
}
func urlQueryEscape(s string) string {
return (&url.URL{Path: s}).EscapedPath() // wrong — use url.QueryEscape
}
```
Fix the helper: import "net/url" and use `url.QueryEscape(s)` — do NOT hand-roll.
Create pkg/dorks/github_test.go using httptest.NewServer. Override
executor.BaseURL to the test server URL. One subtest per behavior case.
For Retry-After test: server returns 403 with Retry-After: 1 on first
request, 200 with fake items on second.
Do NOT register GitHubExecutor into a global Runner here — Plan 08-06 does
the wiring inside cmd/dorks.go via NewGitHubExecutor(viper.GetString(...)).
cd /home/salva/Documents/apikey && go test ./pkg/dorks/... -run GitHub -v
All GitHub executor test cases pass; Execute honors token, rate limit, and
limit cap; Match fields populated from real response shape.
`go test ./pkg/dorks/...` passes including all new GitHub cases.
- pkg/dorks.GitHubExecutor implements Executor interface
- Live GitHub Code Search calls are testable via httptest (BaseURL override)
- ErrMissingAuth surfaces with actionable setup instructions
- Retry-After respected once before giving up