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> <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
View File
@@ -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
} }
+7
View File
@@ -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
}