Compare commits

...
28 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
Hadi 0041c0c132 edit readme instructions & flags
Signed-off-by: Hadi <[email protected]>
2025-05-28 10:02:19 +02:00
Hadi d3fdfdcdc5 comments & sorting flags
Signed-off-by: Hadi <[email protected]>
2025-05-28 10:00:12 +02:00
Hadi 4f087cb7f0 ignore errors
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:57:50 +02:00
Hadi befb546292 refactor main
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:56:03 +02:00
Hadi 9a825bc7cc remove New func
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:46:34 +02:00
Hadi 72b8635832 remove @ from username
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:44:57 +02:00
Hadi 2ba8695674 edit flags sorting
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:44:49 +02:00
Hadi f8eb7d58ba add flag normalization
Signed-off-by: Hadi <[email protected]>
2025-05-27 20:30:29 +02:00
Hadi 637d9811f2 change struct
Signed-off-by: Hadi <[email protected]>
2025-05-27 20:01:05 +02:00
Hadi c498e7bfdd typo
Signed-off-by: Hadi <[email protected]>
2025-05-27 16:12:01 +02:00
Hadi 7e2b6dd426 Cover your tracks!
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:45:28 +02:00
Hadi 05b449afcd add --max-size & --refresh
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:33:00 +02:00
Hadi b4392f972d edit close friends algo
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:12:40 +02:00
Hadi e4cec2b07a Avoid double newline
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:12:33 +02:00
Hadi 15458ab78e not anymore, now by year/half-year
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:26:30 +02:00
Hadi deec50febf fix typo
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:19:14 +02:00
Hadi de47073083 edit readme
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:14:32 +02:00
Hadi a3f4b19382 add logo
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:02:23 +02:00
Hadi afdd30d34b add assets
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:00:15 +02:00
Hadi 58ac92bff8 rephrase
Signed-off-by: Hadi <[email protected]>
2025-05-20 17:00:38 +02:00
Hadi ce96d1f307 cleaner version
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:42:49 +02:00
Hadi bd734f11af format
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:32:20 +02:00
Hadi 1ff0e222c7 update flake
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:30:08 +02:00
17 changed files with 479 additions and 193 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

