Files
keyhunter/cmd/recon.go

111 lines
3.9 KiB
Go

package cmd
import (
"context"
"fmt"
"os"
"github.com/salvacybersec/keyhunter/pkg/providers"
"github.com/salvacybersec/keyhunter/pkg/recon"
"github.com/salvacybersec/keyhunter/pkg/recon/sources"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
reconStealth bool
reconRespectRobots bool
reconQuery string
)
var reconCmd = &cobra.Command{
Use: "recon",
Short: "Run OSINT recon across internet sources",
Long: "Run OSINT recon sweeps across registered sources. Phase 10 adds ten code-hosting sources (GitHub/GitLab/Bitbucket/Gist/Codeberg/HuggingFace/Replit/CodeSandbox/Sandboxes/Kaggle). Further phases add pastebins, search engines, etc.",
}
var reconFullCmd = &cobra.Command{
Use: "full",
Short: "Sweep all enabled sources in parallel and deduplicate findings",
RunE: func(cmd *cobra.Command, args []string) error {
eng := buildReconEngine()
cfg := recon.Config{
Stealth: reconStealth,
RespectRobots: reconRespectRobots,
Query: reconQuery,
}
ctx := context.Background()
all, err := eng.SweepAll(ctx, cfg)
if err != nil {
return fmt.Errorf("recon sweep: %w", err)
}
deduped := recon.Dedup(all)
fmt.Printf("recon: swept %d sources, %d findings (%d after dedup)\n", len(eng.List()), len(all), len(deduped))
for _, f := range deduped {
fmt.Printf(" [%s] %s %s %s\n", f.SourceType, f.ProviderName, f.KeyMasked, f.Source)
}
return nil
},
}
var reconListCmd = &cobra.Command{
Use: "list",
Short: "List registered recon sources",
RunE: func(cmd *cobra.Command, args []string) error {
eng := buildReconEngine()
for _, name := range eng.List() {
fmt.Println(name)
}
return nil
},
}
// buildReconEngine constructs the recon Engine with all registered sources.
// Phase 9 contributes ExampleSource; Phase 10 contributes ten code-hosting
// sources via sources.RegisterAll. Credentials are read from environment
// variables first, then from viper config keys under `recon.<source>.*`.
// Sources whose credentials are missing are still registered but Enabled()
// will report false so SweepAll skips them cleanly.
func buildReconEngine() *recon.Engine {
e := recon.NewEngine()
e.Register(recon.ExampleSource{})
reg, err := providers.NewRegistry()
if err != nil {
fmt.Fprintf(os.Stderr, "recon: failed to load providers: %v\n", err)
return e
}
cfg := sources.SourcesConfig{
Registry: reg,
Limiters: recon.NewLimiterRegistry(),
GitHubToken: firstNonEmpty(os.Getenv("GITHUB_TOKEN"), viper.GetString("recon.github.token")),
GitLabToken: firstNonEmpty(os.Getenv("GITLAB_TOKEN"), viper.GetString("recon.gitlab.token")),
BitbucketToken: firstNonEmpty(os.Getenv("BITBUCKET_TOKEN"), viper.GetString("recon.bitbucket.token")),
BitbucketWorkspace: firstNonEmpty(os.Getenv("BITBUCKET_WORKSPACE"), viper.GetString("recon.bitbucket.workspace")),
CodebergToken: firstNonEmpty(os.Getenv("CODEBERG_TOKEN"), viper.GetString("recon.codeberg.token")),
HuggingFaceToken: firstNonEmpty(os.Getenv("HUGGINGFACE_TOKEN"), viper.GetString("recon.huggingface.token")),
KaggleUser: firstNonEmpty(os.Getenv("KAGGLE_USERNAME"), viper.GetString("recon.kaggle.username")),
KaggleKey: firstNonEmpty(os.Getenv("KAGGLE_KEY"), viper.GetString("recon.kaggle.key")),
}
sources.RegisterAll(e, cfg)
return e
}
// firstNonEmpty returns a if non-empty, otherwise b. Used to implement the
// env-var → viper-config precedence chain for credential lookup.
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
func init() {
reconFullCmd.Flags().BoolVar(&reconStealth, "stealth", false, "enable UA rotation and jitter delays")
reconFullCmd.Flags().BoolVar(&reconRespectRobots, "respect-robots", true, "respect robots.txt for web-scraping sources")
reconFullCmd.Flags().StringVar(&reconQuery, "query", "", "override query sent to each source")
reconCmd.AddCommand(reconFullCmd)
reconCmd.AddCommand(reconListCmd)
}