diff options
| author | s <[email protected]> | 2025-11-08 02:44:13 -0500 |
|---|---|---|
| committer | s <[email protected]> | 2025-11-08 02:44:13 -0500 |
| commit | dfcf52f30cdbde3a4e1400024b0c27451d179e5d (patch) | |
| tree | cdcac72f0d58b0689777644c771e80d53b502434 | |
| parent | 486a369f05125a3b86d663ea94684466e0658099 (diff) | |
| download | dborg-dfcf52f30cdbde3a4e1400024b0c27451d179e5d.tar.gz dborg-dfcf52f30cdbde3a4e1400024b0c27451d179e5d.zip | |
feat: add unauthenticated client support and new osint/x commands
| -rw-r--r-- | cmd/dns.go | 2 | ||||
| -rw-r--r-- | cmd/osint.go | 45 | ||||
| -rw-r--r-- | cmd/root.go | 2 | ||||
| -rw-r--r-- | cmd/x.go | 54 | ||||
| -rw-r--r-- | internal/client/client.go | 9 | ||||
| -rw-r--r-- | internal/client/client_test.go | 42 | ||||
| -rw-r--r-- | internal/client/osint.go | 33 | ||||
| -rw-r--r-- | internal/client/x.go | 30 | ||||
| -rw-r--r-- | internal/models/osint.go | 11 | ||||
| -rw-r--r-- | internal/models/x.go | 30 |
10 files changed, 252 insertions, 6 deletions
@@ -29,7 +29,7 @@ func runDNSTLDCheck(cmd *cobra.Command, args []string) error { showOnly, _ := cmd.Flags().GetString("show-only") cfg := config.New() - c, err := client.New(cfg) + c, err := client.NewUnauthenticated(cfg) if err != nil { return fmt.Errorf("failed to create client: %w", err) } diff --git a/cmd/osint.go b/cmd/osint.go index 5aa8799..6b97464 100644 --- a/cmd/osint.go +++ b/cmd/osint.go @@ -41,11 +41,20 @@ var osintBreachForumCmd = &cobra.Command{ RunE: runOsintBreachForumSearch, } +var osintFilesCmd = &cobra.Command{ + Use: "files [url]", + Short: "Search open directory files", + Long: `Search for files in open directories using various filters (free OSINT endpoint)`, + Args: cobra.ExactArgs(1), + RunE: runOsintFilesSearch, +} + func init() { rootCmd.AddCommand(osintCmd) osintCmd.AddCommand(osintUsernameCmd) osintCmd.AddCommand(osintBSSIDCmd) osintCmd.AddCommand(osintBreachForumCmd) + osintCmd.AddCommand(osintFilesCmd) osintUsernameCmd.Flags().StringSliceP("sites", "s", []string{}, "Specific sites to check (comma-separated)") osintUsernameCmd.Flags().BoolP("fuzzy", "f", false, "Enable fuzzy validation mode") @@ -56,13 +65,18 @@ func init() { osintBSSIDCmd.Flags().BoolP("osm", "o", false, "Include OpenStreetMap URL for the location") osintBreachForumCmd.Flags().IntP("max_hits", "m", 10, "Maximum number of hits to return") + + osintFilesCmd.Flags().StringP("filename", "n", "", "Search term to match in filenames") + osintFilesCmd.Flags().StringP("extension", "e", "", "Filter by file extension(s) - comma-separated (e.g., pdf,doc,txt)") + osintFilesCmd.Flags().StringP("exclude", "x", "html,HTML", "Exclude file extension(s) - comma-separated") + osintFilesCmd.Flags().IntP("size", "s", 10, "Number of results to return (max 40)") + osintFilesCmd.Flags().IntP("from", "f", 0, "Starting offset for pagination") } func runOsintUsernameCheck(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) + c, err := client.NewUnauthenticated(cfg) if err != nil { return err } @@ -131,3 +145,28 @@ func runOsintBreachForumSearch(cmd *cobra.Command, args []string) error { return utils.PrintJSON(response) } + +func runOsintFilesSearch(cmd *cobra.Command, args []string) error { + cfg := config.New() + + c, err := client.NewUnauthenticated(cfg) + if err != nil { + return err + } + + params := &models.OpenDirectorySearchParams{ + URL: args[0], + } + params.Filename, _ = cmd.Flags().GetString("filename") + params.Extension, _ = cmd.Flags().GetString("extension") + params.Exclude, _ = cmd.Flags().GetString("exclude") + params.Size, _ = cmd.Flags().GetInt("size") + params.From, _ = cmd.Flags().GetInt("from") + + response, err := c.SearchOpenDirectoryFiles(params) + if err != nil { + return err + } + + return utils.PrintJSON(response) +} diff --git a/cmd/root.go b/cmd/root.go index cb6a676..e5939ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,7 @@ var rootCmd = &cobra.Command{ Use: "dborg", Short: "DB.org.ai CLI client", Long: `█▀▀▄ █▀▀▄ █▀▀█ █▀▀█ █▀▀▀ █▀▀█ ▀█▀ -█░░█ █▀▀▄░░░█░░█ █▄▄▀ █░▀█░░░█▄▄█░░█░░ +█░░█░█▀▀▄░░░█░░█░█▄▄▀░█░▀█░░░█▄▄█░░█ ▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀▀▀ DB.org.ai CLI client`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { @@ -32,10 +32,28 @@ var xTweetsCmd = &cobra.Command{ RunE: runXTweetsSearch, } +var xFirstCmd = &cobra.Command{ + Use: "first [username]", + Short: "Get first 20 followers of a Twitter/X account", + Long: `Retrieves the first 20 followers of a Twitter/X account`, + Args: cobra.ExactArgs(1), + RunE: runXFirstFollowers, +} + +var xNFLCmd = &cobra.Command{ + Use: "nfl [username]", + Short: "Get notable followers of a Twitter/X account", + Long: `Retrieves the notable followers (influential accounts) following a Twitter/X account`, + Args: cobra.ExactArgs(1), + RunE: runXNotableFollowers, +} + func init() { rootCmd.AddCommand(xCmd) xCmd.AddCommand(xHistoryCmd) xCmd.AddCommand(xTweetsCmd) + xCmd.AddCommand(xFirstCmd) + xCmd.AddCommand(xNFLCmd) } func runXHistorySearch(cmd *cobra.Command, args []string) error { @@ -72,7 +90,7 @@ func runXHistorySearch(cmd *cobra.Command, args []string) error { func runXTweetsSearch(cmd *cobra.Command, args []string) error { cfg := config.New() - c, err := client.New(cfg) + c, err := client.NewUnauthenticated(cfg) if err != nil { return err } @@ -88,3 +106,37 @@ func runXTweetsSearch(cmd *cobra.Command, args []string) error { return nil } + +func runXFirstFollowers(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.GetFirstFollowers(args[0]) + if err != nil { + return err + } + + return utils.PrintJSON(response) +} + +func runXNotableFollowers(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.GetNotableFollowers(args[0]) + if err != nil { + return err + } + + return utils.PrintJSON(response) +} diff --git a/internal/client/client.go b/internal/client/client.go index 468fae9..eaf87cb 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -29,6 +29,15 @@ func New(cfg *config.Config) (*Client, error) { }, nil } +func NewUnauthenticated(cfg *config.Config) (*Client, error) { + 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 { diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 31939fb..fb21755 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -44,3 +44,45 @@ func TestNewClient(t *testing.T) { }) } } + +func TestNewUnauthenticatedClient(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantErr bool + }{ + { + name: "config with API key", + config: &config.Config{ + APIKey: "test-key", + BaseURL: "https://db.org.ai", + Timeout: 30 * time.Second, + MaxRetries: 3, + UserAgent: "test-agent", + }, + wantErr: false, + }, + { + name: "config without API key", + config: &config.Config{ + BaseURL: "https://db.org.ai", + Timeout: 30 * time.Second, + MaxRetries: 3, + UserAgent: "test-agent", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewUnauthenticated(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("NewUnauthenticated() error = %v, wantErr %v", err, tt.wantErr) + } + if c == nil && !tt.wantErr { + t.Error("NewUnauthenticated() returned nil client") + } + }) + } +} diff --git a/internal/client/osint.go b/internal/client/osint.go index 20f53ce..95e3550 100644 --- a/internal/client/osint.go +++ b/internal/client/osint.go @@ -55,3 +55,36 @@ func (c *Client) SearchBreachForum(params *models.BreachForumSearchParams) (*mod 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 +} diff --git a/internal/client/x.go b/internal/client/x.go index 6c899ad..bd16692 100644 --- a/internal/client/x.go +++ b/internal/client/x.go @@ -26,6 +26,36 @@ func (c *Client) SearchTwitterHistory(username string) (*models.XResponse, error return &response, nil } +func (c *Client) GetFirstFollowers(username string) (*models.FirstFollowersResponse, error) { + path := fmt.Sprintf("/x/first/%s", url.PathEscape(username)) + data, err := c.Get(path, nil) + if err != nil { + return nil, err + } + + var response models.FirstFollowersResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse first followers response: %w", err) + } + + return &response, nil +} + +func (c *Client) GetNotableFollowers(username string) (*models.NotableFollowersResponse, error) { + path := fmt.Sprintf("/x/nfl/%s", url.PathEscape(username)) + data, err := c.Get(path, nil) + if err != nil { + return nil, err + } + + var response models.NotableFollowersResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to parse notable followers response: %w", err) + } + + return &response, nil +} + func (c *Client) FetchTweetsStream(username string, callback func(result json.RawMessage) error) error { path := fmt.Sprintf("/x/tweets/%s", url.PathEscape(username)) fullURL := c.config.BaseURL + path diff --git a/internal/models/osint.go b/internal/models/osint.go index 6096df8..9f714c4 100644 --- a/internal/models/osint.go +++ b/internal/models/osint.go @@ -36,3 +36,14 @@ type BreachForumSearchResponse struct { 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{} diff --git a/internal/models/x.go b/internal/models/x.go index b4e6eac..07c3117 100644 --- a/internal/models/x.go +++ b/internal/models/x.go @@ -49,3 +49,33 @@ type TweetsStreamResponse struct { Complete *Complete `json:"complete,omitempty"` Error string `json:"error,omitempty"` } + +type FirstFollower struct { + Number int `json:"number"` + Username string `json:"username"` + Name string `json:"name"` +} + +type FirstFollowersResponse struct { + Username string `json:"username"` + Followers []FirstFollower `json:"followers"` + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` +} + +type NotableFollower struct { + Username string `json:"username"` + FollowerCount string `json:"follower_count"` + Score float64 `json:"score"` +} + +type NotableFollowersResponse struct { + Username string `json:"username"` + Followers []NotableFollower `json:"followers"` + Credits struct { + Remaining int `json:"remaining"` + Unlimited bool `json:"unlimited"` + } `json:"credits"` +} |