+10
View File
@@ -0,0 +1,10 @@
# Contributing
Everybody is invited and welcome to contribute to this repo. There is a lot to do... Check the issues!
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes. (but skip step 0 and 1)
- [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) this repo.
- Write your changes (bug fixe, new feature, issues fix, ...).
- Create a Pull Request against the main branch.
+50 -33
View File
@@ -1,4 +1,10 @@
# GH-Recon
<div align="center">
<img src="https://raw.githubusercontent.com/anotherhadi/gh-recon/main/.github/assets/logo.png" width="120px" />
</div>
<br>
# 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>
@@ -6,41 +12,45 @@
<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
- Fetch SSH and GPG keys
- Enumerate social accounts
- Extract unique commit authors (name + email) in both chronological orders
- Extract unique commit authors (name + email)
- Find close friends
- Search using an email address
- Find Github accounts using an email address
- 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
```bash
go get github.com/anotherhadi/gh-recon
go install github.com/anotherhadi/gh-recon@latest
```
### With Nix/NixOS
<details>
<summary>Click to expand</summary>
**From anywhere (using the repo URL):**
```bash
@@ -63,7 +73,9 @@ environment.systemPackages = with pkgs; [ # or home.packages
];
```
## Usage
</details>
## 🧪 Usage
```bash
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
@@ -71,37 +83,42 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
### Flags
- `--token`: Personal Access Token (optional but recommended)
```txt
-deep
Enable deep scan (clone repos, regex search, analyse licenses, etc.)
-email string
Search accounts by email address
-json string
Write results to specified JSON file
-only-commits
Display only commits with author info
-silent
Suppress all non-essential output
-token string
GitHub personal access token (e.g. ghp_...)
-username string
GitHub username to analyze
-u, --username string GitHub username to analyze
-t, --token string GitHub personal access token (e.g. ghp_...)
-e, --email string Search accounts by email address
-d, --deep Enable deep scan (clone repos, regex search, analyse licenses, etc.)
--max-size int Limit the size of repositories to scan (in MB) (only for deep scan) (default 150)
--exclude-repo string Exclude repos from deep scan (comma-separated list, only for deep scan)
-r, --refresh Refresh the cache (only for deep scan)
-c, --only-commits Display only commits with author info
-s, --silent Suppress all non-essential output
-j, --json string Write results to specified JSON file
```
## Example
## 💡 Examples
```bash
gh-recon --username anotherhadi --token ghp_ABC123...
gh-recon --email [email protected] --token ghp_ABC123...
gh-recon --email [email protected]
gh-recon --username anotherhadi --json output.json --deep
```
## Todo
## 🕵️‍♂️ Cover your tracks
Feel free to contribute!
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:
**Todo:**
- **Review your public profile**: Regularly check your GitHub profile and repositories to ensure that you are not unintentionally exposing sensitive information.
- **Manage email exposure**: Use GitHub's settings to control which email addresses are visible on your profile and in commit history. You can also use a no-reply email address for commits. Delete/modify any sensitive information in your commit history.
- **Be Mindful of Repository Content**: Avoid including sensitive information in your repositories, such as API keys, passwords, emails or personal data. Use `.gitignore` to exclude files that contain sensitive information.
- Find and parse licenses
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog) to scan your repositories specifically for exposed secrets and tokens.
**Useful links:**
- [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
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+2 -2
View File
@@ -13,7 +13,7 @@
(system: f system (import nixpkgs { inherit system; }));
pname = "gh-recon";
version = "0.2.0";
version = "0.2.1";
ldflags = [ "-s" "-w" ];
@@ -24,7 +24,7 @@
src = ./.;
vendorHash = "sha256-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM=";
vendorHash = "sha256-S8IzmdiVvBtnQQl0AewGZ1yuitvrdnVQ/Jf2230g3Mg=";
meta = with pkgs.lib; {
description =
+110 -17
View File
@@ -1,45 +1,138 @@
package ghrecon
import (
"fmt"
"sort"
)
type CloseFriendsResult struct {
Login string
Score int
}
const (
maxFollowingForTarget = 50
maxFollowersForFollowing = 20
pointPerCriterion = 1
)
// CloseFriends returns a list of close friends of the user
// To derive this, we check the following:
// 1. The target has less than 50 Following
// 2. The target's following has less than 20 followers
// 2. The target's following has less than 20 followers (+1 point)
// 3. The target's following follows the target (+1 point)
func (r Recon) CloseFriends(username string) (response []CloseFriendsResult) {
r.PrintTitle("🧑‍🤝‍🧑 Close Friends")
following, resp, err := r.client.Users.ListFollowing(r.ctx, username, nil)
following, resp, err := r.Client.Users.ListFollowing(r.Ctx, username, nil)
if err != nil {
r.logger.Fatal("Failed to fetch user's close friends", "err", err)
}
if len(following) > 50 {
r.PrintInfo("INFO", "No commits found")
r.Logger.Error("Failed to fetch user's following list", "user", username, "err", err)
r.PrintNewline()
return []CloseFriendsResult{}
return
}
WaitForRateLimit(resp)
for _, user := range following {
followers, resp, err := r.client.Users.Get(r.ctx, user.GetLogin())
WaitForRateLimit(resp)
if err != nil {
if len(following) >= maxFollowingForTarget {
r.PrintInfo(
"INFO",
fmt.Sprintf(
"%s follows %d or more users (%d). Skipping close friends check.",
username,
maxFollowingForTarget,
len(following),
),
)
r.PrintNewline()
return
}
if len(following) == 0 {
r.PrintInfo("INFO", fmt.Sprintf("%s is not following anyone.", username))
r.PrintNewline()
return
}
for _, userBeingFollowedByTarget := range following {
loginName := userBeingFollowedByTarget.GetLogin()
if loginName == "" {
r.Logger.Warn("User in following list has an empty login", "target_user", username)
continue
}
if followers.GetFollowers() < 20 {
currentScore := 0
userDetails, userResp, userErr := r.Client.Users.Get(r.Ctx, loginName)
if userErr != nil {
r.Logger.Warn(
"Failed to fetch details for followed user",
"followed_user",
loginName,
"err",
userErr,
)
if userResp != nil {
WaitForRateLimit(userResp)
}
continue
}
WaitForRateLimit(userResp)
if userDetails.GetFollowers() < maxFollowersForFollowing {
currentScore += pointPerCriterion
}
followsTargetBack, checkErr := r.checkIfUserFollows(loginName, username)
if checkErr != nil {
} else if followsTargetBack {
currentScore += pointPerCriterion
}
if currentScore > 0 {
response = append(response, CloseFriendsResult{
Login: user.GetLogin(),
Score: 1,
Login: loginName,
Score: currentScore,
})
}
}
for _, friend := range response {
r.PrintInfo("Username", "@"+friend.Login)
if len(response) == 0 {
r.PrintInfo(
"INFO",
fmt.Sprintf("No close friends found for %s based on the criteria.", username),
)
} else {
sort.Slice(response, func(i, j int) bool {
return response[i].Score > response[j].Score
})
for i, friend := range response {
r.PrintInfo(
fmt.Sprintf("Friend n°%d", i+1),
"@"+friend.Login,
"Score: "+fmt.Sprintf("%d", friend.Score),
)
}
}
r.PrintNewline()
return
}
// checkIfUserFollows checks if sourceUserLogin follows targetUserLogin.
func (r Recon) checkIfUserFollows(sourceUserLogin, targetUserLogin string) (bool, error) {
isFollowing, resp, err := r.Client.Users.IsFollowing(r.Ctx, sourceUserLogin, targetUserLogin)
if err != nil {
r.Logger.Warn("Error checking if user follows target",
"source_user_checking", sourceUserLogin,
"target_user_to_check", targetUserLogin,
"err", err)
if resp != nil {
WaitForRateLimit(resp)
}
return false, err
}
if resp != nil {
WaitForRateLimit(resp)
}
return isFollowing, nil
}
+4 -4
View File
@@ -20,8 +20,8 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
collect := func(date string) error {
for page := 1; page <= 10; page++ {
result, resp, err := r.client.Search.Commits(
r.ctx,
result, resp, err := r.Client.Search.Commits(
r.Ctx,
fmt.Sprintf("author:%s author-date:%s", username, date),
&github.SearchOptions{
Sort: "author-date",
@@ -40,7 +40,7 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
name := item.Commit.GetAuthor().GetName()
email := item.Commit.GetAuthor().GetEmail()
if SkipResult(name, email) {
// continue
continue
}
if _, seen := results[name+" - "+email]; !seen {
author := CommitsResult{
@@ -71,7 +71,7 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
">2026-01-01",
} {
if err := collect(date); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
}
}
+154 -34
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,23 +93,18 @@ func findEmailsAndOccurrencesInDir(rootPath string) ([]EmailOccurrence, error) {
return results, nil
}
type DeepResult struct {
Repository string
Owner string
Name string
}
func (r Recon) Deep(username, excludeRepos string) (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,
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)
r.Logger.Error("Failed to fetch repositories", "err", err)
return
}
@@ -103,10 +113,32 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
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(),
Size: repo.GetSize(),
})
}
}
@@ -119,23 +151,30 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
}
tmp_folder := "/tmp/ghrecon-" + username
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
}
r.PrintInfo(
"Downloading repository",
repo.Owner+"/"+repo.Name,
)
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 repositories {
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
if folderExists(destination) {
r.PrintInfo("INFO", "Directory already exists, skipping")
r.PrintInfo("INFO", "Directory already downloaded, skipping "+repo.Owner+"/"+repo.Name)
continue
}
r.PrintInfo(
"Downloading",
repo.Owner+"/"+repo.Name,
fmt.Sprintf("%d", repo.Size/1024)+"MB",
)
cmd := exec.Command(
"git",
"clone",
@@ -144,7 +183,7 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
)
err := cmd.Run()
if err != nil {
r.logger.Error(
r.Logger.Error(
"ERROR",
"Failed to clone repository",
"err",
@@ -157,23 +196,104 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
}
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)
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 {
r.PrintInfo("Email", email.Email)
r.PrintInfo("Found in", tmp_folder, email.FoundIn...)
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
}
+5 -3
View File
@@ -21,8 +21,8 @@ func (r Recon) Email(email string) (response []EmailResult) {
collect := func(date string) error {
for page := 1; page <= 10; page++ {
result, resp, err := r.client.Search.Commits(
r.ctx,
result, resp, err := r.Client.Search.Commits(
r.Ctx,
fmt.Sprintf("author-email:%s author-date:%s", email, date),
&github.SearchOptions{
Sort: "author-date",
@@ -77,7 +77,7 @@ func (r Recon) Email(email string) (response []EmailResult) {
">2026-01-01",
} {
if err := collect(date); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
}
}
@@ -92,5 +92,7 @@ func (r Recon) Email(email string) (response []EmailResult) {
if len(results) == 0 {
r.PrintInfo("INFO", "No commits found")
}
r.PrintNewline()
return
}
+22 -11
View File
@@ -17,9 +17,9 @@ type SSHKeyResult struct {
}
func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil)
sshKeys, resp, err := r.Client.Users.ListKeys(r.Ctx, username, nil)
if err != nil {
r.logger.Error("Failed to fetch ssh keys", "err", err)
r.Logger.Error("Failed to fetch ssh keys", "err", err)
} else if len(sshKeys) == 0 {
r.PrintTitle("🔑 SSH Keys")
r.PrintInfo("INFO", "No SSH Keys found")
@@ -48,7 +48,9 @@ func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
r.PrintInfo("Verified", k.Verified)
r.PrintInfo("Last Used", k.LastUsed)
r.PrintInfo("Added By", k.AddedBy)
r.PrintNewline()
if i != len(sshKeys)-1 {
r.PrintNewline()
}
}
}
r.PrintNewline()
@@ -72,9 +74,9 @@ type GPGKeyResult struct {
}
func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
gpgKeys, resp, err := r.client.Users.ListGPGKeys(r.ctx, username, nil)
gpgKeys, resp, err := r.Client.Users.ListGPGKeys(r.Ctx, username, nil)
if err != nil {
r.logger.Error("Failed to fetch user's gpg keys", "err", err)
r.Logger.Error("Failed to fetch user's gpg keys", "err", err)
} else if len(gpgKeys) == 0 {
r.PrintTitle("🗝️ GPG Keys")
r.PrintInfo("INFO", "No GPG Keys found")
@@ -122,6 +124,9 @@ func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
r.PrintInfo(" Email n°", fmt.Sprintf("%d", j))
r.PrintInfo(" Email", email.Email)
r.PrintInfo(" Verified", email.Verified)
if j != len(k.Emails)-1 {
r.PrintNewline()
}
}
r.PrintInfo("Subkeys", fmt.Sprintf("%d", len(k.Subkeys)))
for j, subkey := range k.Subkeys {
@@ -131,9 +136,13 @@ func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
r.PrintInfo(" Subkey Created At", subkey.CreatedAt)
r.PrintInfo(" Subkey Primary Key ID", subkey.PrimaryKeyID)
r.PrintInfo(" Subkey Raw Key", subkey.RawKey)
if j != len(k.Subkeys)-1 {
r.PrintNewline()
}
}
if i != len(gpgKeys)-1 {
r.PrintNewline()
}
r.PrintNewline()
}
}
r.PrintNewline()
@@ -149,13 +158,13 @@ type SSHSigningKeyResult struct {
}
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
signingKeys, resp, err := r.client.Users.ListSSHSigningKeys(
r.ctx,
signingKeys, resp, err := r.Client.Users.ListSSHSigningKeys(
r.Ctx,
username,
nil,
)
if err != nil {
r.logger.Error("Failed to fetch user's ssh signing keys", "err", err)
r.Logger.Error("Failed to fetch user's ssh signing keys", "err", err)
} else if len(signingKeys) == 0 {
r.PrintTitle("📝 SSH Signing Keys")
r.PrintInfo("INFO", "No SSH Signing Keys found")
@@ -173,7 +182,9 @@ func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult)
r.PrintInfo("Title", k.Title)
r.PrintInfo("Created At", k.CreatedAt)
r.PrintInfo("Key", k.Key)
r.PrintNewline()
if i != len(signingKeys)-1 {
r.PrintNewline()
}
response = append(response, k)
}
}
+6 -21
View File
@@ -8,25 +8,10 @@ import (
)
type Recon struct {
client *github.Client
logger *log.Logger
ctx context.Context
silent bool
jsonFile string
}
func NewRecon(
client *github.Client,
logger *log.Logger,
ctx context.Context,
silent bool,
jsonFile string,
) *Recon {
return &Recon{
client: client,
logger: logger,
ctx: ctx,
silent: silent,
jsonFile: jsonFile,
}
Client *github.Client
Logger *log.Logger
Ctx context.Context
Silent bool
JsonFile string
MaxRepoSize int
}
+5 -3
View File
@@ -12,9 +12,9 @@ type OrgResult struct {
}
func (r Recon) Orgs(username string) (response []OrgResult) {
orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil)
orgs, resp, err := r.Client.Organizations.List(r.Ctx, username, nil)
if err != nil {
r.logger.Error("Failed to fetch organizations", "err", err)
r.Logger.Error("Failed to fetch organizations", "err", err)
} else if len(orgs) == 0 {
r.PrintTitle("🏢 Organizations")
r.PrintInfo("INFO", "No Organizations found")
@@ -32,7 +32,9 @@ func (r Recon) Orgs(username string) (response []OrgResult) {
r.PrintInfo("ID", o.ID)
r.PrintInfo("URL", o.URL)
r.PrintInfo("Description", o.Description)
r.PrintNewline()
if i != len(orgs)-1 {
r.PrintNewline()
}
response = append(response, o)
}
}
+3 -3
View File
@@ -11,16 +11,16 @@ type SocialResult struct {
}
func (r Recon) Socials(username string) (response []SocialResult) {
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
resp, err := FetchGitHubAPI(r.Client, "", "/users/"+username+"/social_accounts")
if err != nil {
r.logger.Error("Failed to fetch socials", "err", err)
r.Logger.Error("Failed to fetch socials", "err", err)
return
}
var socialAccounts []SocialResult
err = json.Unmarshal(resp, &socialAccounts)
if err != nil {
r.logger.Error("Failed to unmarshal socials", "err", err)
r.Logger.Error("Failed to unmarshal socials", "err", err)
return
}
+3 -3
View File
@@ -30,12 +30,12 @@ type UserResult struct {
}
func (r Recon) User(username string) (response UserResult) {
user, resp, err := r.client.Users.Get(r.ctx, username)
user, resp, err := r.Client.Users.Get(r.Ctx, username)
if resp.StatusCode == 404 {
r.logger.Fatal("User not found")
r.Logger.Fatal("User not found")
}
if err != nil {
r.logger.Fatal("Failed to fetch user's information", "err", err)
r.Logger.Fatal("Failed to fetch user's information", "err", err)
}
r.PrintTitle("👤 User informations")
+22 -11
View File
@@ -25,7 +25,7 @@ var (
)
func (r Recon) Header() {
if r.silent {
if r.Silent {
return
}
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
@@ -66,7 +66,9 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(resp.Body)
@@ -91,14 +93,14 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
}
func (r Recon) PrintNewline() {
if r.silent {
if r.Silent {
return
}
fmt.Println()
}
func (r Recon) PrintTitle(title string) {
if r.silent {
if r.Silent {
return
}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
@@ -106,7 +108,7 @@ func (r Recon) PrintTitle(title string) {
}
func (r Recon) PrintInfo(key, value string, more ...string) {
if r.silent {
if r.Silent {
return
}
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
@@ -144,20 +146,29 @@ func SkipResult(name, email string) bool {
}
func (r Recon) WriteJson(data any) {
if r.jsonFile == "" {
if r.JsonFile == "" {
return
}
file, err := os.Create(r.jsonFile)
file, err := os.Create(r.JsonFile)
if err != nil {
r.logger.Error("Failed to create JSON file", "err", err)
r.Logger.Error("Failed to create JSON file", "err", err)
return
}
defer file.Close()
defer func() {
_ = file.Close()
}()
as_json, _ := json.MarshalIndent(data, "", "\t")
_, err = file.Write(as_json)
if err != nil {
r.logger.Error("Failed to write to JSON file", "err", err)
r.Logger.Error("Failed to write to JSON file", "err", err)
return
}
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
}
+67 -48
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"os"
"strings"
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
"github.com/charmbracelet/log"
@@ -19,16 +20,13 @@ func main() {
var silent bool
var jsonFile string
var excludeRepos string
var maxRepoSize int
var refresh bool
// FLAGS
flag.StringVarP(&username, "username", "u", "", "GitHub username to analyze")
flag.StringVarP(&token, "token", "t", "", "GitHub personal access token (e.g. ghp_...)")
flag.StringVarP(&fromEmail, "email", "e", "", "Search accounts by email address")
flag.BoolVarP(
&onlyCommitsLeak,
"only-commits",
"c",
false,
"Display only commits with author info",
)
flag.BoolVarP(
&deep,
"deep",
@@ -36,60 +34,81 @@ func main() {
false,
"Enable deep scan (clone repos, regex search, analyse licenses, etc.)",
)
flag.BoolVarP(&silent, "silent", "s", false, "Suppress all non-essential output")
flag.StringVarP(&jsonFile, "json", "j", "", "Write results to specified JSON file")
flag.IntVar(
&maxRepoSize,
"max-size",
150,
"Limit the size of repositories to scan (in MB) (only for deep scan)",
)
flag.StringVar(
&excludeRepos,
"exclude-repo",
"",
"Exclude repos from deep scan (comma-separated list)",
"Exclude repos from deep scan (comma-separated list, only for deep scan)",
)
flag.BoolVarP(
&refresh,
"refresh",
"r",
false,
"Refresh the cache (only for deep scan)",
)
flag.BoolVarP(
&onlyCommitsLeak,
"only-commits",
"c",
false,
"Display only commits with author info",
)
flag.BoolVarP(&silent, "silent", "s", false, "Suppress all non-essential output")
flag.StringVarP(&jsonFile, "json", "j", "", "Write results to specified JSON file")
// FLAGS SETTINGS
flag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
flag.CommandLine.SortFlags = false
flag.Parse()
styles := log.DefaultStyles()
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
logger := log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: false,
ReportTimestamp: false,
})
logger.SetStyles(styles)
// INITIALIZE RECON OBJECT
r := &ghrecon.Recon{
Client: github.NewClient(nil),
Logger: log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: false,
ReportTimestamp: false,
}),
Ctx: context.Background(),
Silent: silent,
JsonFile: jsonFile,
MaxRepoSize: maxRepoSize,
}
// CHECK FLAGS
if username == "" && fromEmail == "" {
logger.Error(
r.Logger.Fatal(
"Please provide a username with the --username (-u) flag or an email with the --email (-e) flag",
)
os.Exit(1)
} else if username != "" {
username = strings.TrimPrefix(username, "@")
if err := ghrecon.ParseUsername(username); err != nil {
logger.Error("Invalid username", "err", err)
os.Exit(1)
r.Logger.Fatal("Invalid username", "err", err)
}
}
client := github.NewClient(nil)
if token == "" {
if !silent {
logger.Info(
"It's recommended to set a Github token for better rate limits. You can set it using the --token (-t) flag.",
)
}
r.PrintInfo(
"INFO",
"It's recommended to set a Github token for better rate limits. You can set it using the --token (-t) flag.",
)
} else {
client = client.WithAuthToken(token)
r.Client = r.Client.WithAuthToken(token)
}
ctx := context.Background()
r := ghrecon.NewRecon(
client,
logger,
ctx,
silent,
jsonFile,
)
// START
r.Header()
if fromEmail != "" {
emailsInfo := r.Email(fromEmail)
r.WriteJson(
@@ -120,18 +139,18 @@ func main() {
commitsInfo := r.Commits(username)
results := map[string]any{
"User": userInfo,
"Orgs": orgsInfo,
"SSHKeys": sshKeysInfo,
"GPGKeys": gpgKeysInfo,
"SSHSigningKeys": sshSigningKeysInfo,
"Socials": socialsInfo,
"Commits": commitsInfo,
"CloseFriends": closeFriendsInfo,
}
"User": userInfo,
"Orgs": orgsInfo,
"SSHKeys": sshKeysInfo,
"GPGKeys": gpgKeysInfo,
"SSHSigningKeys": sshSigningKeysInfo,
"Socials": socialsInfo,
"Commits": commitsInfo,
"CloseFriends": closeFriendsInfo,
}
if deep {
results["Deep"] = r.Deep(username, excludeRepos)
results["Deep"] = r.Deep(username, excludeRepos, refresh)
}
r.WriteJson(results)
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"strings"
flag "github.com/spf13/pflag"
)
func wordSepNormalizeFunc(f *flag.FlagSet, name string) flag.NormalizedName {
from := []string{".", "_"}
to := "-"
for _, sep := range from {
name = strings.ReplaceAll(name, sep, to)
}
return flag.NormalizedName(name)
}