diff options
| author | s <[email protected]> | 2025-11-03 21:17:12 -0500 |
|---|---|---|
| committer | s <[email protected]> | 2025-11-03 21:17:12 -0500 |
| commit | f7fcfa623e670dc533bb378912829c73a3593e63 (patch) | |
| tree | 910119ff7293b407affa9ff34706d627d77a3a04 | |
| download | dborg-f7fcfa623e670dc533bb378912829c73a3593e63.tar.gz dborg-f7fcfa623e670dc533bb378912829c73a3593e63.zip | |
hi
| -rw-r--r-- | .gitignore | 32 | ||||
| -rw-r--r-- | Makefile | 16 | ||||
| -rw-r--r-- | README.md | 177 | ||||
| -rw-r--r-- | cmd/admin.go | 238 | ||||
| -rw-r--r-- | cmd/npd.go | 95 | ||||
| -rw-r--r-- | cmd/osint.go | 103 | ||||
| -rw-r--r-- | cmd/root.go | 25 | ||||
| -rw-r--r-- | cmd/skiptrace.go | 199 | ||||
| -rw-r--r-- | cmd/sl.go | 73 | ||||
| -rw-r--r-- | cmd/x.go | 61 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | internal/client/admin.go | 124 | ||||
| -rw-r--r-- | internal/client/client.go | 119 | ||||
| -rw-r--r-- | internal/client/client_test.go | 46 | ||||
| -rw-r--r-- | internal/client/npd.go | 91 | ||||
| -rw-r--r-- | internal/client/osint.go | 32 | ||||
| -rw-r--r-- | internal/client/skiptrace.go | 179 | ||||
| -rw-r--r-- | internal/client/sl.go | 53 | ||||
| -rw-r--r-- | internal/client/usrsx.go | 72 | ||||
| -rw-r--r-- | internal/client/x.go | 23 | ||||
| -rw-r--r-- | internal/config/config.go | 36 | ||||
| -rw-r--r-- | internal/config/errors.go | 7 | ||||
| -rw-r--r-- | internal/models/admin.go | 43 | ||||
| -rw-r--r-- | internal/models/npd.go | 42 | ||||
| -rw-r--r-- | internal/models/osint.go | 29 | ||||
| -rw-r--r-- | internal/models/skiptrace.go | 92 | ||||
| -rw-r--r-- | internal/models/sl.go | 23 | ||||
| -rw-r--r-- | internal/models/usrsx.go | 33 | ||||
| -rw-r--r-- | internal/models/x.go | 22 | ||||
| -rw-r--r-- | internal/utils/output.go | 39 | ||||
| -rw-r--r-- | main.go | 7 |
32 files changed, 2151 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaadf73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..04e7e39 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: build build-admin clean install install-admin + +build: + go build -o dborg . + +build-admin: + go build -tags admin -o dborg . + +clean: + rm -f dborg + +install: + go install . + +install-admin: + go install -tags admin . diff --git a/README.md b/README.md new file mode 100644 index 0000000..a835822 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# dborg - DB.org.ai CLI + +CLI tool for querying the DB.org.ai API services. + +## Project Structure + +``` +dborg/ +├── cmd/ # CLI commands +│ ├── admin.go # Admin commands (build tag: admin) +│ ├── npd.go # NPD breach data search +│ ├── root.go # Root command configuration +│ ├── sl.go # Stealer logs search +│ ├── usrsx.go # Username availability check +│ └── x.go # Twitter/X username history +├── internal/ # Private application code +│ ├── client/ # API client implementation +│ │ ├── admin.go # Admin API methods +│ │ ├── client.go # Base HTTP client +│ │ ├── client_test.go # Client tests +│ │ ├── npd.go # NPD API methods +│ │ ├── sl.go # Stealer logs API methods +│ │ ├── usrsx.go # Username check API methods +│ │ └── x.go # Twitter/X API methods +│ ├── config/ # Configuration management +│ │ ├── config.go # Config structure +│ │ └── errors.go # Custom errors +│ ├── models/ # Data models +│ │ ├── admin.go # Admin response types +│ │ ├── npd.go # NPD response types +│ │ ├── sl.go # Stealer logs types +│ │ ├── usrsx.go # Username check types +│ │ └── x.go # Twitter/X types +│ └── utils/ # Utility functions +│ └── output.go # Output formatting helpers +├── go.mod +├── go.sum +├── main.go +├── Makefile +└── README.md +``` + +## Installation + +```bash +go install +``` + +To build with admin commands: + +```bash +make build-admin +``` + +Or: + +```bash +go build -tags admin -o dborg . +``` + +## Configuration + +Set your API key: + +```bash +export DBORG_API_KEY=your_api_key_here +``` + +Or pass it with each command: + +```bash +dborg --api-key YOUR_KEY [command] +``` + +## Commands + +### NPD - Search NPD breach data + +```bash +dborg npd --firstname John --lastname Doe --max_hits 20 +``` + +Available flags: +- `--id`, `--firstname`, `--lastname`, `--middlename` +- `--dob`, `--ssn`, `--phone1` +- `--address`, `--city`, `--st`, `--zip`, `--county_name` +- `--name_suff`, `--aka1fullname`, `--aka2fullname`, `--aka3fullname` +- `--alt1dob`, `--alt2dob`, `--alt3dob`, `--startdat` +- `--max_hits` (default: 10) +- `--sort_by` + +### SL - Search stealer logs + +```bash +dborg sl "domain.com" --max_hits 20 --format json +``` + +Available flags: +- `--max_hits` (default: 10) +- `--sort_by` (ingest_timestamp or date_posted) +- `--ingest_start_date`, `--ingest_end_date` +- `--posted_start_date`, `--posted_end_date` +- `--format` (json, ulp, up, pul, etc.) + +### USRSX - Check username availability + +```bash +dborg usrsx username123 --sites GitHub,Twitter --max_tasks 100 +``` + +Available flags: +- `--sites` (comma-separated list) +- `--fuzzy` (enable fuzzy validation) +- `--max_tasks` (default: 50) + +### X - Twitter/X username history + +```bash +dborg x elonmusk +``` + +### Admin Commands (requires build tag) + +Only available when built with `-tags admin`: + +```bash +dborg admin list +dborg admin create john_doe --credits 1000 --unlimited +dborg admin delete API_KEY +dborg admin credits API_KEY 500 +dborg admin disable API_KEY +dborg admin disable API_KEY --enable +``` + +## Architecture Benefits + +The refactored structure provides: + +1. **Separation of Concerns**: API logic is separated from CLI commands +2. **Testability**: Components can be easily unit tested +3. **Maintainability**: Clear organization makes code easier to maintain +4. **Reusability**: Client can be reused in other Go applications +5. **Error Handling**: Centralized error handling with custom error types +6. **Configuration Management**: Single source of truth for configuration +7. **Type Safety**: Strongly typed models for API requests/responses + +## Building + +Standard build (no admin commands): +```bash +make build +``` + +Build with admin commands: +```bash +make build-admin +``` + +Run tests: +```bash +go test ./... +``` + +Clean: +```bash +make clean +``` + +## Development + +The codebase follows Go best practices: + +- Internal packages for private implementation +- Interface-based design for testability +- Structured error handling +- Clear separation between CLI and business logic +- Comprehensive type definitions for API interactions diff --git a/cmd/admin.go b/cmd/admin.go new file mode 100644 index 0000000..3c43e20 --- /dev/null +++ b/cmd/admin.go @@ -0,0 +1,238 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "dborg/internal/models" + "encoding/json" + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +var adminCmd = &cobra.Command{ + Use: "admin", + Short: "Admin operations", + Long: `Administrative operations for managing accounts`, +} + +var adminListCmd = &cobra.Command{ + Use: "list", + Short: "List all accounts", + RunE: runAdminList, +} + +var adminCreateCmd = &cobra.Command{ + Use: "create [name]", + Short: "Create new account", + Args: cobra.ExactArgs(1), + RunE: runAdminCreate, +} + +var adminDeleteCmd = &cobra.Command{ + Use: "delete [api_key]", + Short: "Delete account", + Args: cobra.ExactArgs(1), + RunE: runAdminDelete, +} + +var adminCreditsCmd = &cobra.Command{ + Use: "credits [api_key] [amount]", + Short: "Add credits to account", + Args: cobra.ExactArgs(2), + RunE: runAdminCredits, +} + +var adminSetCreditsCmd = &cobra.Command{ + Use: "set-credits [api_key] [amount]", + Short: "Set account credits to specific amount", + Args: cobra.ExactArgs(2), + RunE: runAdminSetCredits, +} + +var adminDisableCmd = &cobra.Command{ + Use: "disable [api_key]", + Short: "Disable/enable account", + Args: cobra.ExactArgs(1), + RunE: runAdminDisable, +} + +func init() { + rootCmd.AddCommand(adminCmd) + adminCmd.AddCommand(adminListCmd) + adminCmd.AddCommand(adminCreateCmd) + adminCmd.AddCommand(adminDeleteCmd) + adminCmd.AddCommand(adminCreditsCmd) + adminCmd.AddCommand(adminSetCreditsCmd) + adminCmd.AddCommand(adminDisableCmd) + + adminCreateCmd.Flags().IntP("credits", "c", 0, "Initial credits") + adminCreateCmd.Flags().BoolP("unlimited", "u", false, "Unlimited credits") + adminCreateCmd.Flags().BoolP("premium", "p", false, "Premium account (enables skiptrace access)") + adminDisableCmd.Flags().BoolP("enable", "e", false, "Enable account instead of disable") +} + +func getAdminClient(cmd *cobra.Command) (*client.Client, error) { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + return client.New(cfg) +} + +func runAdminList(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + response, err := c.ListAccounts() + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + output, err := json.MarshalIndent(response.Accounts, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + + fmt.Println(string(output)) + return nil +} + +func runAdminCreate(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + credits, _ := cmd.Flags().GetInt("credits") + unlimited, _ := cmd.Flags().GetBool("unlimited") + premium, _ := cmd.Flags().GetBool("premium") + + req := &models.AccountCreateRequest{ + Name: args[0], + Credits: credits, + Unlimited: unlimited, + IsPremium: premium, + } + + response, err := c.CreateAccount(req) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if response.Account != nil { + fmt.Printf("Account created successfully!\n") + fmt.Printf("Name: %s\n", response.Account.Name) + fmt.Printf("API Key: %s\n", response.Account.APIKey) + fmt.Printf("Credits: %d\n", response.Account.Credits) + fmt.Printf("Unlimited: %v\n", response.Account.Unlimited) + fmt.Printf("Premium: %v\n", response.Account.IsPremium) + fmt.Printf("Disabled: %v\n", response.Account.Disabled) + } else { + fmt.Println(response.Message) + } + return nil +} + +func runAdminDelete(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + response, err := c.DeleteAccount(args[0]) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + fmt.Println(response.Message) + return nil +} + +func runAdminCredits(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + credits, err := strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid credits amount: %s", args[1]) + } + + response, err := c.UpdateCredits(args[0], credits) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + fmt.Println(response.Message) + return nil +} + +func runAdminSetCredits(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + credits, err := strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid credits amount: %s", args[1]) + } + + response, err := c.SetCredits(args[0], credits) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if response.Account != nil { + fmt.Printf("Credits set successfully!\n") + fmt.Printf("Account: %s\n", response.Account.Name) + fmt.Printf("API Key: %s\n", response.Account.APIKey) + fmt.Printf("Credits: %d\n", response.Account.Credits) + } else if response.Message != "" { + fmt.Println(response.Message) + } + return nil +} + +func runAdminDisable(cmd *cobra.Command, args []string) error { + c, err := getAdminClient(cmd) + if err != nil { + return err + } + + enable, _ := cmd.Flags().GetBool("enable") + response, err := c.ToggleAccount(args[0], enable) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + fmt.Println(response.Message) + return nil +} diff --git a/cmd/npd.go b/cmd/npd.go new file mode 100644 index 0000000..0df627f --- /dev/null +++ b/cmd/npd.go @@ -0,0 +1,95 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "dborg/internal/models" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +var npdCmd = &cobra.Command{ + Use: "npd", + Short: "Search NPD breach data", + Long: `Search NPD breach data by various fields`, + RunE: runNPDSearch, +} + +func init() { + rootCmd.AddCommand(npdCmd) + npdCmd.Flags().StringP("id", "i", "", "ID") + npdCmd.Flags().StringP("firstname", "f", "", "First name") + npdCmd.Flags().StringP("lastname", "l", "", "Last name") + npdCmd.Flags().StringP("middlename", "m", "", "Middle name") + npdCmd.Flags().StringP("dob", "d", "", "Date of birth") + npdCmd.Flags().StringP("ssn", "s", "", "Social security number") + npdCmd.Flags().StringP("phone1", "p", "", "Phone number") + npdCmd.Flags().StringP("address", "a", "", "Address") + npdCmd.Flags().StringP("city", "c", "", "City") + npdCmd.Flags().StringP("st", "t", "", "State") + npdCmd.Flags().StringP("zip", "z", "", "ZIP code") + npdCmd.Flags().StringP("county_name", "y", "", "County name") + npdCmd.Flags().StringP("name_suff", "x", "", "Name suffix") + npdCmd.Flags().StringP("aka1fullname", "1", "", "AKA 1 full name") + npdCmd.Flags().StringP("aka2fullname", "2", "", "AKA 2 full name") + npdCmd.Flags().StringP("aka3fullname", "3", "", "AKA 3 full name") + npdCmd.Flags().StringP("alt1dob", "4", "", "Alternate DOB 1") + npdCmd.Flags().StringP("alt2dob", "5", "", "Alternate DOB 2") + npdCmd.Flags().StringP("alt3dob", "6", "", "Alternate DOB 3") + npdCmd.Flags().StringP("startdat", "r", "", "Start date") + npdCmd.Flags().IntP("max_hits", "n", 10, "Maximum number of hits to return") + npdCmd.Flags().StringP("sort_by", "o", "", "Sort by field") +} + +func runNPDSearch(cmd *cobra.Command, args []string) error { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + + c, err := client.New(cfg) + if err != nil { + return err + } + + params := &models.NPDParams{} + params.ID, _ = cmd.Flags().GetString("id") + params.FirstName, _ = cmd.Flags().GetString("firstname") + params.LastName, _ = cmd.Flags().GetString("lastname") + params.MiddleName, _ = cmd.Flags().GetString("middlename") + params.DOB, _ = cmd.Flags().GetString("dob") + params.SSN, _ = cmd.Flags().GetString("ssn") + params.Phone1, _ = cmd.Flags().GetString("phone1") + params.Address, _ = cmd.Flags().GetString("address") + params.City, _ = cmd.Flags().GetString("city") + params.State, _ = cmd.Flags().GetString("st") + params.Zip, _ = cmd.Flags().GetString("zip") + params.CountyName, _ = cmd.Flags().GetString("county_name") + params.NameSuffix, _ = cmd.Flags().GetString("name_suff") + params.AKA1FullName, _ = cmd.Flags().GetString("aka1fullname") + params.AKA2FullName, _ = cmd.Flags().GetString("aka2fullname") + params.AKA3FullName, _ = cmd.Flags().GetString("aka3fullname") + params.Alt1DOB, _ = cmd.Flags().GetString("alt1dob") + params.Alt2DOB, _ = cmd.Flags().GetString("alt2dob") + params.Alt3DOB, _ = cmd.Flags().GetString("alt3dob") + params.StartDate, _ = cmd.Flags().GetString("startdat") + params.MaxHits, _ = cmd.Flags().GetInt("max_hits") + params.SortBy, _ = cmd.Flags().GetString("sort_by") + + response, err := c.SearchNPD(params) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + output, err := json.MarshalIndent(response.Results.Hits, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + + fmt.Println(string(output)) + return nil +} diff --git a/cmd/osint.go b/cmd/osint.go new file mode 100644 index 0000000..2f1f427 --- /dev/null +++ b/cmd/osint.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "dborg/internal/models" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +var osintCmd = &cobra.Command{ + Use: "osint", + Short: "OSINT tools and searches", + Long: `Open Source Intelligence tools for username, email, and other searches`, +} + +var osintUsernameCmd = &cobra.Command{ + Use: "username [username]", + Short: "Check username availability across websites", + Long: `Check username availability across hundreds of websites using WhatsMyName dataset`, + Args: cobra.ExactArgs(1), + RunE: runOsintUsernameCheck, +} + +var osintBSSIDCmd = &cobra.Command{ + Use: "bssid [bssid]", + Short: "Lookup WiFi access point location by BSSID", + Long: `Lookup geographic location of a WiFi access point by its BSSID (MAC address) using Apple's location services`, + Args: cobra.ExactArgs(1), + RunE: runOsintBSSIDLookup, +} + +func init() { + rootCmd.AddCommand(osintCmd) + osintCmd.AddCommand(osintUsernameCmd) + osintCmd.AddCommand(osintBSSIDCmd) + + osintUsernameCmd.Flags().StringSliceP("sites", "s", []string{}, "Specific sites to check (comma-separated)") + osintUsernameCmd.Flags().BoolP("fuzzy", "f", false, "Enable fuzzy validation mode") + osintUsernameCmd.Flags().IntP("max_tasks", "m", 50, "Maximum concurrent tasks") + + osintBSSIDCmd.Flags().BoolP("all", "a", false, "Show all related results instead of exact match only") + osintBSSIDCmd.Flags().BoolP("map", "m", false, "Include Google Maps URL for the location") +} + +func runOsintUsernameCheck(cmd *cobra.Command, args []string) error { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + + c, err := client.New(cfg) + if err != nil { + return err + } + + params := &models.USRSXParams{ + Username: args[0], + } + params.Sites, _ = cmd.Flags().GetStringSlice("sites") + params.Fuzzy, _ = cmd.Flags().GetBool("fuzzy") + params.MaxTasks, _ = cmd.Flags().GetInt("max_tasks") + + err = c.CheckUsernameStream(params, func(result json.RawMessage) error { + fmt.Println(string(result)) + return nil + }) + + if err != nil { + return err + } + + return nil +} + +func runOsintBSSIDLookup(cmd *cobra.Command, args []string) error { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + + c, err := client.New(cfg) + if err != nil { + return err + } + + params := &models.BSSIDParams{ + BSSID: args[0], + } + params.All, _ = cmd.Flags().GetBool("all") + params.Map, _ = cmd.Flags().GetBool("map") + + response, err := c.LookupBSSID(params) + if err != nil { + return err + } + + output, err := json.MarshalIndent(response, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + + fmt.Println(string(output)) + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b8ab9e4 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "dborg", + Short: "DB.org.ai CLI client", + Long: `Query db.org.ai API`, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringP("api-key", "k", os.Getenv("DBORG_API_KEY"), "API key for authentication") +} diff --git a/cmd/skiptrace.go b/cmd/skiptrace.go new file mode 100644 index 0000000..68fb832 --- /dev/null +++ b/cmd/skiptrace.go @@ -0,0 +1,199 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "dborg/internal/models" + "encoding/json" + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +var skiptraceCmd = &cobra.Command{ + Use: "skiptrace", + Short: "Premium skiptrace operations (requires premium API access)", + Long: `Search for people, phone numbers, and email addresses using premium skiptrace data. + +Note: All skiptrace commands require a premium API key. If you receive a 403 error, +contact support to upgrade your account for premium access.`, +} + +var skiptracePeopleCmd = &cobra.Command{ + Use: "people", + Short: "Search for people by name", + Long: `Search for people by first name, last name, and optional location/age filters`, + RunE: runSkiptracePeople, +} + +var skiptraceReportCmd = &cobra.Command{ + Use: "report [sx_key] [selection]", + Short: "Get detailed report for selected person", + Long: `Retrieve detailed report for a person from previous search results using sx_key and selection number`, + Args: cobra.ExactArgs(2), + RunE: runSkiptraceReport, +} + +var skiptracePhoneCmd = &cobra.Command{ + Use: "phone [phone_number]", + Short: "Search for phone number", + Long: `Look up information about a phone number (10 digits, no +1 prefix)`, + Args: cobra.ExactArgs(1), + RunE: runSkiptracePhone, +} + +var skiptraceEmailCmd = &cobra.Command{ + Use: "email [email_address]", + Short: "Search for email address", + Long: `Look up information about an email address`, + Args: cobra.ExactArgs(1), + RunE: runSkiptraceEmail, +} + +func init() { + rootCmd.AddCommand(skiptraceCmd) + skiptraceCmd.AddCommand(skiptracePeopleCmd) + skiptraceCmd.AddCommand(skiptraceReportCmd) + skiptraceCmd.AddCommand(skiptracePhoneCmd) + skiptraceCmd.AddCommand(skiptraceEmailCmd) + + skiptracePeopleCmd.Flags().StringP("first-name", "f", "", "First name (required)") + skiptracePeopleCmd.Flags().StringP("last-name", "l", "", "Last name (required)") + skiptracePeopleCmd.Flags().StringP("city", "c", "", "City") + skiptracePeopleCmd.Flags().StringP("state", "s", "", "State (2-letter code)") + skiptracePeopleCmd.Flags().StringP("age", "a", "", "Age") + skiptracePeopleCmd.MarkFlagRequired("first-name") + skiptracePeopleCmd.MarkFlagRequired("last-name") +} + +func getSkiptraceClient(cmd *cobra.Command) (*client.Client, error) { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + return client.New(cfg) +} + +func runSkiptracePeople(cmd *cobra.Command, args []string) error { + c, err := getSkiptraceClient(cmd) + if err != nil { + return err + } + + params := &models.SkiptraceParams{} + params.FirstName, _ = cmd.Flags().GetString("first-name") + params.LastName, _ = cmd.Flags().GetString("last-name") + params.City, _ = cmd.Flags().GetString("city") + params.State, _ = cmd.Flags().GetString("state") + params.Age, _ = cmd.Flags().GetString("age") + + response, err := c.SearchPeople(params) + if err != nil { + return err + } + + if response.Data != nil && len(response.Data) > 0 { + output, err := json.MarshalIndent(response.Data, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } + + return nil +} + +func runSkiptraceReport(cmd *cobra.Command, args []string) error { + c, err := getSkiptraceClient(cmd) + if err != nil { + return err + } + + sxKey := args[0] + selection, err := strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid selection number: %s", args[1]) + } + + response, err := c.GetPersonReport(sxKey, selection) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if response.Data != nil && len(response.Data) > 0 { + output, err := json.MarshalIndent(response.Data, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } + + if response.Message != "" { + fmt.Println(response.Message) + } + + return nil +} + +func runSkiptracePhone(cmd *cobra.Command, args []string) error { + c, err := getSkiptraceClient(cmd) + if err != nil { + return err + } + + response, err := c.SearchPhone(args[0]) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if response.Data != nil && len(response.Data) > 0 { + output, err := json.MarshalIndent(response.Data, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } + + if response.Message != "" { + fmt.Println(response.Message) + } + + return nil +} + +func runSkiptraceEmail(cmd *cobra.Command, args []string) error { + c, err := getSkiptraceClient(cmd) + if err != nil { + return err + } + + response, err := c.SearchEmail(args[0]) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if response.Data != nil && len(response.Data) > 0 { + output, err := json.MarshalIndent(response.Data, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } + + if response.Message != "" { + fmt.Println(response.Message) + } + + return nil +} diff --git a/cmd/sl.go b/cmd/sl.go new file mode 100644 index 0000000..1551681 --- /dev/null +++ b/cmd/sl.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "dborg/internal/models" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +var slCmd = &cobra.Command{ + Use: "sl [query]", + Short: "Search stealer logs", + Long: `Search stealer logs with various filters`, + Args: cobra.ExactArgs(1), + RunE: runSLSearch, +} + +func init() { + rootCmd.AddCommand(slCmd) + slCmd.Flags().IntP("max_hits", "n", 10, "Maximum number of hits to return") + slCmd.Flags().StringP("sort_by", "s", "", "Sort by field (ingest_timestamp or date_posted)") + slCmd.Flags().StringP("ingest_start_date", "i", "", "Ingest timestamp start date") + slCmd.Flags().StringP("ingest_end_date", "e", "", "Ingest timestamp end date") + slCmd.Flags().StringP("posted_start_date", "p", "", "Date posted start date") + slCmd.Flags().StringP("posted_end_date", "d", "", "Date posted end date") + slCmd.Flags().StringP("format", "f", "json", "Response format") +} + +func runSLSearch(cmd *cobra.Command, args []string) error { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + + c, err := client.New(cfg) + if err != nil { + return err + } + + params := &models.SLParams{ + Query: args[0], + } + params.MaxHits, _ = cmd.Flags().GetInt("max_hits") + params.SortBy, _ = cmd.Flags().GetString("sort_by") + params.IngestStartDate, _ = cmd.Flags().GetString("ingest_start_date") + params.IngestEndDate, _ = cmd.Flags().GetString("ingest_end_date") + params.PostedStartDate, _ = cmd.Flags().GetString("posted_start_date") + params.PostedEndDate, _ = cmd.Flags().GetString("posted_end_date") + params.Format, _ = cmd.Flags().GetString("format") + + response, err := c.SearchStealerLogs(params) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if params.Format != "json" { + fmt.Println(response.Message) + return nil + } + + output, err := json.MarshalIndent(response.Results, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + + fmt.Println(string(output)) + return nil +} diff --git a/cmd/x.go b/cmd/x.go new file mode 100644 index 0000000..b4114b5 --- /dev/null +++ b/cmd/x.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "dborg/internal/client" + "dborg/internal/config" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +var xCmd = &cobra.Command{ + Use: "x [username]", + Short: "Search Twitter/X username history", + Long: `Search for Twitter/X username history and previous usernames`, + Args: cobra.ExactArgs(1), + RunE: runXSearch, +} + +func init() { + rootCmd.AddCommand(xCmd) +} + +func runXSearch(cmd *cobra.Command, args []string) error { + apiKey, _ := cmd.Flags().GetString("api-key") + cfg := config.New().WithAPIKey(apiKey) + + c, err := client.New(cfg) + if err != nil { + return err + } + + response, err := c.SearchTwitterHistory(args[0]) + if err != nil { + return err + } + + if response.Error != "" { + return fmt.Errorf("API error: %s", response.Error) + } + + if len(response.PreviousUsernames) > 0 { + output, err := json.MarshalIndent(response.PreviousUsernames, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } else if response.Response != "" { + fmt.Println(response.Response) + } else if response.Data != nil { + output, err := json.MarshalIndent(response.Data, "", " ") + if err != nil { + return fmt.Errorf("failed to format response: %w", err) + } + fmt.Println(string(output)) + } else { + fmt.Println("No username history found") + } + + return nil +} @@ -0,0 +1,10 @@ +module dborg + +go 1.24.4 + +require github.com/spf13/cobra v1.10.1 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/admin.go b/internal/client/admin.go new file mode 100644 index 0000000..a5a9519 --- /dev/null +++ b/internal/client/admin.go @@ -0,0 +1,124 @@ +package client + +import ( + "dborg/internal/models" + "encoding/json" + "fmt" + "net/url" +) + +func (c *Client) ListAccounts() (*models.AdminResponse, error) { + data, err := c.Get("/admin/accounts", nil) + if err != nil { + return nil, err + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} + +func (c *Client) CreateAccount(req *models.AccountCreateRequest) (*models.AdminResponse, error) { + data, err := c.Post("/admin/accounts", req) + if err != nil { + return nil, err + } + + var account models.Account + if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" { + return &models.AdminResponse{ + Success: true, + Account: &account, + }, nil + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} + +func (c *Client) DeleteAccount(apiKey string) (*models.AdminResponse, error) { + path := fmt.Sprintf("/admin/accounts/%s", url.PathEscape(apiKey)) + data, err := c.Delete(path) + if err != nil { + return nil, err + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} + +func (c *Client) UpdateCredits(apiKey string, credits int) (*models.AdminResponse, error) { + path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey)) + req := &models.AddCreditsRequest{ + Credits: credits, + } + + data, err := c.Post(path, req) + if err != nil { + return nil, err + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} + +func (c *Client) SetCredits(apiKey string, credits int) (*models.AdminResponse, error) { + path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey)) + req := &models.SetCreditsRequest{ + Credits: credits, + } + + data, err := c.Put(path, req) + if err != nil { + return nil, err + } + + var account models.Account + if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" { + return &models.AdminResponse{ + Success: true, + Account: &account, + }, nil + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} + +func (c *Client) ToggleAccount(apiKey string, enable bool) (*models.AdminResponse, error) { + path := fmt.Sprintf("/admin/accounts/%s/disable", url.PathEscape(apiKey)) + req := &models.DisableAccountRequest{ + Disabled: !enable, + } + + data, err := c.Patch(path, req) + if err != nil { + return nil, err + } + + var response models.AdminResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse admin response: %w", err) + } + + return &response, nil +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..098479f --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,119 @@ +package client + +import ( + "bytes" + "dborg/internal/config" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +type Client struct { + config *config.Config + httpClient *http.Client +} + +func New(cfg *config.Config) (*Client, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + return &Client{ + config: cfg, + httpClient: &http.Client{ + Timeout: cfg.Timeout, + }, + }, nil +} + +func (c *Client) doRequest(method, path string, params url.Values, body interface{}) ([]byte, error) { + fullURL := c.config.BaseURL + path + if params != nil && len(params) > 0 { + fullURL += "?" + params.Encode() + } + + var reqBody io.Reader + if body != nil { + jsonData, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + reqBody = bytes.NewBuffer(jsonData) + } + + req, err := http.NewRequest(method, fullURL, reqBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-API-Key", c.config.APIKey) + req.Header.Set("User-Agent", c.config.UserAgent) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + var resp *http.Response + var lastErr error + + for attempt := 0; attempt <= c.config.MaxRetries; attempt++ { + if attempt > 0 { + time.Sleep(time.Duration(attempt) * time.Second) + } + + resp, err = c.httpClient.Do(req) + if err != nil { + lastErr = err + continue + } + + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { + return io.ReadAll(resp.Body) + } + + bodyBytes, _ := io.ReadAll(resp.Body) + + switch resp.StatusCode { + case http.StatusForbidden: + lastErr = fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes)) + case http.StatusUnauthorized: + lastErr = fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes)) + case http.StatusTooManyRequests: + lastErr = fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes)) + case http.StatusBadRequest: + lastErr = fmt.Errorf("bad request (400): %s", string(bodyBytes)) + default: + lastErr = fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 { + break + } + } + + return nil, lastErr +} + +func (c *Client) Get(path string, params url.Values) ([]byte, error) { + return c.doRequest(http.MethodGet, path, params, nil) +} + +func (c *Client) Post(path string, body interface{}) ([]byte, error) { + return c.doRequest(http.MethodPost, path, nil, body) +} + +func (c *Client) Delete(path string) ([]byte, error) { + return c.doRequest(http.MethodDelete, path, nil, nil) +} + +func (c *Client) Patch(path string, body interface{}) ([]byte, error) { + return c.doRequest(http.MethodPatch, path, nil, body) +} + +func (c *Client) Put(path string, body interface{}) ([]byte, error) { + return c.doRequest(http.MethodPut, path, nil, body) +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..9bf453d --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,46 @@ +package client + +import ( + "dborg/internal/config" + "testing" + "time" +) + +func TestNewClient(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantErr bool + }{ + { + name: "valid config", + config: &config.Config{ + APIKey: "test-key", + BaseURL: "https://db.org.ai", + Timeout: 30 * time.Second, + MaxRetries: 3, + UserAgent: "test-agent", + }, + wantErr: false, + }, + { + name: "missing API key", + config: &config.Config{ + BaseURL: "https://db.org.ai", + Timeout: 30 * time.Second, + MaxRetries: 3, + UserAgent: "test-agent", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := New(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/client/npd.go b/internal/client/npd.go new file mode 100644 index 0000000..c63327b --- /dev/null +++ b/internal/client/npd.go @@ -0,0 +1,91 @@ +package client + +import ( + "dborg/internal/models" + "encoding/json" + "fmt" + "net/url" +) + +func (c *Client) SearchNPD(params *models.NPDParams) (*models.NPDResponse, error) { + queryParams := url.Values{} + + if params.ID != "" { + queryParams.Add("id", params.ID) + } + if params.FirstName != "" { + queryParams.Add("firstname", params.FirstName) + } + if params.LastName != "" { + queryParams.Add("lastname", params.LastName) + } + if params.MiddleName != "" { + queryParams.Add("middlename", params.MiddleName) + } + if params.DOB != "" { + queryParams.Add("dob", params.DOB) + } + if params.SSN != "" { + queryParams.Add("ssn", params.SSN) + } + if params.Phone1 != "" { + queryParams.Add("phone1", params.Phone1) + } + if params.Address != "" { + queryParams.Add("address", params.Address) + } + if params.City != "" { + queryParams.Add("city", params.City) + } + if params.State != "" { + queryParams.Add("st", params.State) + } + if params.Zip != "" { + queryParams.Add("zip", params.Zip) + } + if params.CountyName != "" { + queryParams.Add("county_name", params.CountyName) + } + if params.NameSuffix != "" { + queryParams.Add("name_suff", params.NameSuffix) + } + if params.AKA1FullName != "" { + queryParams.Add("aka1fullname", params.AKA1FullName) + } + if params.AKA2FullName != "" { + queryParams.Add("aka2fullname", params.AKA2FullName) + } + if params.AKA3FullName != "" { + queryParams.Add("aka3fullname", params.AKA3FullName) + } + if params.Alt1DOB != "" { + queryParams.Add("alt1dob", params.Alt1DOB) + } + if params.Alt2DOB != "" { + queryParams.Add("alt2dob", params.Alt2DOB) + } + if params.Alt3DOB != "" { + queryParams.Add("alt3dob", params.Alt3DOB) + } + if params.StartDate != "" { + queryParams.Add("startdat", params.StartDate) + } + if params.MaxHits > 0 && params.MaxHits != 10 { + queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits)) + } + if params.SortBy != "" { + queryParams.Add("sort_by", params.SortBy) + } + + data, err := c.Get("/npd/search", queryParams) + if err != nil { + return nil, err + } + + var response models.NPDResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse NPD response: %w", err) + } + + return &response, nil +} diff --git a/internal/client/osint.go b/internal/client/osint.go new file mode 100644 index 0000000..70dbb72 --- /dev/null +++ b/internal/client/osint.go @@ -0,0 +1,32 @@ +package client + +import ( + "dborg/internal/models" + "encoding/json" + "fmt" + "net/url" +) + +func (c *Client) LookupBSSID(params *models.BSSIDParams) (*models.BSSIDLookupResponse, error) { + path := fmt.Sprintf("/osint/bssid/%s", url.PathEscape(params.BSSID)) + + queryParams := url.Values{} + if params.All { + queryParams.Add("all", "true") + } + if params.Map { + queryParams.Add("map", "true") + } + + data, err := c.Get(path, queryParams) + if err != nil { + return nil, err + } + + var response models.BSSIDLookupResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse BSSID lookup response: %w", err) + } + + return &response, nil +} diff --git a/internal/client/skiptrace.go b/internal/client/skiptrace.go new file mode 100644 index 0000000..b1d5008 --- /dev/null +++ b/internal/client/skiptrace.go @@ -0,0 +1,179 @@ +package client + +import ( + "bufio" + "bytes" + "dborg/internal/models" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func parseSSEResponse(data []byte) ([]byte, error) { + scanner := bufio.NewScanner(bytes.NewReader(data)) + + const maxScanTokenSize = 10 * 1024 * 1024 + buf := make([]byte, maxScanTokenSize) + scanner.Buffer(buf, maxScanTokenSize) + + var resultData []byte + var foundResult bool + + for scanner.Scan() { + line := scanner.Text() + + if line == "event: result" { + foundResult = true + continue + } + + if foundResult && strings.HasPrefix(line, "data: ") { + resultData = []byte(strings.TrimPrefix(line, "data: ")) + break + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading SSE response: %w", err) + } + + if resultData == nil { + return nil, fmt.Errorf("no result event found in SSE response") + } + + trimmed := strings.TrimSpace(string(resultData)) + if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") { + return nil, fmt.Errorf("API returned: %s", trimmed) + } + + return resultData, nil +} + +func (c *Client) getSSE(path string, params url.Values) ([]byte, error) { + fullURL := c.config.BaseURL + path + if params != nil && len(params) > 0 { + fullURL += "?" + params.Encode() + } + + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-API-Key", c.config.APIKey) + req.Header.Set("User-Agent", c.config.UserAgent) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + switch resp.StatusCode { + case http.StatusForbidden: + return nil, fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes)) + case http.StatusUnauthorized: + return nil, fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes)) + case http.StatusTooManyRequests: + return nil, fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes)) + case http.StatusBadRequest: + return nil, fmt.Errorf("bad request (400): %s", string(bodyBytes)) + default: + return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + contentType := resp.Header.Get("Content-Type") + + if strings.HasPrefix(string(data), "event:") || strings.Contains(contentType, "text/event-stream") { + return parseSSEResponse(data) + } + + return data, nil +} + +func (c *Client) SearchPeople(params *models.SkiptraceParams) (*models.SkiptraceResponse, error) { + queryParams := url.Values{} + queryParams.Set("first_name", params.FirstName) + queryParams.Set("last_name", params.LastName) + + if params.City != "" { + queryParams.Set("city", params.City) + } + if params.State != "" { + queryParams.Set("state", params.State) + } + if params.Age != "" { + queryParams.Set("age", params.Age) + } + + data, err := c.getSSE("/prem/skiptrace/people/search", queryParams) + if err != nil { + return nil, err + } + + var response models.SkiptraceResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +func (c *Client) GetPersonReport(sxKey string, selection int) (*models.SkiptraceReportResponse, error) { + path := fmt.Sprintf("/prem/skiptrace/people/report/%s/%d", sxKey, selection) + + data, err := c.getSSE(path, nil) + if err != nil { + return nil, err + } + + var response models.SkiptraceReportResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +func (c *Client) SearchPhone(phone string) (*models.SkiptracePhoneResponse, error) { + path := fmt.Sprintf("/prem/skiptrace/phone/%s", phone) + + data, err := c.getSSE(path, nil) + if err != nil { + return nil, err + } + + var response models.SkiptracePhoneResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +func (c *Client) SearchEmail(email string) (*models.SkiptraceEmailResponse, error) { + path := fmt.Sprintf("/prem/skiptrace/email/%s", email) + + data, err := c.getSSE(path, nil) + if err != nil { + return nil, err + } + + var response models.SkiptraceEmailResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} diff --git a/internal/client/sl.go b/internal/client/sl.go new file mode 100644 index 0000000..fb3b270 --- /dev/null +++ b/internal/client/sl.go @@ -0,0 +1,53 @@ +package client + +import ( + "dborg/internal/models" + "encoding/json" + "fmt" + "net/url" +) + +func (c *Client) SearchStealerLogs(params *models.SLParams) (*models.SLResponse, error) { + queryParams := url.Values{} + queryParams.Add("query", params.Query) + + if params.MaxHits > 0 && params.MaxHits != 10 { + queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits)) + } + if params.SortBy != "" { + queryParams.Add("sort_by", params.SortBy) + } + if params.IngestStartDate != "" { + queryParams.Add("ingest_start_date", params.IngestStartDate) + } + if params.IngestEndDate != "" { + queryParams.Add("ingest_end_date", params.IngestEndDate) + } + if params.PostedStartDate != "" { + queryParams.Add("posted_start_date", params.PostedStartDate) + } + if params.PostedEndDate != "" { + queryParams.Add("posted_end_date", params.PostedEndDate) + } + if params.Format != "" && params.Format != "json" { + queryParams.Add("format", params.Format) + } + + data, err := c.Get("/sl/search", queryParams) + if err != nil { + return nil, err + } + + if params.Format != "" && params.Format != "json" { + return &models.SLResponse{ + Message: string(data), + }, nil + } + + var response models.SLResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse stealer logs response: %w", err) + } + + return &response, nil +} diff --git a/internal/client/usrsx.go b/internal/client/usrsx.go new file mode 100644 index 0000000..456acbf --- /dev/null +++ b/internal/client/usrsx.go @@ -0,0 +1,72 @@ +package client + +import ( + "bufio" + "dborg/internal/models" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func (c *Client) CheckUsernameStream(params *models.USRSXParams, callback func(result json.RawMessage) error) error { + queryParams := url.Values{} + + if len(params.Sites) > 0 { + queryParams.Add("sites", strings.Join(params.Sites, ",")) + } + if params.Fuzzy { + queryParams.Add("fuzzy", "true") + } + if params.MaxTasks > 0 && params.MaxTasks != 50 { + queryParams.Add("max_tasks", fmt.Sprintf("%d", params.MaxTasks)) + } + + path := fmt.Sprintf("/osint/username/%s", url.PathEscape(params.Username)) + fullURL := c.config.BaseURL + path + if len(queryParams) > 0 { + fullURL += "?" + queryParams.Encode() + } + + req, err := http.NewRequest(http.MethodGet, fullURL, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-API-Key", c.config.APIKey) + req.Header.Set("User-Agent", c.config.UserAgent) + req.Header.Set("Accept", "application/x-ndjson, application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + if err := callback(json.RawMessage(line)); err != nil { + return err + } + } + + if err := scanner.Err(); err != nil { + if !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "timeout") { + return fmt.Errorf("stream reading error: %w", err) + } + } + + return nil +} diff --git a/internal/client/x.go b/internal/client/x.go new file mode 100644 index 0000000..8bdb21c --- /dev/null +++ b/internal/client/x.go @@ -0,0 +1,23 @@ +package client + +import ( + "dborg/internal/models" + "encoding/json" + "fmt" + "net/url" +) + +func (c *Client) SearchTwitterHistory(username string) (*models.XResponse, error) { + path := fmt.Sprintf("/x/search/%s", url.PathEscape(username)) + data, err := c.Get(path, nil) + if err != nil { + return nil, err + } + + var response models.XResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse Twitter/X response: %w", err) + } + + return &response, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..44ca7e6 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "os" + "time" +) + +type Config struct { + APIKey string + BaseURL string + Timeout time.Duration + MaxRetries int + UserAgent string +} + +func New() *Config { + return &Config{ + APIKey: os.Getenv("DBORG_API_KEY"), + BaseURL: "https://db.org.ai", + Timeout: 30 * time.Second, + MaxRetries: 3, + UserAgent: "dborg-cli/1.0", + } +} + +func (c *Config) WithAPIKey(key string) *Config { + c.APIKey = key + return c +} + +func (c *Config) Validate() error { + if c.APIKey == "" { + return ErrMissingAPIKey + } + return nil +} diff --git a/internal/config/errors.go b/internal/config/errors.go new file mode 100644 index 0000000..4fd3636 --- /dev/null +++ b/internal/config/errors.go @@ -0,0 +1,7 @@ +package config + +import "errors" + +var ( + ErrMissingAPIKey = errors.New("API key required: set DBORG_API_KEY environment variable or use --api-key flag") +) diff --git a/internal/models/admin.go b/internal/models/admin.go new file mode 100644 index 0000000..5cf0f37 --- /dev/null +++ b/internal/models/admin.go @@ -0,0 +1,43 @@ +package models + +type Account struct { + APIKey string `json:"api_key"` + Name string `json:"name"` + Credits int `json:"credits"` + Unlimited bool `json:"unlimited"` + Disabled bool `json:"disabled"` + IsPremium bool `json:"is_premium"` + CreatedAt interface{} `json:"created_at,omitempty"` +} + +type AccountCreateRequest struct { + Name string `json:"name"` + Credits int `json:"credits,omitempty"` + Unlimited bool `json:"unlimited,omitempty"` + IsPremium bool `json:"is_premium,omitempty"` +} + +type AccountUpdateRequest struct { + Credits int `json:"credits,omitempty"` + Disabled bool `json:"disabled"` +} + +type AddCreditsRequest struct { + Credits int `json:"credits"` +} + +type SetCreditsRequest struct { + Credits int `json:"credits"` +} + +type DisableAccountRequest struct { + Disabled bool `json:"disabled"` +} + +type AdminResponse struct { + Success bool `json:"success,omitempty"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + Account *Account `json:"account,omitempty"` + Accounts []Account `json:"accounts,omitempty"` +} diff --git a/internal/models/npd.go b/internal/models/npd.go new file mode 100644 index 0000000..a1da05e --- /dev/null +++ b/internal/models/npd.go @@ -0,0 +1,42 @@ +package models + +type NPDParams struct { + ID string `json:"id,omitempty"` + FirstName string `json:"firstname,omitempty"` + LastName string `json:"lastname,omitempty"` + MiddleName string `json:"middlename,omitempty"` + DOB string `json:"dob,omitempty"` + SSN string `json:"ssn,omitempty"` + Phone1 string `json:"phone1,omitempty"` + Address string `json:"address,omitempty"` + City string `json:"city,omitempty"` + State string `json:"st,omitempty"` + Zip string `json:"zip,omitempty"` + CountyName string `json:"county_name,omitempty"` + NameSuffix string `json:"name_suff,omitempty"` + AKA1FullName string `json:"aka1fullname,omitempty"` + AKA2FullName string `json:"aka2fullname,omitempty"` + AKA3FullName string `json:"aka3fullname,omitempty"` + Alt1DOB string `json:"alt1dob,omitempty"` + Alt2DOB string `json:"alt2dob,omitempty"` + Alt3DOB string `json:"alt3dob,omitempty"` + StartDate string `json:"startdat,omitempty"` + MaxHits int `json:"max_hits,omitempty"` + SortBy string `json:"sort_by,omitempty"` +} + +type NPDResponse struct { + MaxHits int `json:"max_hits"` + Results struct { + ElapsedTimeMicros int `json:"elapsed_time_micros"` + Errors []string `json:"errors"` + Hits []map[string]any `json:"hits"` + NumHits int `json:"num_hits"` + } `json:"results"` + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/internal/models/osint.go b/internal/models/osint.go new file mode 100644 index 0000000..7170c27 --- /dev/null +++ b/internal/models/osint.go @@ -0,0 +1,29 @@ +package models + +type BSSIDParams struct { + BSSID string + All bool + Map bool +} + +type LocationInfo struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Accuracy int `json:"accuracy"` +} + +type BSSIDResult struct { + BSSID string `json:"bssid"` + Location *LocationInfo `json:"location"` + MapURL string `json:"map_url,omitempty"` +} + +type BSSIDLookupResponse struct { + BSSID string `json:"bssid"` + Results []BSSIDResult `json:"results"` + MapURL string `json:"map_url,omitempty"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} diff --git a/internal/models/skiptrace.go b/internal/models/skiptrace.go new file mode 100644 index 0000000..c87fe72 --- /dev/null +++ b/internal/models/skiptrace.go @@ -0,0 +1,92 @@ +package models + +import "encoding/json" + +type SkiptraceParams struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Age string `json:"age,omitempty"` +} + +type SkiptraceResponse struct { + Data map[string]interface{} `json:"-"` + SXKey string `json:"sx_key,omitempty"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +func (s *SkiptraceResponse) UnmarshalJSON(data []byte) error { + type Alias SkiptraceResponse + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + s.Data = make(map[string]interface{}) + return json.Unmarshal(data, &s.Data) +} + +type SkiptraceReportResponse struct { + Data map[string]interface{} `json:"-"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +func (s *SkiptraceReportResponse) UnmarshalJSON(data []byte) error { + type Alias SkiptraceReportResponse + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + s.Data = make(map[string]interface{}) + return json.Unmarshal(data, &s.Data) +} + +type SkiptracePhoneResponse struct { + Data map[string]interface{} `json:"-"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +func (s *SkiptracePhoneResponse) UnmarshalJSON(data []byte) error { + type Alias SkiptracePhoneResponse + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + s.Data = make(map[string]interface{}) + return json.Unmarshal(data, &s.Data) +} + +type SkiptraceEmailResponse struct { + Data map[string]interface{} `json:"-"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +func (s *SkiptraceEmailResponse) UnmarshalJSON(data []byte) error { + type Alias SkiptraceEmailResponse + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + s.Data = make(map[string]interface{}) + return json.Unmarshal(data, &s.Data) +} diff --git a/internal/models/sl.go b/internal/models/sl.go new file mode 100644 index 0000000..d55279f --- /dev/null +++ b/internal/models/sl.go @@ -0,0 +1,23 @@ +package models + +type SLParams struct { + Query string `json:"query"` + MaxHits int `json:"max_hits,omitempty"` + SortBy string `json:"sort_by,omitempty"` + IngestStartDate string `json:"ingest_start_date,omitempty"` + IngestEndDate string `json:"ingest_end_date,omitempty"` + PostedStartDate string `json:"posted_start_date,omitempty"` + PostedEndDate string `json:"posted_end_date,omitempty"` + Format string `json:"format,omitempty"` +} + +type SLResponse struct { + MaxHits int `json:"max_hits"` + Results interface{} `json:"results"` + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/internal/models/usrsx.go b/internal/models/usrsx.go new file mode 100644 index 0000000..f9264be --- /dev/null +++ b/internal/models/usrsx.go @@ -0,0 +1,33 @@ +package models + +type USRSXParams struct { + Username string `json:"username"` + Sites []string `json:"sites,omitempty"` + Fuzzy bool `json:"fuzzy,omitempty"` + MaxTasks int `json:"max_tasks,omitempty"` +} + +type USRSXResponse struct { + Username string `json:"username"` + Results []SiteResult `json:"results"` + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +type SiteResult struct { + SiteName string `json:"site_name"` + Username string `json:"username"` + URL string `json:"url"` + Status string `json:"status"` + ResponseCode int `json:"response_code"` + Category string `json:"category,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Type string `json:"type,omitempty"` + Elapsed float64 `json:"elapsed,omitempty"` + Error string `json:"error,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/internal/models/x.go b/internal/models/x.go new file mode 100644 index 0000000..f8c7a70 --- /dev/null +++ b/internal/models/x.go @@ -0,0 +1,22 @@ +package models + +type XResponse struct { + Username string `json:"username,omitempty"` + PreviousUsernames []UserHistory `json:"previous_usernames,omitempty"` + + Query string `json:"query,omitempty"` + Response string `json:"response,omitempty"` + Data interface{} `json:"data,omitempty"` + + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` +} + +type UserHistory struct { + Username string `json:"username"` + TimeAgo string `json:"time_ago"` +} diff --git a/internal/utils/output.go b/internal/utils/output.go new file mode 100644 index 0000000..3f2347c --- /dev/null +++ b/internal/utils/output.go @@ -0,0 +1,39 @@ +package utils + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" +) + +func PrintJSON(data any) error { + output, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to format JSON: %w", err) + } + fmt.Println(string(output)) + return nil +} + +func PrintTable(headers []string, rows [][]string) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + defer w.Flush() + + for _, h := range headers { + fmt.Fprintf(w, "%s\t", h) + } + fmt.Fprintln(w) + + for _, row := range rows { + for _, col := range row { + fmt.Fprintf(w, "%s\t", col) + } + fmt.Fprintln(w) + } +} + +func PrintError(err error) { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) +} @@ -0,0 +1,7 @@ +package main + +import "dborg/cmd" + +func main() { + cmd.Execute() +} |
