mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-12 00:47:26 +02:00
init
This commit is contained in:
114
back/internal/search/demo.go
Normal file
114
back/internal/search/demo.go
Normal 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()
|
||||
}
|
||||
274
back/internal/search/manager.go
Normal file
274
back/internal/search/manager.go
Normal 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, ""
|
||||
}
|
||||
97
back/internal/search/search.go
Normal file
97
back/internal/search/search.go
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user