package ghrecon import ( "fmt" "io/fs" "os" "os/exec" "path/filepath" "regexp" "slices" "strings" "github.com/google/go-github/v72/github" ) func folderExists(path string) bool { if stat, err := os.Stat(path); err == nil && stat.IsDir() { return true } return false } type EmailOccurrence struct { Email string FoundIn []string } func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) { emailLocations := make(map[string]map[string]bool) emailRegex := regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`) normalizedRootPath := filepath.Clean(rootPath) err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error { if err != nil { fmt.Printf("Can't access %s: %v\n", path, err) return err } if !d.IsDir() { content, err := os.ReadFile(path) if err != nil { fmt.Printf("Can't read %s: %v\n", path, err) return nil } currentFileEmails := emailRegex.FindAllString(string(content), -1) if len(currentFileEmails) > 0 { relativePath, errRel := filepath.Rel(normalizedRootPath, path) if errRel != nil { fmt.Printf("Can't find the relative path %s: %v\n", path, errRel) relativePath = path } for _, email := range currentFileEmails { if len(email) > 12 { if _, ok := emailLocations[email]; !ok { emailLocations[email] = make(map[string]bool) } emailLocations[email][relativePath] = true } } } } return nil }) if err != nil { return nil, err } var results []EmailOccurrence for email, pathSet := range emailLocations { var paths []string for path := range pathSet { paths = append(paths, path) } results = append(results, EmailOccurrence{Email: email, FoundIn: paths}) } return results, nil } type DeepResult struct { Repository string Owner string Name string Size int } func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []DeepResult) { excludeReposList := strings.Split(excludeRepos, ",") repos, resp, err := r.client.Repositories.ListByUser( r.ctx, username, &github.RepositoryListByUserOptions{ Type: "all", }, ) if err != nil { r.logger.Error("Failed to fetch repositories", "err", err) return } r.PrintTitle("📦 Repositories") if len(repos) == 0 { r.PrintInfo("INFO", "No repositories found") } else { for _, repo := range repos { response = append(response, DeepResult{ Repository: repo.GetCloneURL(), Owner: repo.GetOwner().GetLogin(), Name: repo.GetName(), Size: repo.GetSize(), }) } } WaitForRateLimit(resp) cmd := exec.Command("git", "--version") if err := cmd.Run(); err != nil { r.PrintInfo("ERROR", "Git is not installed, please install it to use this feature") return } tmp_folder := "/tmp/ghrecon-" + username if folderExists(tmp_folder) { if refresh { r.PrintInfo("INFO", "Deleting existing folder "+tmp_folder) err := os.RemoveAll(tmp_folder) if err != nil { r.PrintInfo("ERROR", "Failed to delete existing folder "+tmp_folder) } } } for _, repo := range response { if slices.Contains(excludeReposList, repo.Name) || slices.Contains(excludeReposList, repo.Owner+"/"+repo.Name) { r.PrintInfo("INFO", "Skipping repository", repo.Owner+"/"+repo.Name) continue } maxRepoSize := r.maxRepoSize * 1024 if repo.Size > maxRepoSize { r.PrintInfo( "INFO", "Skipping repository "+repo.Owner+"/"+repo.Name+" due to size", fmt.Sprintf( "%d", repo.Size/1024, )+"MB > "+fmt.Sprintf( "%d", maxRepoSize/1024, )+"MB", ) continue } r.PrintInfo( "Downloading", repo.Owner+"/"+repo.Name, fmt.Sprintf("%d", repo.Size/1024)+"MB", ) destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name if folderExists(destination) { r.PrintInfo("INFO", "Directory already exists, skipping") continue } cmd := exec.Command( "git", "clone", repo.Repository, destination, ) err := cmd.Run() if err != nil { r.logger.Error( "ERROR", "Failed to clone repository", "err", err, "repo", repo.Repository, ) continue } } r.PrintInfo("INFO", "Cloned all repositories to "+tmp_folder) r.PrintInfo("INFO", "Now searching for emails in cloned repositories, this may take a while...") results, err := findEmailsAndOccurrencesInDir(tmp_folder) if err != nil { r.logger.Error("Failed to find emails in directory", "err", err) return } if len(results) == 0 { r.PrintInfo("INFO", "No emails found") } else { r.PrintInfo("INFO", "Found emails:") for _, email := range results { r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", ")) } } r.PrintNewline() return }