This commit is contained in:
Hadi
2026-04-06 15:12:34 +02:00
commit 4989225671
117 changed files with 11454 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
package search
import (
"context"
"time"
"github.com/google/uuid"
"github.com/anotherhadi/iknowyou/internal/tools"
)
func ptr(n int) *int { return &n }
func (m *Manager) InjectDemoSearches() {
now := time.Now()
_, cancel1 := context.WithCancel(context.Background())
s1 := &Search{
ID: uuid.NewString(),
Target: "john.doe@example.com",
InputType: tools.InputTypeEmail,
Profile: "default",
StartedAt: now.Add(-2 * time.Hour),
PlannedTools: []ToolStatus{
{Name: "user-scanner", ResultCount: ptr(10)},
{Name: "github-recon", ResultCount: ptr(3)},
},
cancelFn: cancel1,
status: StatusDone,
finishedAt: now.Add(-2*time.Hour + 18*time.Second),
}
s1.events = []tools.Event{
{Tool: "user-scanner", Type: tools.EventTypeOutput, Payload: "\x1b[35m== ADULT SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Xvideos (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Pornhub (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== CREATOR SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Adobe (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== MUSIC SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Spotify (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== LEARNING SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Duolingo (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Vedantu (john.doe@example.com): Registered\n \x1b[36m└── Phone: +9112****07\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== SOCIAL SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Pinterest (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Facebook (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== GAMING SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Chess.com (john.doe@example.com): Registered\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== SHOPPING SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Amazon (john.doe@example.com): Registered\x1b[0m\n"},
{Tool: "user-scanner", Type: tools.EventTypeDone},
{Tool: "github-recon", Type: tools.EventTypeOutput, Payload: "\x1b[1;38;2;113;135;253m👤 Commits author\x1b[0m\n\n" +
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"fastHack2025\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"Unknown\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m36\x1b[0m\n\n" +
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"Anthony\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"Unknown\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m52\x1b[0m\n\n" +
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"Gill\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"johndoe\"\x1b[0m\n" +
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m60\x1b[0m"},
{Tool: "github-recon", Type: tools.EventTypeDone},
}
_, cancel2 := context.WithCancel(context.Background())
s2 := &Search{
ID: uuid.NewString(),
Target: "janedoe",
InputType: tools.InputTypeUsername,
Profile: "default",
StartedAt: now.Add(-30 * time.Minute),
PlannedTools: []ToolStatus{
{Name: "user-scanner", ResultCount: ptr(10)},
{Name: "github-recon", ResultCount: ptr(0)},
},
cancelFn: cancel2,
status: StatusDone,
finishedAt: now.Add(-30*time.Minute + 22*time.Second),
}
s2.events = []tools.Event{
{Tool: "user-scanner", Type: tools.EventTypeOutput, Payload: "\x1b[35m== SOCIAL SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Reddit (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Threads (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] X (twitter) (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Youtube (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Telegram (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Tiktok (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Instagram (janedoe): Found\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== GAMING SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Chess.com (janedoe): Found\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Roblox (janedoe): Found\x1b[0m\n" +
"\x1b[0m\n" +
"\x1b[35m== EMAIL SITES ==\x1b[0m\n" +
"\x1b[0m \x1b[32m[✔] Protonmail (janedoe): Found\x1b[0m"},
{Tool: "user-scanner", Type: tools.EventTypeDone},
{Tool: "github-recon", Type: tools.EventTypeOutput, Payload: "\x1b[1;38;2;113;135;253m👤 User informations\x1b[0m\n\n" +
" \x1b[38;2;125;125;125mNo data found\x1b[0m"},
{Tool: "github-recon", Type: tools.EventTypeDone},
}
m.mu.Lock()
m.searches[s1.ID] = s1
m.searches[s2.ID] = s2
m.mu.Unlock()
}

View File

@@ -0,0 +1,274 @@
package search
import (
"context"
"fmt"
"reflect"
"sync"
"time"
"github.com/google/uuid"
"github.com/anotherhadi/iknowyou/config"
"github.com/anotherhadi/iknowyou/internal/tools"
)
type Manager struct {
mu sync.RWMutex
searches map[string]*Search
configPath string
factories []func() tools.ToolRunner
searchTTL time.Duration
cleanupInterval time.Duration
done chan struct{} // closed by Stop()
}
func NewManager(configPath string, factories []func() tools.ToolRunner, searchTTL, cleanupInterval time.Duration) *Manager {
m := &Manager{
searches: make(map[string]*Search),
configPath: configPath,
factories: factories,
searchTTL: searchTTL,
cleanupInterval: cleanupInterval,
done: make(chan struct{}),
}
go m.cleanupLoop()
return m
}
func (m *Manager) Stop() {
close(m.done)
}
func (m *Manager) Start(
parentCtx context.Context,
target string,
inputType tools.InputType,
profileName string,
) (*Search, error) {
// "default" is the canonical UI name for the no-filter profile.
if profileName == "default" {
profileName = ""
}
cfg, err := config.Load(m.configPath)
if err != nil {
return nil, fmt.Errorf("manager: loading config: %w", err)
}
activeTools, statuses, err := m.instantiate(cfg, inputType, profileName)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(parentCtx)
s := &Search{
ID: uuid.NewString(),
Target: target,
InputType: inputType,
Profile: profileName,
StartedAt: time.Now(),
PlannedTools: statuses,
cancelFn: cancel,
status: StatusRunning,
}
m.mu.Lock()
m.searches[s.ID] = s
m.mu.Unlock()
go m.runAll(ctx, s, activeTools)
return s, nil
}
func (m *Manager) Get(id string) (*Search, error) {
return m.get(id)
}
func (m *Manager) All() []*Search {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]*Search, 0, len(m.searches))
for _, s := range m.searches {
out = append(out, s)
}
return out
}
func (m *Manager) Delete(id string) error {
s, err := m.get(id)
if err != nil {
return err
}
s.Cancel()
m.mu.Lock()
delete(m.searches, id)
m.mu.Unlock()
return nil
}
func (m *Manager) cleanupLoop() {
ticker := time.NewTicker(m.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.purgeExpired()
case <-m.done:
return
}
}
}
func (m *Manager) purgeExpired() {
now := time.Now()
m.mu.Lock()
defer m.mu.Unlock()
for id, s := range m.searches {
ft := s.FinishedAt()
if ft.IsZero() {
continue // still running
}
if now.Sub(ft) > m.searchTTL {
delete(m.searches, id)
}
}
}
func (m *Manager) instantiate(cfg *config.Config, inputType tools.InputType, profileName string) ([]tools.ToolRunner, []ToolStatus, error) {
allNames := make([]string, len(m.factories))
allInstances := make([]tools.ToolRunner, len(m.factories))
for i, factory := range m.factories {
t := factory()
allNames[i] = t.Name()
allInstances[i] = t
}
activeNames, err := cfg.ActiveTools(profileName, allNames)
if err != nil {
return nil, nil, err
}
activeSet := make(map[string]struct{}, len(activeNames))
for _, n := range activeNames {
activeSet[n] = struct{}{}
}
var runners []tools.ToolRunner
var statuses []ToolStatus
for _, tool := range allInstances {
if _, ok := activeSet[tool.Name()]; !ok {
continue
}
if !acceptsInputType(tool, inputType) {
continue
}
if a, ok := tool.(tools.AvailabilityChecker); ok {
if available, reason := a.Available(); !available {
statuses = append(statuses, ToolStatus{
Name: tool.Name(),
Skipped: true,
Reason: reason,
})
continue
}
}
if c, ok := tool.(tools.Configurable); ok {
if err := cfg.DecodeEffective(tool.Name(), profileName, c.ConfigPtr()); err != nil {
return nil, nil, fmt.Errorf("manager: configuring tool %q: %w", tool.Name(), err)
}
if d, ok := tool.(tools.ConfigDescriber); ok {
if missing, fieldName := missingRequiredField(d.ConfigFields()); missing {
statuses = append(statuses, ToolStatus{
Name: tool.Name(),
Skipped: true,
Reason: fmt.Sprintf("missing required config field: %s", fieldName),
})
continue
}
}
}
statuses = append(statuses, ToolStatus{Name: tool.Name()})
runners = append(runners, tool)
}
return runners, statuses, nil
}
func (m *Manager) runAll(ctx context.Context, s *Search, runners []tools.ToolRunner) {
var wg sync.WaitGroup
for _, tool := range runners {
wg.Add(1)
go func(t tools.ToolRunner) {
defer wg.Done()
m.runOne(ctx, s, t)
}(tool)
}
wg.Wait()
s.markDone()
}
func (m *Manager) runOne(ctx context.Context, s *Search, tool tools.ToolRunner) {
out := make(chan tools.Event)
go func() {
_ = tool.Run(ctx, s.Target, s.InputType, out)
}()
var count int
var hasCount bool
for e := range out {
if e.Type == tools.EventTypeCount {
if n, ok := e.Payload.(int); ok {
count += n
hasCount = true
}
continue
}
s.append(e)
}
if hasCount {
s.setToolResultCount(tool.Name(), count)
}
}
func (m *Manager) get(id string) (*Search, error) {
m.mu.RLock()
defer m.mu.RUnlock()
s, ok := m.searches[id]
if !ok {
return nil, fmt.Errorf("search %q not found", id)
}
return s, nil
}
func acceptsInputType(tool tools.ToolRunner, inputType tools.InputType) bool {
for _, t := range tool.InputTypes() {
if t == inputType {
return true
}
}
return false
}
func missingRequiredField(fields []tools.ConfigField) (missing bool, fieldName string) {
for _, f := range fields {
if !f.Required {
continue
}
if f.Value == nil || reflect.DeepEqual(f.Value, reflect.Zero(reflect.TypeOf(f.Value)).Interface()) {
return true, f.Name
}
}
return false, ""
}

View File

@@ -0,0 +1,97 @@
package search
import (
"context"
"sync"
"time"
"github.com/anotherhadi/iknowyou/internal/tools"
)
type Status string
const (
StatusRunning Status = "running"
StatusDone Status = "done"
StatusCancelled Status = "cancelled"
)
type ToolStatus struct {
Name string `json:"name"`
Skipped bool `json:"skipped"`
Reason string `json:"reason,omitempty"` // non-empty only when Skipped is true
ResultCount *int `json:"result_count,omitempty"` // nil = pending, 0 = no results
}
type Search struct {
ID string
Target string
InputType tools.InputType
Profile string
StartedAt time.Time
PlannedTools []ToolStatus
cancelFn context.CancelFunc
mu sync.RWMutex
events []tools.Event
status Status
finishedAt time.Time
}
func (s *Search) Events() []tools.Event {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]tools.Event, len(s.events))
copy(out, s.events)
return out
}
func (s *Search) Status() Status {
s.mu.RLock()
defer s.mu.RUnlock()
return s.status
}
func (s *Search) FinishedAt() time.Time {
s.mu.RLock()
defer s.mu.RUnlock()
return s.finishedAt
}
func (s *Search) Cancel() {
s.mu.Lock()
if s.status == StatusRunning {
s.status = StatusCancelled
s.finishedAt = time.Now()
}
s.mu.Unlock()
s.cancelFn()
}
func (s *Search) setToolResultCount(toolName string, count int) {
s.mu.Lock()
defer s.mu.Unlock()
for i, t := range s.PlannedTools {
if t.Name == toolName {
s.PlannedTools[i].ResultCount = &count
return
}
}
}
func (s *Search) append(e tools.Event) {
s.mu.Lock()
defer s.mu.Unlock()
s.events = append(s.events, e)
}
func (s *Search) markDone() {
s.mu.Lock()
defer s.mu.Unlock()
if s.status == StatusRunning {
s.status = StatusDone
s.finishedAt = time.Now()
}
}