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 | 923d6aece0b508c303393b23e8f605d63f46835f (patch) | |
| tree | 65b629c84a20d9cb1f34ba16797dbbe8861a7a91 | |
| download | dborg-923d6aece0b508c303393b23e8f605d63f46835f.tar.gz dborg-923d6aece0b508c303393b23e8f605d63f46835f.zip | |
hi
33 files changed, 3595 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 +} diff --git a/docs/doc.json b/docs/doc.json new file mode 100644 index 0000000..306d077 --- /dev/null +++ b/docs/doc.json @@ -0,0 +1,1444 @@ +{ + "schemes": [], + "swagger": "2.0", + "info": { + "description": "API server for DB.org.ai services", + "title": "DB.org.ai API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.db.org.ai/support", + "email": "[email protected]" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "db.org.ai", + "basePath": "/", + "paths": { + "/admin/accounts": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a list of all accounts", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/main.AccountResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create a new user account with API key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create account", + "parameters": [ + { + "description": "Account details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.CreateAccountRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.AccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/admin/accounts/{api_key}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete an account by API key", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete account", + "parameters": [ + { + "type": "string", + "description": "Account API Key", + "name": "api_key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/admin/accounts/{api_key}/credits": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Set account credits to a specific amount", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Set credits", + "parameters": [ + { + "type": "string", + "description": "Account API Key", + "name": "api_key", + "in": "path", + "required": true + }, + { + "description": "Credits amount to set", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.SetCreditsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.AccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add credits to an existing account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add credits", + "parameters": [ + { + "type": "string", + "description": "Account API Key", + "name": "api_key", + "in": "path", + "required": true + }, + { + "description": "Credits to add", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.AddCreditsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.AccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/admin/accounts/{api_key}/disable": { + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Disable or enable an account by setting the disabled flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Disable/Enable account", + "parameters": [ + { + "type": "string", + "description": "Account API Key", + "name": "api_key", + "in": "path", + "required": true + }, + { + "description": "Disable status", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.DisableAccountRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.AccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/npd/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Search NPD breach data by various fields", + "produces": [ + "application/json" + ], + "tags": [ + "npd" + ], + "summary": "Search NPD breach data", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "query" + }, + { + "type": "string", + "description": "First name", + "name": "firstname", + "in": "query" + }, + { + "type": "string", + "description": "Last name", + "name": "lastname", + "in": "query" + }, + { + "type": "string", + "description": "Middle name", + "name": "middlename", + "in": "query" + }, + { + "type": "string", + "description": "Date of birth", + "name": "dob", + "in": "query" + }, + { + "type": "string", + "description": "Social security number", + "name": "ssn", + "in": "query" + }, + { + "type": "string", + "description": "Phone number", + "name": "phone1", + "in": "query" + }, + { + "type": "string", + "description": "Address", + "name": "address", + "in": "query" + }, + { + "type": "string", + "description": "City", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "State", + "name": "st", + "in": "query" + }, + { + "type": "string", + "description": "ZIP code", + "name": "zip", + "in": "query" + }, + { + "type": "string", + "description": "County name", + "name": "county_name", + "in": "query" + }, + { + "type": "string", + "description": "Name suffix", + "name": "name_suff", + "in": "query" + }, + { + "type": "string", + "description": "AKA 1 full name", + "name": "aka1fullname", + "in": "query" + }, + { + "type": "string", + "description": "AKA 2 full name", + "name": "aka2fullname", + "in": "query" + }, + { + "type": "string", + "description": "AKA 3 full name", + "name": "aka3fullname", + "in": "query" + }, + { + "type": "string", + "description": "Alternate DOB 1", + "name": "alt1dob", + "in": "query" + }, + { + "type": "string", + "description": "Alternate DOB 2", + "name": "alt2dob", + "in": "query" + }, + { + "type": "string", + "description": "Alternate DOB 3", + "name": "alt3dob", + "in": "query" + }, + { + "type": "string", + "description": "Start date", + "name": "startdat", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Maximum number of hits to return", + "name": "max_hits", + "in": "query" + }, + { + "type": "string", + "description": "Sort by field", + "name": "sort_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.NPDSearchResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/db_org_ai_services_npd_api.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/db_org_ai_services_npd_api.ErrorResponse" + } + } + } + } + }, + "/osint/bssid/{bssid}": { + "get": { + "description": "Lookup geographic location of a WiFi access point by its BSSID (MAC address) using Apple's location services. Returns latitude/longitude coordinates.", + "produces": [ + "application/json" + ], + "tags": [ + "osint" + ], + "summary": "BSSID Location Lookup", + "parameters": [ + { + "type": "string", + "description": "BSSID/MAC address (format: aa:bb:cc:dd:ee:ff)", + "name": "bssid", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Show all related results instead of exact match only (alias for 'all')", + "name": "a", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Show all related results instead of exact match only", + "name": "all", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Include Google Maps URL for the location", + "name": "map", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.BSSIDLookupResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/db_org_ai_services_bssidx_api.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/db_org_ai_services_bssidx_api.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/db_org_ai_services_bssidx_api.ErrorResponse" + } + } + } + } + }, + "/osint/username/{username}": { + "get": { + "description": "Check username availability across hundreds of websites using WhatsMyName dataset. Results are streamed as NDJSON (newline-delimited JSON) with each result containing the full profile URL.", + "produces": [ + "application/x-ndjson" + ], + "tags": [ + "osint" + ], + "summary": "Check username availability", + "parameters": [ + { + "type": "string", + "description": "Username to check", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Specific sites to check (comma-separated)", + "name": "sites", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Enable fuzzy validation mode", + "name": "fuzzy", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "Maximum concurrent tasks", + "name": "max_tasks", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Stream of NDJSON results, each containing full profile URL in 'url' field", + "schema": { + "$ref": "#/definitions/api.SiteResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/db_org_ai_services_usrsx_api.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/db_org_ai_services_usrsx_api.ErrorResponse" + } + } + } + } + }, + "/prem/skiptrace/email/{email}": { + "get": { + "tags": [ + "skiptrace" + ], + "summary": "Search for email address", + "parameters": [ + { + "type": "string", + "description": "Email address", + "name": "email", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "API Key (Premium Required)", + "name": "X-API-Key", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Email report data", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "Premium access required", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "429": { + "description": "Rate limit exceeded", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/prem/skiptrace/people/report/{sx_key}/{selection}": { + "get": { + "tags": [ + "skiptrace" + ], + "summary": "Get detailed report for selected person", + "parameters": [ + { + "type": "string", + "description": "Search session key", + "name": "sx_key", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Person selection (1-based index)", + "name": "selection", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "API Key (Premium Required)", + "name": "X-API-Key", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Detailed person report", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "Premium access required", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "429": { + "description": "Rate limit exceeded", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/prem/skiptrace/people/search": { + "get": { + "tags": [ + "skiptrace" + ], + "summary": "Search for people by name", + "parameters": [ + { + "type": "string", + "description": "First name", + "name": "first_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Last name", + "name": "last_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "City", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "State (2-letter code)", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Age", + "name": "age", + "in": "query" + }, + { + "type": "string", + "description": "API Key (Premium Required)", + "name": "X-API-Key", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Search results with sx_key", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "Premium access required", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/prem/skiptrace/phone/{phone}": { + "get": { + "tags": [ + "skiptrace" + ], + "summary": "Search for phone number", + "parameters": [ + { + "type": "string", + "description": "Phone number (10 digits, no +1 prefix)", + "name": "phone", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "API Key (Premium Required)", + "name": "X-API-Key", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Phone report data", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "403": { + "description": "Premium access required", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "429": { + "description": "Rate limit exceeded", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/sl/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Search stealer logs", + "produces": [ + "application/json", + "text/plain" + ], + "tags": [ + "logs" + ], + "summary": "Search stealer logs", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Maximum number of hits to return", + "name": "max_hits", + "in": "query" + }, + { + "enum": [ + "ingest_timestamp", + "date_posted" + ], + "type": "string", + "description": "Sort by field (ingest_timestamp or date_posted)", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Ingest timestamp start date (Quickwit date format, e.g., 'now-10d', '2025-01-01T00:00:00Z')", + "name": "ingest_start_date", + "in": "query" + }, + { + "type": "string", + "description": "Ingest timestamp end date (Quickwit date format, e.g., 'now', '2025-12-31T23:59:59Z')", + "name": "ingest_end_date", + "in": "query" + }, + { + "type": "string", + "description": "Date posted start date (Quickwit date format, e.g., 'now-10d', '2025-01-01T00:00:00Z')", + "name": "posted_start_date", + "in": "query" + }, + { + "type": "string", + "description": "Date posted end date (Quickwit date format, e.g., 'now', '2025-12-31T23:59:59Z')", + "name": "posted_end_date", + "in": "query" + }, + { + "type": "string", + "default": "json", + "description": "Response format: json (default) or custom format like 'ulp' (url:username:password), 'up' (url:password), 'pul' (password:url), etc.", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.QuickwitSearchResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/db_org_ai_services_sl_api.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/db_org_ai_services_sl_api.ErrorResponse" + } + } + } + } + }, + "/x/search/{username}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Search for Twitter/X username history and previous usernames", + "produces": [ + "application/json" + ], + "tags": [ + "x" + ], + "summary": "Search username history", + "parameters": [ + { + "type": "string", + "description": "Twitter/X username to search", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.SearchResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/db_org_ai_services_x_api.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/db_org_ai_services_x_api.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "api.BSSIDLookupResponse": { + "type": "object", + "properties": { + "bssid": { + "type": "string" + }, + "map_url": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/api.BSSIDResult" + } + } + } + }, + "api.BSSIDResult": { + "type": "object", + "properties": { + "bssid": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/api.LocationInfo" + }, + "map_url": { + "type": "string" + } + } + }, + "api.LocationInfo": { + "type": "object", + "properties": { + "accuracy": { + "type": "integer" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + } + } + }, + "api.NPDSearchResponse": { + "type": "object", + "properties": { + "credits": { + "$ref": "#/definitions/db_org_ai_services_npd_api.CreditsInfo" + }, + "max_hits": { + "type": "integer" + }, + "results": {} + } + }, + "api.ProfileMetadata": { + "type": "object", + "properties": { + "additional_links": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "avatar_url": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "custom_fields": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "display_name": { + "type": "string" + }, + "follower_count": { + "type": "integer" + }, + "following_count": { + "type": "integer" + }, + "is_verified": { + "type": "boolean" + }, + "join_date": { + "type": "string" + }, + "location": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "api.QuickwitSearchResponse": { + "type": "object", + "properties": { + "credits": { + "$ref": "#/definitions/db_org_ai_services_sl_api.CreditsInfo" + }, + "max_hits": { + "type": "integer" + }, + "results": {} + } + }, + "api.SearchResponse": { + "type": "object", + "properties": { + "credits": { + "$ref": "#/definitions/db_org_ai_services_x_api.CreditsInfo" + }, + "data": {}, + "query": { + "type": "string", + "example": "elonmusk" + }, + "response": { + "type": "string", + "example": "Username history found..." + } + } + }, + "api.SiteResult": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "elapsed": { + "type": "number" + }, + "error": { + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/api.ProfileMetadata" + }, + "response_code": { + "type": "integer" + }, + "site_name": { + "type": "string" + }, + "status": { + "description": "Changed from result_status to match CLI output", + "type": "string" + }, + "timestamp": { + "description": "Changed from created_at to match CLI output", + "type": "string" + }, + "type": { + "description": "Added type field from CLI output", + "type": "string" + }, + "url": { + "description": "Changed from result_url to match CLI output - contains full profile URL", + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "api.TwitterUsernameHistory": { + "type": "object", + "properties": { + "credits": { + "$ref": "#/definitions/db_org_ai_services_x_api.CreditsInfo" + }, + "previous_usernames": { + "type": "array", + "items": { + "$ref": "#/definitions/api.UsernameEntry" + } + }, + "username": { + "type": "string", + "example": "elonmusk" + } + } + }, + "api.UsernameEntry": { + "type": "object", + "properties": { + "time_ago": { + "type": "string", + "example": "2 days ago" + }, + "username": { + "type": "string", + "example": "elonmusk" + } + } + }, + "db_org_ai_services_bssidx_api.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "db_org_ai_services_npd_api.CreditsInfo": { + "type": "object", + "properties": { + "remaining": { + "type": "integer" + }, + "unlimited": { + "type": "boolean" + } + } + }, + "db_org_ai_services_npd_api.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "db_org_ai_services_sl_api.CreditsInfo": { + "type": "object", + "properties": { + "remaining": { + "type": "integer" + }, + "unlimited": { + "type": "boolean" + } + } + }, + "db_org_ai_services_sl_api.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "db_org_ai_services_usrsx_api.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "db_org_ai_services_x_api.CreditsInfo": { + "type": "object", + "properties": { + "remaining": { + "type": "integer", + "example": 999 + }, + "unlimited": { + "type": "boolean", + "example": false + } + } + }, + "db_org_ai_services_x_api.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Invalid request" + } + } + }, + "main.AccountResponse": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "example": "abc123..." + }, + "credits": { + "type": "integer", + "example": 1000 + }, + "disabled": { + "type": "boolean", + "example": false + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_admin": { + "type": "boolean", + "example": false + }, + "is_premium": { + "type": "boolean", + "example": false + }, + "name": { + "type": "string", + "example": "john_doe" + }, + "unlimited": { + "type": "boolean", + "example": false + } + } + }, + "main.AddCreditsRequest": { + "type": "object", + "required": [ + "credits" + ], + "properties": { + "credits": { + "type": "integer", + "example": 500 + } + } + }, + "main.CreateAccountRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "credits": { + "type": "integer", + "example": 1000 + }, + "is_premium": { + "type": "boolean", + "example": false + }, + "name": { + "type": "string", + "example": "john_doe" + }, + "unlimited": { + "type": "boolean", + "example": false + } + } + }, + "main.DisableAccountRequest": { + "type": "object", + "required": [ + "disabled" + ], + "properties": { + "disabled": { + "type": "boolean", + "example": true + } + } + }, + "main.SetCreditsRequest": { + "type": "object", + "required": [ + "credits" + ], + "properties": { + "credits": { + "type": "integer", + "example": 1000 + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + } + } +}
\ No newline at end of file @@ -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() +} |
