summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors <[email protected]>2025-11-10 15:22:32 -0500
committers <[email protected]>2025-11-10 15:22:32 -0500
commit8383a241fc3cf5b022c9c53f8f19690edf04177b (patch)
tree887a489f7931d07373530c7e053f0343dca65e1d
parent9a9e79f232b83d3bd2a816287272515863df1299 (diff)
downloaddborg-8383a241fc3cf5b022c9c53f8f19690edf04177b.tar.gz
dborg-8383a241fc3cf5b022c9c53f8f19690edf04177b.zip
refactor: restructure client modules and add config file supportv0.8.1
- Split large osint.go client into focused modules (bssid.go, breachforum.go, buckets.go, etc.) - Add config file support with init command for API key management - Remove api-key flag in favor of config file + env var fallback - Update API paths to remove /osint prefix - Add crawl endpoint streaming support - Improve error handling with 402 payment required status
-rw-r--r--README.md16
-rw-r--r--cmd/admin.go3
-rw-r--r--cmd/init.go59
-rw-r--r--cmd/npd.go3
-rw-r--r--cmd/osint.go24
-rw-r--r--cmd/reddit.go15
-rw-r--r--cmd/root.go1
-rw-r--r--cmd/skiptrace.go3
-rw-r--r--cmd/sl.go3
-rw-r--r--cmd/x.go15
-rw-r--r--internal/client/breachforum.go30
-rw-r--r--internal/client/bssid.go35
-rw-r--r--internal/client/buckets.go65
-rw-r--r--internal/client/crawl.go50
-rw-r--r--internal/client/files.go41
-rw-r--r--internal/client/geo.go30
-rw-r--r--internal/client/osint.go208
-rw-r--r--internal/client/shortlinks.go47
-rw-r--r--internal/client/skiptrace.go2
-rw-r--r--internal/client/usrsx.go2
-rw-r--r--internal/config/config.go71
-rw-r--r--internal/config/errors.go2
-rw-r--r--internal/models/breachforum.go12
-rw-r--r--internal/models/bssid.go23
-rw-r--r--internal/models/buckets.go29
-rw-r--r--internal/models/files.go12
-rw-r--r--internal/models/geo.go10
-rw-r--r--internal/models/osint.go101
-rw-r--r--internal/models/shortlinks.go16
29 files changed, 563 insertions, 365 deletions
diff --git a/README.md b/README.md
index c83c2d5..b322f35 100644
--- a/README.md
+++ b/README.md
@@ -85,18 +85,26 @@ dborg version
## Configuration
-Set your API key:
+### Initial Setup
+
+Run the initialization command to set up your API key:
```bash
-export DBORG_API_KEY=your_api_key_here
+dborg init
```
-Or pass it with each command:
+This will prompt you to enter your API key and save it to `~/.config/dborg/config.json`.
+
+### Alternative Configuration Methods
+
+You can also set your API key via environment variable:
```bash
-dborg --api-key YOUR_KEY [command]
+export DBORG_API_KEY=your_api_key_here
```
+**Note:** Environment variables take precedence over the config file.
+
## Commands
### NPD - Search NPD breach data
diff --git a/cmd/admin.go b/cmd/admin.go
index e0f0653..82e2c29 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -74,8 +74,7 @@ func init() {
}
func getAdminClient(cmd *cobra.Command) (*client.Client, error) {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
return client.New(cfg)
}
diff --git a/cmd/init.go b/cmd/init.go
new file mode 100644
index 0000000..954dd31
--- /dev/null
+++ b/cmd/init.go
@@ -0,0 +1,59 @@
+package cmd
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "git.db.org.ai/dborg/internal/config"
+ "github.com/spf13/cobra"
+)
+
+var initCmd = &cobra.Command{
+ Use: "init",
+ Short: "Initialize dborg CLI configuration",
+ Long: `Initialize the dborg CLI by setting up your API key and other configuration`,
+ RunE: runInit,
+}
+
+func init() {
+ rootCmd.AddCommand(initCmd)
+}
+
+func runInit(cmd *cobra.Command, args []string) error {
+ reader := bufio.NewReader(os.Stdin)
+
+ fmt.Println("Welcome to dborg CLI setup!")
+ fmt.Println("----------------------------")
+ fmt.Print("\nEnter your DB.org.ai API key: ")
+
+ apiKey, err := reader.ReadString('\n')
+ if err != nil {
+ return fmt.Errorf("failed to read API key: %w", err)
+ }
+
+ apiKey = strings.TrimSpace(apiKey)
+ if apiKey == "" {
+ return fmt.Errorf("API key cannot be empty")
+ }
+
+ configPath, err := config.GetConfigPath()
+ if err != nil {
+ return fmt.Errorf("failed to get config path: %w", err)
+ }
+
+ cfg := &config.FileConfig{
+ APIKey: apiKey,
+ }
+
+ if err := config.SaveConfig(cfg); err != nil {
+ return fmt.Errorf("failed to save config: %w", err)
+ }
+
+ fmt.Printf("\n✓ Configuration saved to: %s\n", configPath)
+ fmt.Println("\nYou can now use dborg commands without specifying an API key.")
+ fmt.Println("Example: dborg osint username john_doe")
+
+ return nil
+}
diff --git a/cmd/npd.go b/cmd/npd.go
index a6bd7b2..9868eae 100644
--- a/cmd/npd.go
+++ b/cmd/npd.go
@@ -44,8 +44,7 @@ func init() {
}
func runNPDSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
diff --git a/cmd/osint.go b/cmd/osint.go
index 33b5416..9f2cfef 100644
--- a/cmd/osint.go
+++ b/cmd/osint.go
@@ -228,11 +228,7 @@ func runOsintFilesSearch(cmd *cobra.Command, args []string) error {
}
func runOsintBucketsSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- if apiKey == "" {
- return fmt.Errorf("API key required for buckets endpoint")
- }
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -252,11 +248,7 @@ func runOsintBucketsSearch(cmd *cobra.Command, args []string) error {
}
func runOsintBucketFilesSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- if apiKey == "" {
- return fmt.Errorf("API key required for bucket files endpoint")
- }
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -279,11 +271,7 @@ func runOsintBucketFilesSearch(cmd *cobra.Command, args []string) error {
}
func runOsintShortlinksSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- if apiKey == "" {
- return fmt.Errorf("API key required for shortlinks endpoint")
- }
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -308,11 +296,7 @@ func runOsintShortlinksSearch(cmd *cobra.Command, args []string) error {
}
func runOsintGeoSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- if apiKey == "" {
- return fmt.Errorf("API key required for geo endpoint (costs 1 credit)")
- }
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
diff --git a/cmd/reddit.go b/cmd/reddit.go
index 1096a23..194fdab 100644
--- a/cmd/reddit.go
+++ b/cmd/reddit.go
@@ -82,8 +82,7 @@ func init() {
}
func runRedditSubredditPosts(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -107,8 +106,7 @@ func runRedditSubredditPosts(cmd *cobra.Command, args []string) error {
}
func runRedditSubredditComments(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -132,8 +130,7 @@ func runRedditSubredditComments(cmd *cobra.Command, args []string) error {
}
func runRedditUserPosts(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -157,8 +154,7 @@ func runRedditUserPosts(cmd *cobra.Command, args []string) error {
}
func runRedditUserComments(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -182,8 +178,7 @@ func runRedditUserComments(cmd *cobra.Command, args []string) error {
}
func runRedditUserAbout(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
diff --git a/cmd/root.go b/cmd/root.go
index e5939ca..890d336 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -28,5 +28,4 @@ func Execute() {
}
func init() {
- rootCmd.PersistentFlags().StringP("api-key", "k", "", "API key for authentication (or set DBORG_API_KEY env var)")
}
diff --git a/cmd/skiptrace.go b/cmd/skiptrace.go
index a9ab862..9ce1b4b 100644
--- a/cmd/skiptrace.go
+++ b/cmd/skiptrace.go
@@ -68,8 +68,7 @@ func init() {
}
func getSkiptraceClient(cmd *cobra.Command) (*client.Client, error) {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
return client.New(cfg)
}
diff --git a/cmd/sl.go b/cmd/sl.go
index 6a6af63..1efa9e2 100644
--- a/cmd/sl.go
+++ b/cmd/sl.go
@@ -30,8 +30,7 @@ func init() {
}
func runSLSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
diff --git a/cmd/x.go b/cmd/x.go
index 3164b23..0820b27 100644
--- a/cmd/x.go
+++ b/cmd/x.go
@@ -78,8 +78,7 @@ func init() {
}
func runXHistorySearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -129,8 +128,7 @@ func runXTweetsSearch(cmd *cobra.Command, args []string) error {
}
func runXFirstFollowers(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -146,8 +144,7 @@ func runXFirstFollowers(cmd *cobra.Command, args []string) error {
}
func runXNotableFollowers(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -163,9 +160,8 @@ func runXNotableFollowers(cmd *cobra.Command, args []string) error {
}
func runXReplies(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
limit, _ := cmd.Flags().GetInt("limit")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
@@ -185,9 +181,8 @@ func runXReplies(cmd *cobra.Command, args []string) error {
}
func runXSearch(cmd *cobra.Command, args []string) error {
- apiKey, _ := cmd.Flags().GetString("api-key")
limit, _ := cmd.Flags().GetInt("limit")
- cfg := config.New().WithAPIKey(apiKey)
+ cfg := config.New()
c, err := client.New(cfg)
if err != nil {
diff --git a/internal/client/breachforum.go b/internal/client/breachforum.go
new file mode 100644
index 0000000..66fa1fe
--- /dev/null
+++ b/internal/client/breachforum.go
@@ -0,0 +1,30 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) SearchBreachForum(params *models.BreachForumSearchParams) (*models.BreachForumSearchResponse, error) {
+ path := "/breachforum/search"
+
+ queryParams := url.Values{}
+ queryParams.Add("search", params.Search)
+ if params.MaxHits > 0 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BreachForumSearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse BreachForum search response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/bssid.go b/internal/client/bssid.go
new file mode 100644
index 0000000..c3a5e67
--- /dev/null
+++ b/internal/client/bssid.go
@@ -0,0 +1,35 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) LookupBSSID(params *models.BSSIDParams) (*models.BSSIDLookupResponse, error) {
+ path := fmt.Sprintf("/bssid/%s", url.PathEscape(params.BSSID))
+
+ queryParams := url.Values{}
+ if params.All {
+ queryParams.Add("all", "true")
+ }
+ if params.Google {
+ queryParams.Add("google", "true")
+ }
+ if params.OSM {
+ queryParams.Add("osm", "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/buckets.go b/internal/client/buckets.go
new file mode 100644
index 0000000..a3f8936
--- /dev/null
+++ b/internal/client/buckets.go
@@ -0,0 +1,65 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) SearchBuckets(params *models.BucketsSearchParams) (*models.BucketsSearchResponse, error) {
+ path := "/buckets/buckets"
+
+ queryParams := url.Values{}
+ if params.Limit > 0 {
+ queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
+ }
+ if params.Start > 0 {
+ queryParams.Add("start", fmt.Sprintf("%d", params.Start))
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BucketsSearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse buckets search response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchBucketFiles(params *models.BucketsFilesSearchParams) (*models.BucketsFilesSearchResponse, error) {
+ path := "/buckets/files"
+
+ queryParams := url.Values{}
+ if params.Keywords != "" {
+ queryParams.Add("keywords", params.Keywords)
+ }
+ if params.Extensions != "" {
+ queryParams.Add("extensions", params.Extensions)
+ }
+ if params.Buckets != "" {
+ queryParams.Add("buckets", params.Buckets)
+ }
+ if params.Limit > 0 {
+ queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
+ }
+ if params.Start > 0 {
+ queryParams.Add("start", fmt.Sprintf("%d", params.Start))
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BucketsFilesSearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse bucket files search response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/crawl.go b/internal/client/crawl.go
new file mode 100644
index 0000000..f33fbcd
--- /dev/null
+++ b/internal/client/crawl.go
@@ -0,0 +1,50 @@
+package client
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+)
+
+func (c *Client) CrawlDomain(domain string, callback func(line string) error) error {
+ path := fmt.Sprintf("/crawl/%s", url.PathEscape(domain))
+ fullURL := c.config.BaseURL + path
+
+ req, err := http.NewRequest(http.MethodGet, fullURL, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("User-Agent", c.config.UserAgent)
+
+ 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.Text()
+ if len(line) == 0 {
+ continue
+ }
+
+ if err := callback(line); err != nil {
+ return err
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("stream reading error: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/client/files.go b/internal/client/files.go
new file mode 100644
index 0000000..c4ca372
--- /dev/null
+++ b/internal/client/files.go
@@ -0,0 +1,41 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) SearchOpenDirectoryFiles(params *models.OpenDirectorySearchParams) (*models.OpenDirectorySearchResponse, error) {
+ path := fmt.Sprintf("/files/%s", url.PathEscape(params.URL))
+
+ queryParams := url.Values{}
+ if params.Filename != "" {
+ queryParams.Add("filename", params.Filename)
+ }
+ if params.Extension != "" {
+ queryParams.Add("extension", params.Extension)
+ }
+ if params.Exclude != "" {
+ queryParams.Add("exclude", params.Exclude)
+ }
+ if params.Size > 0 {
+ queryParams.Add("size", fmt.Sprintf("%d", params.Size))
+ }
+ if params.From > 0 {
+ queryParams.Add("from", fmt.Sprintf("%d", params.From))
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.OpenDirectorySearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse open directory search response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/geo.go b/internal/client/geo.go
new file mode 100644
index 0000000..3fc5146
--- /dev/null
+++ b/internal/client/geo.go
@@ -0,0 +1,30 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) SearchGeo(params *models.GeoSearchParams) (*models.GeoSearchResponse, error) {
+ path := "/geo"
+
+ queryParams := url.Values{}
+ queryParams.Add("street", params.Street)
+ queryParams.Add("city", params.City)
+ queryParams.Add("state", params.State)
+ queryParams.Add("zip", params.Zip)
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.GeoSearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse geo search response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/osint.go b/internal/client/osint.go
deleted file mode 100644
index 2ee10c6..0000000
--- a/internal/client/osint.go
+++ /dev/null
@@ -1,208 +0,0 @@
-package client
-
-import (
- "encoding/json"
- "fmt"
- "git.db.org.ai/dborg/internal/models"
- "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.Google {
- queryParams.Add("google", "true")
- }
- if params.OSM {
- queryParams.Add("osm", "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
-}
-
-func (c *Client) SearchBreachForum(params *models.BreachForumSearchParams) (*models.BreachForumSearchResponse, error) {
- path := "/osint/breachforum/search"
-
- queryParams := url.Values{}
- queryParams.Add("search", params.Search)
- if params.MaxHits > 0 {
- queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
- }
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.BreachForumSearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse BreachForum search response: %w", err)
- }
-
- return &response, nil
-}
-
-func (c *Client) SearchOpenDirectoryFiles(params *models.OpenDirectorySearchParams) (*models.OpenDirectorySearchResponse, error) {
- path := fmt.Sprintf("/osint/files/%s", url.PathEscape(params.URL))
-
- queryParams := url.Values{}
- if params.Filename != "" {
- queryParams.Add("filename", params.Filename)
- }
- if params.Extension != "" {
- queryParams.Add("extension", params.Extension)
- }
- if params.Exclude != "" {
- queryParams.Add("exclude", params.Exclude)
- }
- if params.Size > 0 {
- queryParams.Add("size", fmt.Sprintf("%d", params.Size))
- }
- if params.From > 0 {
- queryParams.Add("from", fmt.Sprintf("%d", params.From))
- }
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.OpenDirectorySearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse open directory search response: %w", err)
- }
-
- return &response, nil
-}
-
-func (c *Client) SearchBuckets(params *models.BucketsSearchParams) (*models.BucketsSearchResponse, error) {
- path := "/osint/buckets/buckets"
-
- queryParams := url.Values{}
- if params.Limit > 0 {
- queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
- }
- if params.Start > 0 {
- queryParams.Add("start", fmt.Sprintf("%d", params.Start))
- }
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.BucketsSearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse buckets search response: %w", err)
- }
-
- return &response, nil
-}
-
-func (c *Client) SearchBucketFiles(params *models.BucketsFilesSearchParams) (*models.BucketsFilesSearchResponse, error) {
- path := "/osint/buckets/files"
-
- queryParams := url.Values{}
- if params.Keywords != "" {
- queryParams.Add("keywords", params.Keywords)
- }
- if params.Extensions != "" {
- queryParams.Add("extensions", params.Extensions)
- }
- if params.Buckets != "" {
- queryParams.Add("buckets", params.Buckets)
- }
- if params.Limit > 0 {
- queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
- }
- if params.Start > 0 {
- queryParams.Add("start", fmt.Sprintf("%d", params.Start))
- }
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.BucketsFilesSearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse bucket files search response: %w", err)
- }
-
- return &response, nil
-}
-
-func (c *Client) SearchShortlinks(params *models.ShortlinksSearchParams) (*models.ShortlinksSearchResponse, error) {
- path := "/osint/shortlinks"
-
- queryParams := url.Values{}
- if params.Keywords != "" {
- queryParams.Add("keywords", params.Keywords)
- }
- if params.Ext != "" {
- queryParams.Add("ext", params.Ext)
- }
- if params.Order != "" {
- queryParams.Add("order", params.Order)
- }
- if params.Direction != "" {
- queryParams.Add("direction", params.Direction)
- }
- if params.Regexp {
- queryParams.Add("regexp", "true")
- }
- if params.Limit > 0 {
- queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
- }
- if params.Start > 0 {
- queryParams.Add("start", fmt.Sprintf("%d", params.Start))
- }
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.ShortlinksSearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse shortlinks search response: %w", err)
- }
-
- return &response, nil
-}
-
-func (c *Client) SearchGeo(params *models.GeoSearchParams) (*models.GeoSearchResponse, error) {
- path := "/osint/geo"
-
- queryParams := url.Values{}
- queryParams.Add("street", params.Street)
- queryParams.Add("city", params.City)
- queryParams.Add("state", params.State)
- queryParams.Add("zip", params.Zip)
-
- data, err := c.Get(path, queryParams)
- if err != nil {
- return nil, err
- }
-
- var response models.GeoSearchResponse
- if err := json.Unmarshal(data, &response); err != nil {
- return nil, fmt.Errorf("failed to parse geo search response: %w", err)
- }
-
- return &response, nil
-}
diff --git a/internal/client/shortlinks.go b/internal/client/shortlinks.go
new file mode 100644
index 0000000..0815b73
--- /dev/null
+++ b/internal/client/shortlinks.go
@@ -0,0 +1,47 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "git.db.org.ai/dborg/internal/models"
+ "net/url"
+)
+
+func (c *Client) SearchShortlinks(params *models.ShortlinksSearchParams) (*models.ShortlinksSearchResponse, error) {
+ path := "/shortlinks"
+
+ queryParams := url.Values{}
+ if params.Keywords != "" {
+ queryParams.Add("keywords", params.Keywords)
+ }
+ if params.Ext != "" {
+ queryParams.Add("ext", params.Ext)
+ }
+ if params.Order != "" {
+ queryParams.Add("order", params.Order)
+ }
+ if params.Direction != "" {
+ queryParams.Add("direction", params.Direction)
+ }
+ if params.Regexp {
+ queryParams.Add("regexp", "true")
+ }
+ if params.Limit > 0 {
+ queryParams.Add("limit", fmt.Sprintf("%d", params.Limit))
+ }
+ if params.Start > 0 {
+ queryParams.Add("start", fmt.Sprintf("%d", params.Start))
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.ShortlinksSearchResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse shortlinks search response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/skiptrace.go b/internal/client/skiptrace.go
index 6b84336..d4e26ea 100644
--- a/internal/client/skiptrace.go
+++ b/internal/client/skiptrace.go
@@ -75,6 +75,8 @@ func (c *Client) getSSE(path string, params url.Values) ([]byte, error) {
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
switch resp.StatusCode {
+ case http.StatusPaymentRequired:
+ return nil, fmt.Errorf("insufficient credits (402): %s - Please add credits to your account", string(bodyBytes))
case http.StatusForbidden:
return nil, fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
case http.StatusUnauthorized:
diff --git a/internal/client/usrsx.go b/internal/client/usrsx.go
index a3aaa66..9740468 100644
--- a/internal/client/usrsx.go
+++ b/internal/client/usrsx.go
@@ -24,7 +24,7 @@ func (c *Client) CheckUsernameStream(params *models.USRSXParams, callback func(r
queryParams.Add("max_tasks", fmt.Sprintf("%d", params.MaxTasks))
}
- path := fmt.Sprintf("/osint/username/%s", url.PathEscape(params.Username))
+ path := fmt.Sprintf("/username/%s", url.PathEscape(params.Username))
fullURL := c.config.BaseURL + path
if len(queryParams) > 0 {
fullURL += "?" + queryParams.Encode()
diff --git a/internal/config/config.go b/internal/config/config.go
index e1da2c2..b8538be 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,7 +1,10 @@
package config
import (
+ "encoding/json"
+ "fmt"
"os"
+ "path/filepath"
"time"
)
@@ -13,9 +16,21 @@ type Config struct {
UserAgent string
}
+type FileConfig struct {
+ APIKey string `json:"api_key"`
+}
+
func New() *Config {
+ apiKey := os.Getenv("DBORG_API_KEY")
+
+ if apiKey == "" {
+ if fileCfg, err := LoadConfig(); err == nil && fileCfg.APIKey != "" {
+ apiKey = fileCfg.APIKey
+ }
+ }
+
return &Config{
- APIKey: os.Getenv("DBORG_API_KEY"),
+ APIKey: apiKey,
BaseURL: "https://db.org.ai",
Timeout: 30 * time.Second,
MaxRetries: 3,
@@ -36,3 +51,57 @@ func (c *Config) Validate() error {
}
return nil
}
+
+func GetConfigPath() (string, error) {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("failed to get home directory: %w", err)
+ }
+
+ configDir := filepath.Join(homeDir, ".config", "dborg")
+ if err := os.MkdirAll(configDir, 0755); err != nil {
+ return "", fmt.Errorf("failed to create config directory: %w", err)
+ }
+
+ return filepath.Join(configDir, "config.json"), nil
+}
+
+func LoadConfig() (*FileConfig, error) {
+ configPath, err := GetConfigPath()
+ if err != nil {
+ return nil, err
+ }
+
+ data, err := os.ReadFile(configPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return &FileConfig{}, nil
+ }
+ return nil, fmt.Errorf("failed to read config file: %w", err)
+ }
+
+ var cfg FileConfig
+ if err := json.Unmarshal(data, &cfg); err != nil {
+ return nil, fmt.Errorf("failed to parse config file: %w", err)
+ }
+
+ return &cfg, nil
+}
+
+func SaveConfig(cfg *FileConfig) error {
+ configPath, err := GetConfigPath()
+ if err != nil {
+ return err
+ }
+
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %w", err)
+ }
+
+ if err := os.WriteFile(configPath, data, 0600); err != nil {
+ return fmt.Errorf("failed to write config file: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/config/errors.go b/internal/config/errors.go
index 4fd3636..b733071 100644
--- a/internal/config/errors.go
+++ b/internal/config/errors.go
@@ -3,5 +3,5 @@ package config
import "errors"
var (
- ErrMissingAPIKey = errors.New("API key required: set DBORG_API_KEY environment variable or use --api-key flag")
+ ErrMissingAPIKey = errors.New("API key required: run 'dborg init' to configure your API key or set DBORG_API_KEY environment variable")
)
diff --git a/internal/models/breachforum.go b/internal/models/breachforum.go
new file mode 100644
index 0000000..c9314dd
--- /dev/null
+++ b/internal/models/breachforum.go
@@ -0,0 +1,12 @@
+package models
+
+type BreachForumSearchParams struct {
+ Search string
+ MaxHits int
+}
+
+type BreachForumSearchResponse struct {
+ Query string `json:"query"`
+ MaxHits int `json:"max_hits"`
+ Results interface{} `json:"results"`
+}
diff --git a/internal/models/bssid.go b/internal/models/bssid.go
new file mode 100644
index 0000000..a5e00bc
--- /dev/null
+++ b/internal/models/bssid.go
@@ -0,0 +1,23 @@
+package models
+
+type BSSIDParams struct {
+ BSSID string
+ All bool
+ Google bool
+ OSM 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"`
+ GoogleMap string `json:"google_map,omitempty"`
+ OpenStreetMap string `json:"openstreetmap,omitempty"`
+}
+
+type BSSIDLookupResponse []BSSIDResult
diff --git a/internal/models/buckets.go b/internal/models/buckets.go
new file mode 100644
index 0000000..c8aa1a3
--- /dev/null
+++ b/internal/models/buckets.go
@@ -0,0 +1,29 @@
+package models
+
+type BucketsSearchParams struct {
+ Limit int
+ Start int
+}
+
+type BucketsFilesSearchParams struct {
+ Keywords string
+ Extensions string
+ Buckets string
+ Limit int
+ Start int
+}
+
+type CreditsInfo struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+}
+
+type BucketsSearchResponse struct {
+ Credits CreditsInfo `json:"credits"`
+ Results interface{} `json:"results"`
+}
+
+type BucketsFilesSearchResponse struct {
+ Credits CreditsInfo `json:"credits"`
+ Results interface{} `json:"results"`
+}
diff --git a/internal/models/files.go b/internal/models/files.go
new file mode 100644
index 0000000..269d6cc
--- /dev/null
+++ b/internal/models/files.go
@@ -0,0 +1,12 @@
+package models
+
+type OpenDirectorySearchParams struct {
+ URL string
+ Filename string
+ Extension string
+ Exclude string
+ Size int
+ From int
+}
+
+type OpenDirectorySearchResponse map[string]interface{}
diff --git a/internal/models/geo.go b/internal/models/geo.go
new file mode 100644
index 0000000..a380ddb
--- /dev/null
+++ b/internal/models/geo.go
@@ -0,0 +1,10 @@
+package models
+
+type GeoSearchParams struct {
+ Street string
+ City string
+ State string
+ Zip string
+}
+
+type GeoSearchResponse map[string]interface{}
diff --git a/internal/models/osint.go b/internal/models/osint.go
deleted file mode 100644
index 6c2774e..0000000
--- a/internal/models/osint.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package models
-
-type BSSIDParams struct {
- BSSID string
- All bool
- Google bool
- OSM 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"`
- GoogleMap string `json:"google_map,omitempty"`
- OpenStreetMap string `json:"openstreetmap,omitempty"`
-}
-
-type BSSIDLookupResponse []BSSIDResult
-
-type ErrorResponse struct {
- Error string `json:"error"`
-}
-
-type BreachForumSearchParams struct {
- Search string
- MaxHits int
-}
-
-type BreachForumSearchResponse struct {
- Query string `json:"query"`
- MaxHits int `json:"max_hits"`
- Results interface{} `json:"results"`
-}
-
-type OpenDirectorySearchParams struct {
- URL string
- Filename string
- Extension string
- Exclude string
- Size int
- From int
-}
-
-type OpenDirectorySearchResponse map[string]interface{}
-
-type BucketsSearchParams struct {
- Limit int
- Start int
-}
-
-type BucketsFilesSearchParams struct {
- Keywords string
- Extensions string
- Buckets string
- Limit int
- Start int
-}
-
-type ShortlinksSearchParams struct {
- Keywords string
- Ext string
- Order string
- Direction string
- Regexp bool
- Limit int
- Start int
-}
-
-type CreditsInfo struct {
- Remaining int `json:"remaining"`
- Unlimited bool `json:"unlimited"`
-}
-
-type BucketsSearchResponse struct {
- Credits CreditsInfo `json:"credits"`
- Results interface{} `json:"results"`
-}
-
-type BucketsFilesSearchResponse struct {
- Credits CreditsInfo `json:"credits"`
- Results interface{} `json:"results"`
-}
-
-type ShortlinksSearchResponse struct {
- Credits CreditsInfo `json:"credits"`
- Results interface{} `json:"results"`
-}
-
-type GeoSearchParams struct {
- Street string
- City string
- State string
- Zip string
-}
-
-type GeoSearchResponse map[string]interface{}
diff --git a/internal/models/shortlinks.go b/internal/models/shortlinks.go
new file mode 100644
index 0000000..206a6cd
--- /dev/null
+++ b/internal/models/shortlinks.go
@@ -0,0 +1,16 @@
+package models
+
+type ShortlinksSearchParams struct {
+ Keywords string
+ Ext string
+ Order string
+ Direction string
+ Regexp bool
+ Limit int
+ Start int
+}
+
+type ShortlinksSearchResponse struct {
+ Credits CreditsInfo `json:"credits"`
+ Results interface{} `json:"results"`
+}