mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-12 00:47:26 +02:00
init enumerate
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
296
back/internal/api/handler/enumerate.go
Normal file
296
back/internal/api/handler/enumerate.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/respond"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
// ansiRe strips all ANSI/VT100 escape sequences (CSI, OSC, etc.).
|
||||
// RunWithPTY only strips OSC sequences; CSI colour codes need this.
|
||||
var ansiRe = regexp.MustCompile(`\x1b[\x5b-\x5f][0-9;]*[A-Za-z]|\x1b[^[\x5b-\x5f]`)
|
||||
|
||||
type EnumerateHandler struct {
|
||||
demo bool
|
||||
}
|
||||
|
||||
func NewEnumerateHandler(_ string, demo bool) *EnumerateHandler {
|
||||
return &EnumerateHandler{demo: demo}
|
||||
}
|
||||
|
||||
// --- Generate ---
|
||||
|
||||
type generateRequest struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type emailEntry struct {
|
||||
Address string `json:"address"`
|
||||
Username string `json:"username"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type generateResponse struct {
|
||||
Usernames []string `json:"usernames"`
|
||||
Emails []emailEntry `json:"emails"`
|
||||
}
|
||||
|
||||
var emailDomains = []string{
|
||||
"gmail.com",
|
||||
"outlook.com",
|
||||
"hotmail.com",
|
||||
"yahoo.com",
|
||||
"protonmail.com",
|
||||
"proton.me",
|
||||
"icloud.com",
|
||||
"live.com",
|
||||
"msn.com",
|
||||
}
|
||||
|
||||
var accentMap = map[rune]rune{
|
||||
'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e',
|
||||
'à': 'a', 'â': 'a', 'ä': 'a', 'á': 'a',
|
||||
'ù': 'u', 'û': 'u', 'ü': 'u', 'ú': 'u',
|
||||
'ô': 'o', 'ö': 'o', 'ó': 'o',
|
||||
'î': 'i', 'ï': 'i', 'í': 'i',
|
||||
'ç': 'c', 'ñ': 'n', 'ß': 's',
|
||||
}
|
||||
|
||||
func normalizeName(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if mapped, ok := accentMap[r]; ok {
|
||||
b.WriteRune(mapped)
|
||||
} else if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
b.WriteRune(r)
|
||||
} else if r == ' ' || r == '_' || r == '.' {
|
||||
b.WriteRune('-')
|
||||
}
|
||||
}
|
||||
return strings.Trim(b.String(), "-")
|
||||
}
|
||||
|
||||
func buildUsernames(first, last string) []string {
|
||||
f := strings.ReplaceAll(normalizeName(first), "-", "")
|
||||
l := strings.ReplaceAll(normalizeName(last), "-", "")
|
||||
if f == "" || l == "" {
|
||||
return nil
|
||||
}
|
||||
fi := string([]rune(f)[0])
|
||||
li := string([]rune(l)[0])
|
||||
|
||||
patterns := []string{
|
||||
f + l,
|
||||
l + f,
|
||||
fi + l,
|
||||
f + "." + l,
|
||||
l + "." + f,
|
||||
fi + "." + l,
|
||||
f + "_" + l,
|
||||
l + "_" + f,
|
||||
fi + "_" + l,
|
||||
f + li,
|
||||
f + "." + li,
|
||||
fi + li,
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
var result []string
|
||||
for _, p := range patterns {
|
||||
// Skip patterns that are too short to be realistic usernames/emails.
|
||||
if len([]rune(p)) < 4 {
|
||||
continue
|
||||
}
|
||||
if p != "" && !seen[p] {
|
||||
seen[p] = true
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GET /api/enumerate/status
|
||||
type statusResponse struct {
|
||||
UserScannerAvailable bool `json:"user_scanner_available"`
|
||||
}
|
||||
|
||||
func (h *EnumerateHandler) Status(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := exec.LookPath("user-scanner")
|
||||
respond.JSON(w, http.StatusOK, statusResponse{UserScannerAvailable: err == nil})
|
||||
}
|
||||
|
||||
// POST /api/enumerate/generate
|
||||
func (h *EnumerateHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
var req generateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respond.Error(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
var usernames []string
|
||||
if req.Username != "" {
|
||||
u := normalizeName(req.Username)
|
||||
if u == "" {
|
||||
respond.Error(w, http.StatusBadRequest, "invalid username")
|
||||
return
|
||||
}
|
||||
usernames = []string{u}
|
||||
} else {
|
||||
if req.FirstName == "" || req.LastName == "" {
|
||||
respond.Error(w, http.StatusBadRequest, "provide either username or first_name+last_name")
|
||||
return
|
||||
}
|
||||
usernames = buildUsernames(req.FirstName, req.LastName)
|
||||
if len(usernames) == 0 {
|
||||
respond.Error(w, http.StatusBadRequest, "could not generate usernames from provided names")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var emails []emailEntry
|
||||
for _, u := range usernames {
|
||||
for _, d := range emailDomains {
|
||||
emails = append(emails, emailEntry{
|
||||
Address: u + "@" + d,
|
||||
Username: u,
|
||||
Domain: d,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, generateResponse{Usernames: usernames, Emails: emails})
|
||||
}
|
||||
|
||||
// --- Check Email ---
|
||||
|
||||
type checkEmailRequest struct {
|
||||
Email string `json:"email"`
|
||||
Quick bool `json:"quick"`
|
||||
}
|
||||
|
||||
type checkEmailResponse struct {
|
||||
Status string `json:"status"` // "found" | "not_found" | "maybe"
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// userScannerCheck runs user-scanner via PTY (required for output).
|
||||
// flag is either "-e" (email) or "-u" (username).
|
||||
// Office365 is excluded — it's a known false positive.
|
||||
// quick=true uses a shorter timeout for a faster but incomplete scan.
|
||||
func userScannerCheck(ctx context.Context, flag, target string, quick bool) (status, reason string, sites []string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
status, reason, sites = "maybe", "internal error during scan", nil
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := exec.LookPath("user-scanner"); err != nil {
|
||||
return "maybe", "user-scanner not available", nil
|
||||
}
|
||||
|
||||
scanTimeout := 90 * time.Second
|
||||
if quick {
|
||||
scanTimeout = 15 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, scanTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "user-scanner", flag, target, "--only-found")
|
||||
output, _ := tools.RunWithPTY(ctx, cmd)
|
||||
|
||||
// Strip ANSI codes, then normalise PTY line endings (\r\n → \n).
|
||||
output = ansiRe.ReplaceAllString(output, "")
|
||||
output = strings.ReplaceAll(output, "\r\n", "\n")
|
||||
output = strings.ReplaceAll(output, "\r", "\n")
|
||||
|
||||
// Strip banner (first 8 lines)
|
||||
if lines := strings.SplitN(output, "\n", 10); len(lines) > 8 {
|
||||
output = strings.Join(lines[8:], "\n")
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if !strings.Contains(line, "[✔]") {
|
||||
continue
|
||||
}
|
||||
// Office365 is a known false positive — skip it.
|
||||
if strings.Contains(line, "Office365") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
sites = append(sites, strings.TrimRight(parts[1], ":"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(sites) > 0 {
|
||||
return "found", fmt.Sprintf("found via user-scanner (%d result(s))", len(sites)), sites
|
||||
}
|
||||
return "not_found", "not found via user-scanner", nil
|
||||
}
|
||||
|
||||
// POST /api/enumerate/check-email
|
||||
func (h *EnumerateHandler) CheckEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if h.demo {
|
||||
respond.Error(w, http.StatusForbidden, "checking is disabled in demo mode")
|
||||
return
|
||||
}
|
||||
var req checkEmailRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respond.Error(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if req.Email == "" {
|
||||
respond.Error(w, http.StatusBadRequest, "email is required")
|
||||
return
|
||||
}
|
||||
|
||||
status, reason, _ := userScannerCheck(r.Context(), "-e", req.Email, req.Quick)
|
||||
respond.JSON(w, http.StatusOK, checkEmailResponse{Status: status, Reason: reason})
|
||||
}
|
||||
|
||||
// --- Check Username ---
|
||||
|
||||
type checkUsernameRequest struct {
|
||||
Username string `json:"username"`
|
||||
Quick bool `json:"quick"`
|
||||
}
|
||||
|
||||
type checkUsernameResponse struct {
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Sites []string `json:"sites"`
|
||||
}
|
||||
|
||||
// POST /api/enumerate/check-username
|
||||
func (h *EnumerateHandler) CheckUsername(w http.ResponseWriter, r *http.Request) {
|
||||
if h.demo {
|
||||
respond.Error(w, http.StatusForbidden, "checking is disabled in demo mode")
|
||||
return
|
||||
}
|
||||
var req checkUsernameRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respond.Error(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if req.Username == "" {
|
||||
respond.Error(w, http.StatusBadRequest, "username is required")
|
||||
return
|
||||
}
|
||||
|
||||
status, reason, sites := userScannerCheck(r.Context(), "-u", req.Username, req.Quick)
|
||||
if sites == nil {
|
||||
sites = []string{}
|
||||
}
|
||||
respond.JSON(w, http.StatusOK, checkUsernameResponse{Status: status, Reason: reason, Sites: sites})
|
||||
}
|
||||
@@ -29,6 +29,7 @@ func NewRouter(
|
||||
searchHandler := handler.NewSearchHandler(manager, demo)
|
||||
toolsHandler := handler.NewToolsHandler(factories)
|
||||
configHandler := handler.NewConfigHandler(configPath, factories, demo)
|
||||
enumerateHandler := handler.NewEnumerateHandler(configPath, demo)
|
||||
|
||||
searchLimiter := ikymiddleware.New(rate.Every(10*time.Second), 3)
|
||||
|
||||
@@ -45,6 +46,13 @@ func NewRouter(
|
||||
r.Get("/{name}", toolsHandler.Get)
|
||||
})
|
||||
|
||||
r.Route("/enumerate", func(r chi.Router) {
|
||||
r.Get("/status", enumerateHandler.Status)
|
||||
r.Post("/generate", enumerateHandler.Generate)
|
||||
r.Post("/check-email", enumerateHandler.CheckEmail)
|
||||
r.Post("/check-username", enumerateHandler.CheckUsername)
|
||||
})
|
||||
|
||||
r.Route("/config", func(r chi.Router) {
|
||||
r.Get("/", configHandler.Get)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user