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>
|
<br>
|
||||||
|
|
||||||
# GH-Recon
|
# GH-Recon 🔍
|
||||||
|
|
||||||
<p>
|
<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>
|
<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>
|
<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>
|
</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)
|
- Retrieve basic user profile information (username, ID, avatar, bio, creation dates)
|
||||||
- List organizations and roles
|
- 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
|
- Export results to JSON
|
||||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
- 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.
|
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+
|
- 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.
|
- 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
|
### With Go
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ environment.systemPackages = with pkgs; [ # or home.packages
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Usage
|
## 🧪 Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
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
|
-j, --json string Write results to specified JSON file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## 💡 Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username anotherhadi --token ghp_ABC123...
|
gh-recon --username anotherhadi --token ghp_ABC123...
|
||||||
@@ -103,7 +104,7 @@ gh-recon --email [email protected]
|
|||||||
gh-recon --username anotherhadi --json output.json --deep
|
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:
|
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)
|
- [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)
|
- [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.
|
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"
|
"github.com/google/go-github/v72/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
func folderExists(path string) bool {
|
type AuthorOccurrence struct {
|
||||||
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
Name string
|
||||||
return true
|
Email string
|
||||||
}
|
FoundIn []string
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailOccurrence struct {
|
type EmailOccurrence struct {
|
||||||
@@ -25,6 +24,19 @@ type EmailOccurrence struct {
|
|||||||
FoundIn []string
|
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) {
|
func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) {
|
||||||
emailLocations := make(map[string]map[string]bool)
|
emailLocations := make(map[string]map[string]bool)
|
||||||
emailRegex := regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
|
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
|
return err
|
||||||
}
|
}
|
||||||
if !d.IsDir() {
|
if !d.IsDir() {
|
||||||
|
if strings.Contains(path, ".git/logs/") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
content, err := os.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Can't read %s: %v\n", path, err)
|
fmt.Printf("Can't read %s: %v\n", path, err)
|
||||||
@@ -78,14 +93,8 @@ func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeepResult struct {
|
func (r Recon) Deep(username, excludeRepos string, refresh bool) (response DeepResult) {
|
||||||
Repository string
|
repositories := []Repositorie{}
|
||||||
Owner string
|
|
||||||
Name string
|
|
||||||
Size int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []DeepResult) {
|
|
||||||
excludeReposList := strings.Split(excludeRepos, ",")
|
excludeReposList := strings.Split(excludeRepos, ",")
|
||||||
repos, resp, err := r.Client.Repositories.ListByUser(
|
repos, resp, err := r.Client.Repositories.ListByUser(
|
||||||
r.Ctx,
|
r.Ctx,
|
||||||
@@ -104,7 +113,28 @@ func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []Dee
|
|||||||
r.PrintInfo("INFO", "No repositories found")
|
r.PrintInfo("INFO", "No repositories found")
|
||||||
} else {
|
} else {
|
||||||
for _, repo := range repos {
|
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(),
|
Repository: repo.GetCloneURL(),
|
||||||
Owner: repo.GetOwner().GetLogin(),
|
Owner: repo.GetOwner().GetLogin(),
|
||||||
Name: repo.GetName(),
|
Name: repo.GetName(),
|
||||||
@@ -132,40 +162,19 @@ func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []Dee
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, repo := range response {
|
for _, repo := range repositories {
|
||||||
if slices.Contains(excludeReposList, repo.Name) ||
|
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||||
slices.Contains(excludeReposList, repo.Owner+"/"+repo.Name) {
|
if folderExists(destination) {
|
||||||
r.PrintInfo("INFO", "Skipping repository", repo.Owner+"/"+repo.Name)
|
r.PrintInfo("INFO", "Directory already downloaded, skipping "+repo.Owner+"/"+repo.Name)
|
||||||
continue
|
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(
|
r.PrintInfo(
|
||||||
"Downloading",
|
"Downloading",
|
||||||
repo.Owner+"/"+repo.Name,
|
repo.Owner+"/"+repo.Name,
|
||||||
fmt.Sprintf("%d", repo.Size/1024)+"MB",
|
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(
|
cmd := exec.Command(
|
||||||
"git",
|
"git",
|
||||||
"clone",
|
"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)
|
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...")
|
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 {
|
if err != nil {
|
||||||
r.Logger.Error("Failed to find emails in directory", "err", err)
|
r.Logger.Error("Failed to find emails in directory", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(emails) == 0 {
|
||||||
r.PrintInfo("INFO", "No emails found")
|
r.PrintInfo("INFO", "No emails found")
|
||||||
} else {
|
} else {
|
||||||
r.PrintInfo("INFO", "Found emails:")
|
r.PrintInfo("INFO", "Found emails:")
|
||||||
for _, email := range results {
|
for _, email := range emails {
|
||||||
r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", "))
|
r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.Repositories = repositories
|
||||||
|
response.Authors = authorOccurrences
|
||||||
|
response.Emails = emails
|
||||||
|
|
||||||
r.PrintNewline()
|
r.PrintNewline()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,3 +165,10 @@ func (r Recon) WriteJson(data any) {
|
|||||||
}
|
}
|
||||||
r.PrintInfo("INFO", "JSON file created successfully", "file", r.JsonFile)
|
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