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,162 @@
package breachdirectory
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "breachdirectory"
description = "Data breach search via BreachDirectory — checks if an email, username, or phone appears in known data breaches and returns exposed passwords/hashes."
link = "https://breachdirectory.org"
icon = ""
)
type Config struct {
APIKey string `yaml:"api_key" iky:"desc=RapidAPI key for BreachDirectory (required — get one at rapidapi.com/rohan-patra/api/breachdirectory);required=true"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{
tools.InputTypeEmail,
tools.InputTypeUsername,
}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
type bdResponse struct {
Success bool `json:"success"`
Found int `json:"found"`
Result json.RawMessage `json:"result"`
}
type bdEntry struct {
Email string `json:"email"`
Password string `json:"password"`
Hash string `json:"hash"`
SHA1 string `json:"sha1"`
Sources string `json:"sources"`
HasPassword bool `json:"has_password"`
}
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://breachdirectory.p.rapidapi.com/?func=auto&term="+target, nil)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
req.Header.Set("X-RapidAPI-Key", r.cfg.APIKey)
req.Header.Set("X-RapidAPI-Host", "breachdirectory.p.rapidapi.com")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode != http.StatusOK {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var parsed bdResponse
if err := json.Unmarshal(body, &parsed); err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if !parsed.Success || parsed.Found == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var entries []bdEntry
if err := json.Unmarshal(parsed.Result, &entries); err != nil || len(entries) == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found in %d breach record(s)\n\n", parsed.Found))
for _, entry := range entries {
if entry.Sources != "" {
sb.WriteString(fmt.Sprintf("Source: %s\n", entry.Sources))
}
if entry.Password != "" {
sb.WriteString(fmt.Sprintf("Password: %s\n", entry.Password))
}
if entry.Hash != "" {
sb.WriteString(fmt.Sprintf("Hash: %s\n", entry.Hash))
}
if entry.SHA1 != "" {
sb.WriteString(fmt.Sprintf("SHA1: %s\n", entry.SHA1))
}
sb.WriteString("\n")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: parsed.Found}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,155 @@
package tools
import (
"reflect"
"strconv"
"strings"
)
// ReflectConfigFields builds []ConfigField from a struct using yaml/iky tags.
// iky tag format: iky:"desc=...;default=...;required=true;options=a|b|c"
func ReflectConfigFields(cfg any) []ConfigField {
v := reflect.ValueOf(cfg)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
var fields []ConfigField
for i := range t.NumField() {
sf := t.Field(i)
fv := v.Field(i)
yamlKey := sf.Tag.Get("yaml")
if yamlKey == "" || yamlKey == "-" {
continue
}
yamlKey = strings.SplitN(yamlKey, ",", 2)[0]
meta := parseIkyTag(sf.Tag.Get("iky"))
fieldType := goKindToString(sf.Type.Kind())
if len(meta.options) > 0 {
fieldType = "enum"
}
fields = append(fields, ConfigField{
Name: yamlKey,
Type: fieldType,
Required: meta.required,
Description: meta.desc,
Default: parseTypedDefault(meta.rawDefault, sf.Type.Kind()),
Value: fv.Interface(),
Options: meta.options,
})
}
return fields
}
// ApplyDefaults sets each field to its iky default if the field is zero.
func ApplyDefaults(cfg any) {
v := reflect.ValueOf(cfg)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := range t.NumField() {
sf := t.Field(i)
fv := v.Field(i)
if !fv.CanSet() {
continue
}
meta := parseIkyTag(sf.Tag.Get("iky"))
if meta.rawDefault == "" || !fv.IsZero() {
continue
}
applyDefault(fv, sf.Type.Kind(), meta.rawDefault)
}
}
type ikyMeta struct {
desc string
rawDefault string
required bool
options []string
}
func parseIkyTag(tag string) ikyMeta {
var m ikyMeta
for _, part := range strings.Split(tag, ";") {
k, v, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
continue
}
switch strings.TrimSpace(k) {
case "desc":
m.desc = strings.TrimSpace(v)
case "default":
m.rawDefault = strings.TrimSpace(v)
case "required":
m.required = strings.TrimSpace(v) == "true"
case "options":
for _, opt := range strings.Split(v, "|") {
if o := strings.TrimSpace(opt); o != "" {
m.options = append(m.options, o)
}
}
}
}
return m
}
func goKindToString(k reflect.Kind) string {
switch k {
case reflect.String:
return "string"
case reflect.Bool:
return "bool"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "int"
case reflect.Float32, reflect.Float64:
return "float"
default:
return k.String()
}
}
func parseTypedDefault(raw string, k reflect.Kind) any {
if raw == "" {
return nil
}
switch k {
case reflect.Bool:
b, _ := strconv.ParseBool(raw)
return b
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, _ := strconv.ParseInt(raw, 10, 64)
return int(n)
case reflect.Float32, reflect.Float64:
f, _ := strconv.ParseFloat(raw, 64)
return f
default:
return raw
}
}
func applyDefault(fv reflect.Value, k reflect.Kind, raw string) {
switch k {
case reflect.String:
fv.SetString(raw)
case reflect.Bool:
if b, err := strconv.ParseBool(raw); err == nil {
fv.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
fv.SetInt(n)
}
case reflect.Float32, reflect.Float64:
if f, err := strconv.ParseFloat(raw, 64); err == nil {
fv.SetFloat(f)
}
}
}

View File

@@ -0,0 +1,137 @@
package crtsh
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "crt.sh"
description = "SSL/TLS certificate transparency log search via crt.sh — enumerates subdomains and certificates issued for a domain."
link = "https://crt.sh"
icon = ""
)
type Runner struct{}
func New() tools.ToolRunner {
return &Runner{}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeDomain}
}
type crtEntry struct {
IssuerName string `json:"issuer_name"`
CommonName string `json:"common_name"`
NameValue string `json:"name_value"`
NotBefore string `json:"not_before"`
NotAfter string `json:"not_after"`
EntryTimestamp string `json:"entry_timestamp"`
}
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
params := url.Values{}
params.Set("q", "%."+target)
params.Set("output", "json")
apiURL := "https://crt.sh/?" + params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; crtsh-scanner/1.0)")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode != http.StatusOK {
msg := fmt.Sprintf("API error %d", resp.StatusCode)
if resp.StatusCode == http.StatusBadGateway || resp.StatusCode == http.StatusServiceUnavailable || resp.StatusCode == http.StatusGatewayTimeout {
msg = fmt.Sprintf("crt.sh is temporarily unavailable (%d), try again later", resp.StatusCode)
}
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var entries []crtEntry
if err := json.Unmarshal(body, &entries); err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if len(entries) == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
// Deduplicate subdomains from name_value fields
seen := make(map[string]struct{})
for _, e := range entries {
for _, line := range strings.Split(e.NameValue, "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "*") {
seen[line] = struct{}{}
}
}
}
subdomains := make([]string, 0, len(seen))
for s := range seen {
subdomains = append(subdomains, s)
}
sort.Strings(subdomains)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d unique subdomains across %d certificate entries\n\n", len(subdomains), len(entries)))
for _, s := range subdomains {
sb.WriteString(s + "\n")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: len(subdomains)}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,90 @@
package dig
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "dig"
description = "DNS lookup querying A, AAAA, MX, NS, TXT, and SOA records for a domain, or reverse DNS (PTR) for an IP."
link = "https://linux.die.net/man/1/dig"
icon = ""
)
var recordTypes = []string{"A", "AAAA", "MX", "NS", "TXT", "SOA"}
type Runner struct{}
func New() tools.ToolRunner { return &Runner{} }
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeDomain, tools.InputTypeIP}
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("dig"); err != nil {
return false, "dig binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"dig"} }
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
defer close(out)
var sb strings.Builder
totalCount := 0
if inputType == tools.InputTypeIP {
cmd := exec.CommandContext(ctx, "dig", "-x", target, "+noall", "+answer")
output, err := cmd.Output()
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
result := strings.TrimSpace(string(output))
if result != "" {
sb.WriteString("=== Reverse DNS (PTR) ===\n")
sb.WriteString(result)
totalCount += strings.Count(result, "\n") + 1
}
} else {
for _, rtype := range recordTypes {
if ctx.Err() != nil {
break
}
cmd := exec.CommandContext(ctx, "dig", target, rtype, "+noall", "+answer")
output, _ := cmd.Output()
result := strings.TrimSpace(string(output))
if result == "" {
continue
}
sb.WriteString(fmt.Sprintf("=== %s ===\n", rtype))
sb.WriteString(result)
sb.WriteString("\n\n")
totalCount += strings.Count(result, "\n") + 1
}
}
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else if sb.Len() > 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: totalCount}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,91 @@
package githubrecon
import (
"context"
"os/exec"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "github-recon"
description = "GitHub OSINT reconnaissance tool. Gathers profile info, social links, organisations, SSH/GPG keys, commits, and more from a GitHub username or email."
link = "https://github.com/anotherhadi/nur-osint"
icon = "github"
)
type Config struct {
Token string `yaml:"token" iky:"desc=GitHub personal access token (enables higher rate limits and more data);required=false"`
Deepscan bool `yaml:"deepscan" iky:"desc=Enable deep scan (slower - scans all repositories for authors/emails);default=false"`
SpoofEmail bool `yaml:"spoof_email" iky:"desc=Include email spoofing check (email mode only, requires token);default=false"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{
tools.InputTypeUsername,
tools.InputTypeEmail,
}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("github-recon"); err != nil {
return false, "github-recon binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"github-recon"} }
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
defer close(out)
args := []string{target}
if r.cfg.Token != "" {
args = append(args, "--token", r.cfg.Token)
}
if r.cfg.Deepscan {
args = append(args, "--deepscan")
}
if r.cfg.SpoofEmail && inputType == tools.InputTypeEmail {
args = append(args, "--spoof-email")
}
cmd := exec.CommandContext(ctx, "github-recon", args...)
output, err := tools.RunWithPTY(ctx, cmd)
// Remove banner
output = tools.RemoveFirstLines(output, 10)
count := 0
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else if output != "" {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
count = strings.Count(output, "Username:")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,55 @@
package gravatarrecon
import (
"context"
"os/exec"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "gravatar-recon"
description = "Gravatar OSINT tool. Extracts public profile data from a Gravatar account: name, bio, location, employment, social accounts, phone, and more."
link = "https://github.com/anotherhadi/gravatar-recon"
icon = ""
)
type Runner struct{}
func New() tools.ToolRunner { return &Runner{} }
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeEmail}
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("gravatar-recon"); err != nil {
return false, "gravatar-recon binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"gravatar-recon"} }
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
cmd := exec.CommandContext(ctx, "gravatar-recon", "--silent", target)
output, err := tools.RunWithPTY(ctx, cmd)
count := 0
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else if output != "" {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
count = 1
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,133 @@
package ipinfo
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "ipinfo"
description = "IP geolocation via ipinfo.io — returns city, region, country, coordinates, ASN/org, timezone, and hostname."
link = "https://ipinfo.io"
icon = ""
)
type Config struct {
Token string `yaml:"token" iky:"desc=ipinfo.io API token (optional — free tier allows 50k req/month without one);required=false"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeIP}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
type ipinfoResponse struct {
IP string `json:"ip"`
Hostname string `json:"hostname"`
City string `json:"city"`
Region string `json:"region"`
Country string `json:"country"`
Loc string `json:"loc"`
Org string `json:"org"`
Postal string `json:"postal"`
Timezone string `json:"timezone"`
Bogon bool `json:"bogon"`
}
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
url := fmt.Sprintf("https://ipinfo.io/%s/json", target)
if r.cfg.Token != "" {
url += "?token=" + r.cfg.Token
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
defer resp.Body.Close()
var info ipinfoResponse
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if info.Bogon {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: fmt.Sprintf("IP: %s\nType: Bogon/Private address", info.IP)}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 1}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var sb strings.Builder
field := func(label, value string) {
if value != "" {
sb.WriteString(fmt.Sprintf("%-12s %s\n", label+":", value))
}
}
field("IP", info.IP)
field("Hostname", info.Hostname)
field("City", info.City)
field("Region", info.Region)
field("Country", info.Country)
field("Coordinates", info.Loc)
field("Postal", info.Postal)
field("Timezone", info.Timezone)
field("Org/ASN", info.Org)
result := strings.TrimSpace(sb.String())
count := 0
if result != "" {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: result}
count = 1
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,178 @@
package leakcheck
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "leakcheck"
description = "Data breach lookup via LeakCheck.io — searches 7B+ leaked records for email addresses, usernames, and phone numbers across hundreds of breaches."
link = "https://leakcheck.io"
icon = ""
)
type Config struct {
APIKey string `yaml:"api_key" iky:"desc=LeakCheck API key (required — get one at leakcheck.io);required=true"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{
tools.InputTypeEmail,
tools.InputTypeUsername,
tools.InputTypePhone,
}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
type leakCheckResponse struct {
Success bool `json:"success"`
Found int `json:"found"`
Result []struct {
Email string `json:"email"`
Username string `json:"username"`
Phone string `json:"phone"`
Password string `json:"password"`
Hash string `json:"hash"`
Sources []string `json:"sources"`
Fields []string `json:"fields"`
} `json:"result"`
Error string `json:"error"`
}
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
defer close(out)
queryType := "auto"
switch inputType {
case tools.InputTypeEmail:
queryType = "email"
case tools.InputTypeUsername:
queryType = "login"
case tools.InputTypePhone:
queryType = "phone"
}
url := fmt.Sprintf("https://leakcheck.io/api/v2/query/%s?type=%s", target, queryType)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
req.Header.Set("X-API-Key", r.cfg.APIKey)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var result leakCheckResponse
if err := json.Unmarshal(body, &result); err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if !result.Success {
msg := result.Error
if msg == "" {
msg = "API returned failure"
}
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if result.Found == 0 || len(result.Result) == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found in %d breach(es)\n\n", result.Found))
for _, entry := range result.Result {
if len(entry.Sources) > 0 {
sb.WriteString(fmt.Sprintf("Sources: %s\n", strings.Join(entry.Sources, ", ")))
}
if entry.Email != "" {
sb.WriteString(fmt.Sprintf(" Email: %s\n", entry.Email))
}
if entry.Username != "" {
sb.WriteString(fmt.Sprintf(" Username: %s\n", entry.Username))
}
if entry.Phone != "" {
sb.WriteString(fmt.Sprintf(" Phone: %s\n", entry.Phone))
}
if entry.Password != "" {
sb.WriteString(fmt.Sprintf(" Password: %s\n", entry.Password))
}
if entry.Hash != "" {
sb.WriteString(fmt.Sprintf(" Hash: %s\n", entry.Hash))
}
if len(entry.Fields) > 0 {
sb.WriteString(fmt.Sprintf(" Fields: %s\n", strings.Join(entry.Fields, ", ")))
}
sb.WriteString("\n")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: result.Found}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,89 @@
package maigret
import (
"context"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "maigret"
description = "Username OSINT across 3000+ sites. Searches social networks, forums, and online platforms for an account matching the target username."
link = "https://github.com/soxoj/maigret"
icon = ""
)
var accountsRe = regexp.MustCompile(`returned (\d+) accounts`)
type Config struct {
AllSites bool `yaml:"all_sites" iky:"desc=Scan all sites in the database instead of just the top 500 (slower);default=false"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeUsername}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("maigret"); err != nil {
return false, "maigret binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"maigret"} }
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
args := []string{"--no-progressbar", target}
if r.cfg.AllSites {
args = append(args, "-a")
}
cmd := exec.CommandContext(ctx, "maigret", args...)
output, err := tools.RunWithPTY(ctx, cmd)
// Crop at Python traceback (NixOS read-only store error — results are unaffected)
if idx := strings.Index(output, "Traceback (most recent call last)"); idx != -1 {
output = strings.TrimSpace(output[:idx])
}
count := 0
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else if output != "" {
// Parse count from summary line: "returned N accounts"
if m := accountsRe.FindStringSubmatch(output); len(m) == 2 {
count, _ = strconv.Atoi(m[1])
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,28 @@
package tools
import (
"context"
"io"
"os/exec"
"regexp"
"github.com/creack/pty"
)
// oscRe strips OSC terminal sequences emitted by the PTY (e.g. colour queries).
var oscRe = regexp.MustCompile(`\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)`)
// RunWithPTY runs cmd under a pseudo-terminal (preserving ANSI colours) and
// returns the full output once the process exits.
func RunWithPTY(ctx context.Context, cmd *exec.Cmd) (string, error) {
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 50, Cols: 220})
if err != nil {
return "", err
}
defer func() { _ = ptmx.Close() }()
output, _ := io.ReadAll(ptmx)
_ = cmd.Wait()
return oscRe.ReplaceAllString(string(output), ""), ctx.Err()
}

View File

@@ -0,0 +1,72 @@
package tools
import "context"
type EventType string
const (
EventTypeOutput EventType = "output" // raw ANSI text, payload is a plain string
EventTypeError EventType = "error"
EventTypeCount EventType = "count" // payload is int, additive — emit once or multiple times from Run
EventTypeDone EventType = "done"
)
type InputType string
const (
InputTypeEmail InputType = "email"
InputTypeUsername InputType = "username"
InputTypePhone InputType = "phone"
InputTypeIP InputType = "ip"
InputTypeDomain InputType = "domain"
InputTypePassword InputType = "password"
InputTypeName InputType = "name"
)
type Event struct {
Tool string `json:"tool"`
Type EventType `json:"type"`
Payload interface{} `json:"payload,omitempty"`
}
// ToolRunner is the core interface every tool must implement.
type ToolRunner interface {
Name() string
Description() string
Link() string // URL to source or documentation
Icon() string // Simple Icons slug, empty if none
InputTypes() []InputType
// Run executes the tool and sends Events to out. Must close out when done.
// inputType indicates what kind of value target is (email, username, ...).
Run(ctx context.Context, target string, inputType InputType, out chan<- Event) error
}
type Configurable interface {
ConfigPtr() interface{}
}
type ConfigField struct {
Name string `json:"name"`
Type string `json:"type"` // "string", "bool", "int", "float", "enum"
Required bool `json:"required"`
Default any `json:"default"`
Description string `json:"description"`
Value any `json:"value"`
Options []string `json:"options,omitempty"` // non-empty when Type == "enum"
}
type ConfigDescriber interface {
ConfigFields() []ConfigField
}
// AvailabilityChecker is implemented by tools that require an external binary.
type AvailabilityChecker interface {
Available() (ok bool, reason string)
}
type DependencyLister interface {
Dependencies() []string
}

View File

@@ -0,0 +1,95 @@
package userscanner
import (
"context"
"os/exec"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "user-scanner"
description = "🕵️‍♂️ (2-in-1) Email & Username OSINT suite. Analyzes 195+ scan vectors (95+ email / 100+ username) for security research, investigations, and digital footprinting."
link = "https://github.com/kaifcodec/user-scanner"
icon = ""
)
type Config struct {
AllowLoud bool `yaml:"allow_loud" iky:"desc=Enable scanning sites that may send emails/notifications (password resets, etc.);default=false"`
OnlyFound bool `yaml:"only_found" iky:"desc=Only show sites where the username/email was found;default=true"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{
tools.InputTypeEmail,
tools.InputTypeUsername,
}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("user-scanner"); err != nil {
return false, "user-scanner binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"user-scanner"} }
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
defer close(out)
args := make([]string, 0, 6)
switch inputType {
case tools.InputTypeEmail:
args = append(args, "-e", target)
default:
args = append(args, "-u", target)
}
if r.cfg.AllowLoud {
args = append(args, "--allow-loud")
}
if r.cfg.OnlyFound {
args = append(args, "--only-found")
}
cmd := exec.CommandContext(ctx, "user-scanner", args...)
output, err := tools.RunWithPTY(ctx, cmd)
// Removing banner
output = tools.RemoveFirstLines(output, 8)
// count =
output = tools.CropAfterExclude(output, "[i] Scan complete.")
count := 0
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else if output != "" {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
count = strings.Count(output, "[✔]")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,61 @@
package tools
import "strings"
// RemoveFirstLines removes the first n lines. Returns "" if n >= total lines.
func RemoveFirstLines(input string, n int) string {
lines := strings.Split(input, "\n")
if n >= len(lines) {
return ""
}
return strings.Join(lines[n:], "\n")
}
// RemoveLastLines removes the last n lines. Returns "" if n >= total lines.
func RemoveLastLines(input string, n int) string {
lines := strings.Split(input, "\n")
if n >= len(lines) {
return ""
}
return strings.Join(lines[:len(lines)-n], "\n")
}
// CropBefore removes everything before the first occurrence of y (inclusive of y).
// Returns input unchanged if y is not found.
func CropBefore(input string, y string) string {
idx := strings.Index(input, y)
if idx == -1 {
return input
}
return input[idx:]
}
// CropAfter removes everything after the last occurrence of y (inclusive of y).
// Returns input unchanged if y is not found.
func CropAfter(input string, y string) string {
idx := strings.LastIndex(input, y)
if idx == -1 {
return input
}
return input[:idx+len(y)]
}
// CropBeforeExclude removes everything before and including the first occurrence of y.
// Returns input unchanged if y is not found.
func CropBeforeExclude(input string, y string) string {
idx := strings.Index(input, y)
if idx == -1 {
return input
}
return input[idx+len(y):]
}
// CropAfterExclude removes everything from the last occurrence of y onwards.
// Returns input unchanged if y is not found.
func CropAfterExclude(input string, y string) string {
idx := strings.LastIndex(input, y)
if idx == -1 {
return input
}
return input[:idx]
}

View File

@@ -0,0 +1,126 @@
package wappalyzer
import (
"context"
"fmt"
"io"
"net/http"
"sort"
"strings"
wappalyzergo "github.com/projectdiscovery/wappalyzergo"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "wappalyzer"
description = "Web technology fingerprinting via wappalyzergo — detects CMS, frameworks, web servers, analytics, CDN, and 1500+ other technologies running on a domain."
link = "https://github.com/projectdiscovery/wappalyzergo"
icon = "wappalyzer"
)
type Runner struct {
wappalyze *wappalyzergo.Wappalyze
}
func New() tools.ToolRunner {
w, _ := wappalyzergo.New()
return &Runner{wappalyze: w}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeDomain}
}
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
// Try HTTPS first, fall back to HTTP
var (
resp *http.Response
body []byte
err error
)
for _, scheme := range []string{"https", "http"} {
targetURL := fmt.Sprintf("%s://%s", scheme, target)
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
if reqErr != nil {
continue
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")
resp, err = http.DefaultClient.Do(req)
if err == nil {
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err == nil {
break
}
}
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
}
if err != nil || resp == nil {
msg := "failed to connect to target"
if err != nil {
msg = err.Error()
}
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
fingerprints := r.wappalyze.FingerprintWithInfo(resp.Header, body)
if len(fingerprints) == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
// Group by category
byCategory := make(map[string][]string)
for tech, info := range fingerprints {
cats := info.Categories
if len(cats) == 0 {
cats = []string{"Other"}
}
for _, cat := range cats {
byCategory[cat] = append(byCategory[cat], tech)
}
}
cats := make([]string, 0, len(byCategory))
for c := range byCategory {
cats = append(cats, c)
}
sort.Strings(cats)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Detected %d technologies\n\n", len(fingerprints)))
for _, cat := range cats {
techs := byCategory[cat]
sort.Strings(techs)
sb.WriteString(fmt.Sprintf("%s:\n", cat))
for _, t := range techs {
sb.WriteString(fmt.Sprintf(" - %s\n", t))
}
sb.WriteString("\n")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: len(fingerprints)}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,62 @@
package whois
import (
"context"
"os/exec"
"strings"
"github.com/anotherhadi/iknowyou/internal/tools"
)
const (
name = "whois"
description = "WHOIS lookup for domain registration and IP ownership information."
link = "https://en.wikipedia.org/wiki/WHOIS"
icon = ""
)
type Runner struct{}
func New() tools.ToolRunner { return &Runner{} }
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{tools.InputTypeDomain, tools.InputTypeIP}
}
func (r *Runner) Available() (bool, string) {
if _, err := exec.LookPath("whois"); err != nil {
return false, "whois binary not found in PATH"
}
return true, ""
}
func (r *Runner) Dependencies() []string { return []string{"whois"} }
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
defer close(out)
cmd := exec.CommandContext(ctx, "whois", target)
output, err := cmd.Output()
if err != nil && ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
result := strings.TrimSpace(string(output))
count := 0
if result != "" {
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: result}
count = 1
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}

View File

@@ -0,0 +1,232 @@
package whoisfreaks
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/anotherhadi/iknowyou/internal/tools"
"github.com/tidwall/gjson"
)
const (
name = "whoisfreaks"
description = "Reverse WHOIS lookup via WhoisFreaks — find all domains registered by an email, owner name, or keyword across 3.6B+ WHOIS records."
link = "https://whoisfreaks.com"
icon = ""
)
type Config struct {
APIKey string `yaml:"api_key" iky:"desc=WhoisFreaks API key (required — free account at whoisfreaks.com);required=true"`
}
type Runner struct {
cfg Config
}
func New() tools.ToolRunner {
cfg := Config{}
tools.ApplyDefaults(&cfg)
return &Runner{cfg: cfg}
}
func (r *Runner) Name() string { return name }
func (r *Runner) Description() string { return description }
func (r *Runner) Link() string { return link }
func (r *Runner) Icon() string { return icon }
func (r *Runner) InputTypes() []tools.InputType {
return []tools.InputType{
tools.InputTypeEmail,
tools.InputTypeName,
tools.InputTypeDomain,
}
}
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
func (r *Runner) ConfigFields() []tools.ConfigField {
return tools.ReflectConfigFields(r.cfg)
}
var skipKeys = map[string]bool{
"num": true, "status": true, "query_time": true, "update_date": true,
"iana_id": true, "whois_server": true, "handle": true,
"zip_code": true, "country_code": true, "mailing_address": true,
"phone_number": true, "administrative_contact": true, "technical_contact": true,
}
func prettyResult(r gjson.Result, depth int) string {
indent := strings.Repeat(" ", depth)
var sb strings.Builder
r.ForEach(func(key, val gjson.Result) bool {
k := key.String()
if skipKeys[k] {
return true
}
switch val.Type {
case gjson.JSON:
if val.IsArray() {
arr := val.Array()
if len(arr) == 0 {
return true
}
sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k))
for _, item := range arr {
if item.Type == gjson.JSON {
sb.WriteString(fmt.Sprintf("%s -\n", indent))
sb.WriteString(prettyResult(item, depth+2))
} else {
sb.WriteString(fmt.Sprintf("%s - %s\n", indent, item.String()))
}
}
} else {
sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k))
sb.WriteString(prettyResult(val, depth+1))
}
default:
v := val.String()
if v == "" {
return true
}
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, k, v))
}
return true
})
return sb.String()
}
func doRequest(ctx context.Context, req *http.Request) ([]byte, *http.Response, error) {
for {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, nil, err
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, resp, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return body, resp, nil
}
select {
case <-ctx.Done():
return nil, resp, ctx.Err()
case <-time.After(60 * time.Second):
}
// Rebuild the request since the body was consumed
req2, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), nil)
if err != nil {
return nil, resp, err
}
req2.Header = req.Header
req = req2
}
}
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
defer close(out)
params := url.Values{}
params.Set("whois", "reverse")
params.Set("apiKey", r.cfg.APIKey)
switch inputType {
case tools.InputTypeEmail:
params.Set("email", target)
case tools.InputTypeName:
params.Set("owner", target)
case tools.InputTypeDomain:
params.Set("keyword", target)
default:
params.Set("keyword", target)
}
req, err := http.NewRequestWithContext(ctx,
http.MethodGet,
"https://api.whoisfreaks.com/v1.0/whois?"+params.Encode(),
nil,
)
if err != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
req.Header.Set("Accept", "application/json")
body, resp, err := doRequest(ctx, req)
if err != nil {
if ctx.Err() != nil {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
} else {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode == http.StatusNotFound {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
if resp.StatusCode != http.StatusOK {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
j := gjson.ParseBytes(body)
if !j.Get("whois_domains_historical").Exists() {
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "unexpected response format"}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
domains := j.Get("whois_domains_historical").Array()
if len(domains) == 0 {
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}
total := j.Get("total_Result").Int()
totalPages := j.Get("total_Pages").Int()
currentPage := j.Get("current_Page").Int()
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d domain(s)", total))
if totalPages > 1 {
sb.WriteString(fmt.Sprintf(" across %d pages (showing page %d)", totalPages, currentPage))
}
sb.WriteString("\n\n")
for _, d := range domains {
sb.WriteString(prettyResult(d, 0))
sb.WriteString("\n")
}
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: int(total)}
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
return nil
}