Compare commits

..
5 Commits
Author SHA1 Message Date
Hadi df9fff8e97 format
Signed-off-by: Hadi <[email protected]>
2025-05-28 14:10:46 +02:00
Hadi 926bf7239e return authors
Signed-off-by: Hadi <[email protected]>
2025-05-28 14:10:31 +02:00
Hadi 5b79b6492b Filter by repo size
Signed-off-by: Hadi <[email protected]>
2025-05-28 14:03:39 +02:00
Hadi 2137925043 add commit inspection for deep mode
Signed-off-by: Hadi <[email protected]>
2025-05-28 13:41:15 +02:00
Hadi 274b393e53 edit readme
Signed-off-by: Hadi <[email protected]>
2025-05-28 10:10:42 +02:00
3 changed files with 152 additions and 53 deletions
+12 -11
View File
@@ -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
View File
@@ -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
}
+7
View File
@@ -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
}