mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 19:08:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9fff8e97 | ||
|
|
926bf7239e | ||
|
|
5b79b6492b | ||
|
|
2137925043 | ||
|
|
274b393e53 |
@@ -4,7 +4,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
# GH-Recon
|
||||
# GH-Recon 🔍
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/anotherhadi/gh-recon/releases"><img src="https://img.shields.io/github/release/anotherhadi/gh-recon.svg" alt="Latest Release"></a>
|
||||
@@ -12,11 +12,12 @@
|
||||
<a href="https://goreportcard.com/report/github.com/anotherhadi/gh-recon"><img src="https://goreportcard.com/badge/github.com/anotherhadi/gh-recon" alt="GoReportCard"></a>
|
||||
</p>
|
||||
|
||||
## Project Overview
|
||||
## 🧾 Project Overview
|
||||
|
||||
Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.
|
||||
Retrieves and aggregates public OSINT data about a GitHub user using Go and the GitHub API.
|
||||
Finds hidden emails in commit history, previous usernames, friends, other GitHub accounts, and more.
|
||||
|
||||
## Features
|
||||
## 🚀 Features
|
||||
|
||||
- Retrieve basic user profile information (username, ID, avatar, bio, creation dates)
|
||||
- List organizations and roles
|
||||
@@ -28,16 +29,16 @@ Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and th
|
||||
- Export results to JSON
|
||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
||||
|
||||
## Disclaimer
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
This tool is intended for educational purposes only. Use responsibly and ensure you have permission to access the data you are querying.
|
||||
|
||||
## Prerequisites
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Go 1.18+
|
||||
- GitHub Personal Access Token (recommended for higher rate limits): Create a GitHub API token with no permissions/no scope. This will be equivalent to public GitHub access, but it will allow access to use the GitHub Search API.
|
||||
|
||||
## Installation
|
||||
## 📦 Installation
|
||||
|
||||
### With Go
|
||||
|
||||
@@ -74,7 +75,7 @@ environment.systemPackages = with pkgs; [ # or home.packages
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
## 🧪 Usage
|
||||
|
||||
```bash
|
||||
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
||||
@@ -95,7 +96,7 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
||||
-j, --json string Write results to specified JSON file
|
||||
```
|
||||
|
||||
## Example
|
||||
## 💡 Examples
|
||||
|
||||
```bash
|
||||
gh-recon --username anotherhadi --token ghp_ABC123...
|
||||
@@ -103,7 +104,7 @@ gh-recon --email [email protected]
|
||||
gh-recon --username anotherhadi --json output.json --deep
|
||||
```
|
||||
|
||||
## Cover your tracks
|
||||
## 🕵️♂️ Cover your tracks
|
||||
|
||||
Understanding what information about you is publicly visible is the first step to managing your online presence. gh-recon can help you identify your own publicly available data on GitHub. Here’s how you can take steps to protect your privacy and security:
|
||||
|
||||
@@ -118,6 +119,6 @@ You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog)
|
||||
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
|
||||
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
|
||||
|
||||
## Contributing
|
||||
## 🤝 Contributing
|
||||
|
||||
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
|
||||
+133
-42
@@ -13,11 +13,10 @@ import (
|
||||
"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 AuthorOccurrence struct {
|
||||
Name string
|
||||
Email string
|
||||
FoundIn []string
|
||||
}
|
||||
|
||||
type EmailOccurrence struct {
|
||||
@@ -25,6 +24,19 @@ type EmailOccurrence struct {
|
||||
FoundIn []string
|
||||
}
|
||||
|
||||
type DeepResult struct {
|
||||
Repositories []Repositorie
|
||||
Authors []AuthorOccurrence
|
||||
Emails []EmailOccurrence
|
||||
}
|
||||
|
||||
type Repositorie struct {
|
||||
Repository string
|
||||
Owner string
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
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,}`)
|
||||
@@ -36,6 +48,9 @@ func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
if strings.Contains(path, ".git/logs/") {
|
||||
return nil
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't read %s: %v\n", path, err)
|
||||
@@ -78,14 +93,8 @@ func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) {
|
||||
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) {
|
||||
func (r Recon) Deep(username, excludeRepos string, refresh bool) (response DeepResult) {
|
||||
repositories := []Repositorie{}
|
||||
excludeReposList := strings.Split(excludeRepos, ",")
|
||||
repos, resp, err := r.Client.Repositories.ListByUser(
|
||||
r.Ctx,
|
||||
@@ -104,7 +113,28 @@ func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []Dee
|
||||
r.PrintInfo("INFO", "No repositories found")
|
||||
} else {
|
||||
for _, repo := range repos {
|
||||
response = append(response, DeepResult{
|
||||
if slices.Contains(excludeReposList, repo.GetName()) ||
|
||||
slices.Contains(excludeReposList, repo.GetOwner().GetLogin()+"/"+repo.GetName()) {
|
||||
continue
|
||||
}
|
||||
|
||||
maxRepoSize := r.MaxRepoSize * 1024
|
||||
|
||||
if repo.GetSize() > maxRepoSize {
|
||||
r.PrintInfo(
|
||||
"INFO",
|
||||
"Skipping repository "+repo.GetOwner().GetLogin()+"/"+repo.GetName()+" due to size", fmt.Sprintf(
|
||||
"%d",
|
||||
repo.GetSize()/1024,
|
||||
)+"MB > "+fmt.Sprintf(
|
||||
"%d",
|
||||
maxRepoSize/1024,
|
||||
)+"MB",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
repositories = append(repositories, Repositorie{
|
||||
Repository: repo.GetCloneURL(),
|
||||
Owner: repo.GetOwner().GetLogin(),
|
||||
Name: repo.GetName(),
|
||||
@@ -132,40 +162,19 @@ func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []Dee
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
for _, repo := range repositories {
|
||||
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||
if folderExists(destination) {
|
||||
r.PrintInfo("INFO", "Directory already downloaded, skipping "+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",
|
||||
@@ -187,22 +196,104 @@ func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []Dee
|
||||
}
|
||||
r.PrintInfo("INFO", "Cloned all repositories to "+tmp_folder)
|
||||
|
||||
authorOccurrences := []AuthorOccurrence{}
|
||||
mapAuthorToIndex := make(map[string]int)
|
||||
for _, repo := range repositories {
|
||||
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||
if !folderExists(filepath.Join(destination, ".git")) {
|
||||
r.Logger.Error(
|
||||
"No .git directory found, cannot run git log.",
|
||||
"repo",
|
||||
repo.Owner+"/"+repo.Name,
|
||||
"path",
|
||||
destination,
|
||||
)
|
||||
} else {
|
||||
gitLogCmd := exec.Command("git", "log", "--all", "--format=%aN <%aE>")
|
||||
gitLogCmd.Dir = destination
|
||||
logOutput, logErr := gitLogCmd.Output()
|
||||
|
||||
if logErr != nil {
|
||||
if exitErr, ok := logErr.(*exec.ExitError); ok {
|
||||
r.Logger.Error("Failed to execute git log (ExitError)", "repo", repo.Owner+"/"+repo.Name, "stderr", string(exitErr.Stderr), "err", logErr)
|
||||
} else {
|
||||
r.Logger.Error("Failed to execute git log", "repo", repo.Owner+"/"+repo.Name, "err", logErr)
|
||||
}
|
||||
} else {
|
||||
lines := strings.Split(string(logOutput), "\n")
|
||||
repoIdentifier := repo.Owner + "/" + repo.Name
|
||||
|
||||
for _, line := range lines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
if trimmedLine == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if index, exists := mapAuthorToIndex[trimmedLine]; exists {
|
||||
isRepoListed := false
|
||||
for _, foundRepo := range authorOccurrences[index].FoundIn {
|
||||
if foundRepo == repoIdentifier {
|
||||
isRepoListed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isRepoListed {
|
||||
authorOccurrences[index].FoundIn = append(authorOccurrences[index].FoundIn, repoIdentifier)
|
||||
slices.Sort(authorOccurrences[index].FoundIn)
|
||||
}
|
||||
} else {
|
||||
parts := strings.SplitN(trimmedLine, " <", 2)
|
||||
var authorName, authorEmail string
|
||||
if len(parts) == 2 {
|
||||
authorName = parts[0]
|
||||
authorEmail = strings.TrimSuffix(parts[1], ">")
|
||||
} else if len(parts) == 1 {
|
||||
authorName = "-"
|
||||
authorEmail = strings.TrimPrefix(strings.TrimSuffix(parts[0], ">"), "<")
|
||||
} else {
|
||||
r.Logger.Error("Malformed author line from git log", "line", trimmedLine, "repo", repoIdentifier)
|
||||
continue
|
||||
}
|
||||
|
||||
authorOccurrences = append(authorOccurrences, AuthorOccurrence{
|
||||
Name: authorName,
|
||||
Email: authorEmail,
|
||||
FoundIn: []string{repoIdentifier},
|
||||
})
|
||||
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, author := range authorOccurrences {
|
||||
r.PrintInfo(
|
||||
"Author",
|
||||
author.Name+" <"+author.Email+">",
|
||||
"found in:"+strings.Join(author.FoundIn, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
r.PrintInfo("INFO", "Now searching for emails in cloned repositories, this may take a while...")
|
||||
results, err := findEmailsAndOccurrencesInDir(tmp_folder)
|
||||
emails, err := findEmailsAndOccurrencesInDir(tmp_folder)
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to find emails in directory", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
if len(emails) == 0 {
|
||||
r.PrintInfo("INFO", "No emails found")
|
||||
} else {
|
||||
r.PrintInfo("INFO", "Found emails:")
|
||||
for _, email := range results {
|
||||
for _, email := range emails {
|
||||
r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
response.Repositories = repositories
|
||||
response.Authors = authorOccurrences
|
||||
response.Emails = emails
|
||||
|
||||
r.PrintNewline()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,3 +165,10 @@ func (r Recon) WriteJson(data any) {
|
||||
}
|
||||
r.PrintInfo("INFO", "JSON file created successfully", "file", r.JsonFile)
|
||||
}
|
||||
|
||||
func folderExists(path string) bool {
|
||||
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user