Compare commits

..
8 Commits
Author SHA1 Message Date
Hadi 5a9483411a v0.2.1
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:28:32 +02:00
Hadi 7f74f5d28f update
Signed-off-by: Hadi <[email protected]>
2025-05-10 00:52:13 +02:00
Hadi bd99649b4e refactor
Signed-off-by: Hadi <[email protected]>
2025-05-10 00:51:43 +02:00
Hadi be6a9fbc6b add badges
Signed-off-by: Hadi <[email protected]>
2025-05-09 23:54:11 +02:00
Hadi 4b75b6f755 rename
Signed-off-by: Hadi <[email protected]>
2025-05-09 23:24:21 +02:00
Hadi d02479748e edit not found message
Signed-off-by: Hadi <[email protected]>
2025-05-09 23:23:11 +02:00
Hadi 9eee8e60be change utils
Signed-off-by: Hadi <[email protected]>
2025-05-09 23:22:12 +02:00
Hadi 4a3a06fa30 add commit leak url
Signed-off-by: Hadi <[email protected]>
2025-05-09 23:22:03 +02:00
20 changed files with 1021 additions and 406 deletions
+34 -5
View File
@@ -1,5 +1,11 @@
# 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>
<a href="https://pkg.go.dev/github.com/anotherhadi/gh-recon?tab=doc"><img src="https://godoc.org/github.com/anotherhadi/gh-recon?status.svg" alt="GoDoc"></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>
## Project Overview
Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.
@@ -11,6 +17,10 @@ Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and th
- Fetch SSH and GPG keys
- Enumerate social accounts
- Extract unique commit authors (name + email) in both chronological orders
- Find close friends
- Search using an email address
- Export results to JSON
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
## Disclaimer
@@ -19,7 +29,7 @@ This tool is intended for educational purposes only. Use responsibly and ensure
## Prerequisites
- Go 1.18+
- GitHub Personal Access Token (recommended for higher rate limits)
- 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
@@ -61,18 +71,37 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
### Flags
- `--username`: GitHub username to inspect (required)
- `--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
```
## Example
```bash
gh-recon --username anotherhadi --token ghp_ABC123...
gh-recon --email [email protected] --token ghp_ABC123...
gh-recon --username anotherhadi --json output.json --deep
```
## Todo
Feel free to contribute! Here are some ideas:
Feel free to contribute!
- Fetch names in License files
- Fetch emails in README files/comments
**Todo:**
- Find and parse licenses
-74
View File
@@ -1,74 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func commits(client *github.Client, ctx context.Context, username string) {
// Commits
fmt.Println(
GreyStyle.Render("[")+GreenStyle.Render("+")+GreyStyle.Render("]"),
GreyStyle.Render("Commits:\n"),
)
seenNames := make(map[string]struct{})
seenEmails := make(map[string]struct{})
var names, emails []string
collect := func(order string) error {
opts := &github.SearchOptions{
Sort: "author-date",
Order: order, // "desc" ou "asc"
ListOptions: github.ListOptions{PerPage: 100},
}
for page := 1; page <= 10; page++ {
opts.ListOptions.Page = page
result, resp, err := client.Search.Commits(
ctx,
fmt.Sprintf("author:%s", username),
opts,
)
if err != nil {
return fmt.Errorf("fetch page %d (%s): %w", page, order, err)
}
WaitForRateLimit(resp)
if len(result.Commits) == 0 {
break
}
for _, item := range result.Commits {
a := item.Commit.GetAuthor()
name := a.GetName()
email := a.GetEmail()
if _, seen := seenNames[name]; !seen {
seenNames[name] = struct{}{}
names = append(names, name)
}
if _, seen := seenEmails[email]; !seen {
seenEmails[email] = struct{}{}
emails = append(emails, email)
}
}
}
return nil
}
if err := collect("desc"); err != nil {
log.Error("Failed to fetch commits", "err", err)
return
}
if err := collect("asc"); err != nil {
log.Error("Failed to fetch commits", "err", err)
return
}
for i, name := range names {
PrintInfo("Name "+fmt.Sprint(i+1), name)
}
for i, email := range emails {
PrintInfo("Email "+fmt.Sprint(i+1), email)
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
(system: f system (import nixpkgs { inherit system; }));
pname = "gh-recon";
version = "0.1.0";
version = "0.2.0";
ldflags = [ "-s" "-w" ];
+45
View File
@@ -0,0 +1,45 @@
package ghrecon
type CloseFriendsResult struct {
Login string
Score int
}
// 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
func (r Recon) CloseFriends(username string) (response []CloseFriendsResult) {
r.PrintTitle("🧑‍🤝‍🧑 Close Friends")
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.PrintNewline()
return []CloseFriendsResult{}
}
WaitForRateLimit(resp)
for _, user := range following {
followers, resp, err := r.client.Users.Get(r.ctx, user.GetLogin())
WaitForRateLimit(resp)
if err != nil {
continue
}
if followers.GetFollowers() < 20 {
response = append(response, CloseFriendsResult{
Login: user.GetLogin(),
Score: 1,
})
}
}
for _, friend := range response {
r.PrintInfo("Username", "@"+friend.Login)
}
r.PrintNewline()
return
}
+92
View File
@@ -0,0 +1,92 @@
package ghrecon
import (
"fmt"
"github.com/google/go-github/v72/github"
)
type CommitsResult struct {
Name string
Email string
Occurences int
FirstFoundIn string
}
func (r Recon) Commits(username string) (response []CommitsResult) {
r.PrintTitle("🐙 Commits")
results := make(map[string]CommitsResult)
collect := func(date string) error {
for page := 1; page <= 10; page++ {
result, resp, err := r.client.Search.Commits(
r.ctx,
fmt.Sprintf("author:%s author-date:%s", username, date),
&github.SearchOptions{
Sort: "author-date",
Order: "desc",
ListOptions: github.ListOptions{PerPage: 100, Page: page},
},
)
if err != nil {
return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
}
WaitForRateLimit(resp)
if len(result.Commits) == 0 {
break
}
for _, item := range result.Commits {
name := item.Commit.GetAuthor().GetName()
email := item.Commit.GetAuthor().GetEmail()
if SkipResult(name, email) {
// continue
}
if _, seen := results[name+" - "+email]; !seen {
author := CommitsResult{
Name: name,
Email: email,
Occurences: 1,
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
GetName(),
}
results[name+" - "+email] = author
} else {
result := results[name+" - "+email]
result.Occurences++
results[name+" - "+email] = result
}
}
}
return nil
}
// Range of dates to bypass the limit of 1000 results
for _, date := range []string{
"<2023-01-01", "2023-01-01..2023-12-31",
"2024-01-01..2024-05-31",
"2024-06-01..2024-12-31",
"2025-01-01..2025-05-31",
"2025-06-01..2025-12-31",
">2026-01-01",
} {
if err := collect(date); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
}
}
for _, result := range results {
r.PrintInfo(
"Author",
result.Name+" - "+result.Email,
"first from "+result.FirstFoundIn+" (x"+fmt.Sprint(result.Occurences)+")",
)
response = append(response, result)
}
if len(results) == 0 {
r.PrintInfo("INFO", "No commits found")
}
r.PrintNewline()
return
}
+179
View File
@@ -0,0 +1,179 @@
package ghrecon
import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"
"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 EmailOccurrence struct {
Email string
FoundIn []string
}
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,}`)
normalizedRootPath := filepath.Clean(rootPath)
err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Printf("Can't access %s: %v\n", path, err)
return err
}
if !d.IsDir() {
content, err := os.ReadFile(path)
if err != nil {
fmt.Printf("Can't read %s: %v\n", path, err)
return nil
}
currentFileEmails := emailRegex.FindAllString(string(content), -1)
if len(currentFileEmails) > 0 {
relativePath, errRel := filepath.Rel(normalizedRootPath, path)
if errRel != nil {
fmt.Printf("Can't find the relative path %s: %v\n", path, errRel)
relativePath = path
}
for _, email := range currentFileEmails {
if len(email) > 12 {
if _, ok := emailLocations[email]; !ok {
emailLocations[email] = make(map[string]bool)
}
emailLocations[email][relativePath] = true
}
}
}
}
return nil
})
if err != nil {
return nil, err
}
var results []EmailOccurrence
for email, pathSet := range emailLocations {
var paths []string
for path := range pathSet {
paths = append(paths, path)
}
results = append(results, EmailOccurrence{Email: email, FoundIn: paths})
}
return results, nil
}
type DeepResult struct {
Repository string
Owner string
Name string
}
func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
excludeReposList := strings.Split(excludeRepos, ",")
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)
return
}
r.PrintTitle("📦 Repositories")
if len(repos) == 0 {
r.PrintInfo("INFO", "No repositories found")
} else {
for _, repo := range repos {
response = append(response, DeepResult{
Repository: repo.GetCloneURL(),
Owner: repo.GetOwner().GetLogin(),
Name: repo.GetName(),
})
}
}
WaitForRateLimit(resp)
cmd := exec.Command("git", "--version")
if err := cmd.Run(); err != nil {
r.PrintInfo("ERROR", "Git is not installed, please install it to use this feature")
return
}
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,
)
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
if folderExists(destination) {
r.PrintInfo("INFO", "Directory already exists, skipping")
continue
}
cmd := exec.Command(
"git",
"clone",
repo.Repository,
destination,
)
err := cmd.Run()
if err != nil {
r.logger.Error(
"ERROR",
"Failed to clone repository",
"err",
err,
"repo",
repo.Repository,
)
continue
}
}
r.PrintInfo("INFO", "Cloned all repositories to "+tmp_folder)
r.PrintInfo("INFO", "Now searching for emails in cloned repositories, this may take a while...")
results, err := findEmailsAndOccurrencesInDir(tmp_folder)
if err != nil {
r.logger.Error("Failed to find emails in directory", "err", err)
return
}
if len(results) == 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...)
}
}
r.PrintNewline()
return
}
+96
View File
@@ -0,0 +1,96 @@
package ghrecon
import (
"fmt"
"github.com/google/go-github/v72/github"
)
type EmailResult struct {
Name string
Email string
Username string
Occurences int
FirstFoundIn string
}
func (r Recon) Email(email string) (response []EmailResult) {
r.PrintTitle("✉️ Email")
results := make(map[string]EmailResult)
collect := func(date string) error {
for page := 1; page <= 10; page++ {
result, resp, err := r.client.Search.Commits(
r.ctx,
fmt.Sprintf("author-email:%s author-date:%s", email, date),
&github.SearchOptions{
Sort: "author-date",
Order: "desc",
ListOptions: github.ListOptions{PerPage: 100, Page: page},
},
)
if err != nil {
return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
}
WaitForRateLimit(resp)
if len(result.Commits) == 0 {
break
}
for _, item := range result.Commits {
name := item.Commit.GetAuthor().GetName()
email := item.Commit.GetAuthor().GetEmail()
login := item.GetAuthor().GetLogin()
if login == "" {
login = "Unknown"
}
if SkipResult(name, email) {
continue
}
if _, seen := results[name+" - "+email+" - "+login]; !seen {
author := EmailResult{
Name: name,
Email: email,
Username: login,
Occurences: 1,
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
GetName(),
}
results[name+" - "+email+" - "+login] = author
} else {
result := results[name+" - "+email+" - "+login]
result.Occurences++
results[name+" - "+email+" - "+login] = result
}
}
}
return nil
}
// Range of dates to bypass the limit of 1000 results
for _, date := range []string{
"<2023-01-01", "2023-01-01..2023-12-31",
"2024-01-01..2024-05-31",
"2024-06-01..2024-12-31",
"2025-01-01..2025-05-31",
"2025-06-01..2025-12-31",
">2026-01-01",
} {
if err := collect(date); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
}
}
for _, result := range results {
r.PrintInfo(
"Author",
result.Name+" - "+result.Email+" - @"+result.Username,
"first from "+result.FirstFoundIn+" (x"+fmt.Sprint(result.Occurences)+")",
)
response = append(response, result)
}
if len(results) == 0 {
r.PrintInfo("INFO", "No commits found")
}
return
}
+183
View File
@@ -0,0 +1,183 @@
package ghrecon
import (
"fmt"
)
type SSHKeyResult struct {
ID string
Url string
Title string
CreatedAt string
Key string
ReadOnly string
Verified string
LastUsed string
AddedBy string
}
func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil)
if err != nil {
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")
} else {
r.PrintTitle("🔑 SSH Keys")
for i, key := range sshKeys {
k := SSHKeyResult{
ID: fmt.Sprintf("%d", key.GetID()),
Url: key.GetURL(),
Title: key.GetTitle(),
CreatedAt: key.GetCreatedAt().String(),
Key: key.GetKey(),
ReadOnly: fmt.Sprintf("%t", key.GetReadOnly()),
Verified: fmt.Sprintf("%t", key.GetVerified()),
LastUsed: key.GetLastUsed().String(),
AddedBy: key.GetAddedBy(),
}
response = append(response, k)
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
r.PrintInfo("ID", k.ID)
r.PrintInfo("URL", k.Url)
r.PrintInfo("Title", k.Title)
r.PrintInfo("Created At", k.CreatedAt)
r.PrintInfo("Key", k.Key)
r.PrintInfo("Read Only", k.ReadOnly)
r.PrintInfo("Verified", k.Verified)
r.PrintInfo("Last Used", k.LastUsed)
r.PrintInfo("Added By", k.AddedBy)
r.PrintNewline()
}
}
r.PrintNewline()
WaitForRateLimit(resp)
return
}
type GPGKeyEmail struct {
Email string
Verified string
}
type GPGKeyResult struct {
ID string
KeyID string
PublicKey string
CreatedAt string
PrimaryKeyID string
RawKey string
Emails []GPGKeyEmail
Subkeys []GPGKeyResult
}
func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
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)
} else if len(gpgKeys) == 0 {
r.PrintTitle("🗝️ GPG Keys")
r.PrintInfo("INFO", "No GPG Keys found")
} else {
r.PrintTitle("🗝️ GPG Keys")
for i, key := range gpgKeys {
k := GPGKeyResult{
ID: fmt.Sprintf("%d", key.GetID()),
KeyID: key.GetKeyID(),
PublicKey: key.GetPublicKey(),
CreatedAt: key.GetCreatedAt().String(),
PrimaryKeyID: fmt.Sprintf("%d", key.GetPrimaryKeyID()),
RawKey: key.GetRawKey(),
Emails: []GPGKeyEmail{},
Subkeys: []GPGKeyResult{},
}
for _, email := range key.Emails {
email := GPGKeyEmail{
Email: email.GetEmail(),
Verified: fmt.Sprintf("%t", email.GetVerified()),
}
k.Emails = append(k.Emails, email)
}
for _, subkey := range key.Subkeys {
subkey := GPGKeyResult{
ID: fmt.Sprintf("%d", subkey.GetID()),
KeyID: subkey.GetKeyID(),
PublicKey: subkey.GetPublicKey(),
CreatedAt: subkey.GetCreatedAt().String(),
PrimaryKeyID: fmt.Sprintf("%d", subkey.GetPrimaryKeyID()),
RawKey: subkey.GetRawKey(),
}
k.Subkeys = append(k.Subkeys, subkey)
}
response = append(response, k)
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
r.PrintInfo("ID", k.ID)
r.PrintInfo("Key ID", k.KeyID)
r.PrintInfo("Public Key", k.PublicKey)
r.PrintInfo("Created At", k.CreatedAt)
r.PrintInfo("Primary Key ID", k.PrimaryKeyID)
r.PrintInfo("Raw Key", k.RawKey)
r.PrintInfo("Emails", fmt.Sprintf("%d", len(k.Emails)))
for j, email := range k.Emails {
r.PrintInfo(" Email n°", fmt.Sprintf("%d", j))
r.PrintInfo(" Email", email.Email)
r.PrintInfo(" Verified", email.Verified)
}
r.PrintInfo("Subkeys", fmt.Sprintf("%d", len(k.Subkeys)))
for j, subkey := range k.Subkeys {
r.PrintInfo(" Subkey n°", fmt.Sprintf("%d", j))
r.PrintInfo(" Subkey ID", subkey.ID)
r.PrintInfo(" Subkey Key ID", subkey.KeyID)
r.PrintInfo(" Subkey Created At", subkey.CreatedAt)
r.PrintInfo(" Subkey Primary Key ID", subkey.PrimaryKeyID)
r.PrintInfo(" Subkey Raw Key", subkey.RawKey)
}
r.PrintNewline()
}
}
r.PrintNewline()
WaitForRateLimit(resp)
return
}
type SSHSigningKeyResult struct {
ID string
Title string
CreatedAt string
Key string
}
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
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)
} else if len(signingKeys) == 0 {
r.PrintTitle("📝 SSH Signing Keys")
r.PrintInfo("INFO", "No SSH Signing Keys found")
} else {
r.PrintTitle("📝 SSH Signing Keys")
for i, key := range signingKeys {
k := SSHSigningKeyResult{
ID: fmt.Sprintf("%d", key.GetID()),
Title: key.GetTitle(),
CreatedAt: key.GetCreatedAt().String(),
Key: key.GetKey(),
}
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
r.PrintInfo("ID", k.ID)
r.PrintInfo("Title", k.Title)
r.PrintInfo("Created At", k.CreatedAt)
r.PrintInfo("Key", k.Key)
r.PrintNewline()
response = append(response, k)
}
}
WaitForRateLimit(resp)
r.PrintNewline()
return response
}
+32
View File
@@ -0,0 +1,32 @@
package ghrecon
import (
"context"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
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,
}
}
+42
View File
@@ -0,0 +1,42 @@
package ghrecon
import (
"fmt"
)
type OrgResult struct {
Login string
ID string
URL string
Description string
}
func (r Recon) Orgs(username string) (response []OrgResult) {
orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil)
if err != nil {
r.logger.Error("Failed to fetch organizations", "err", err)
} else if len(orgs) == 0 {
r.PrintTitle("🏢 Organizations")
r.PrintInfo("INFO", "No Organizations found")
} else {
r.PrintTitle("🏢 Organizations")
for i, org := range orgs {
o := OrgResult{
Login: org.GetLogin(),
ID: fmt.Sprintf("%d", org.GetID()),
URL: org.GetURL(),
Description: org.GetDescription(),
}
r.PrintInfo("Organization n°", fmt.Sprintf("%d", i))
r.PrintInfo("Login", o.Login)
r.PrintInfo("ID", o.ID)
r.PrintInfo("URL", o.URL)
r.PrintInfo("Description", o.Description)
r.PrintNewline()
response = append(response, o)
}
}
r.PrintNewline()
WaitForRateLimit(resp)
return
}
+41
View File
@@ -0,0 +1,41 @@
package ghrecon
import (
"encoding/json"
"fmt"
)
type SocialResult struct {
Provider string `json:"provider"`
URL string `json:"url"`
}
func (r Recon) Socials(username string) (response []SocialResult) {
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
if err != nil {
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)
return
}
if len(socialAccounts) == 0 {
r.PrintTitle("🐥 Socials")
r.PrintInfo("INFO", "No commits found")
} else {
r.PrintTitle("🐥 Socials")
for i, account := range socialAccounts {
r.PrintInfo("Social n°", fmt.Sprintf("%d", i))
r.PrintInfo("Provider", account.Provider)
r.PrintInfo("URL", account.URL)
}
}
r.PrintNewline()
return socialAccounts
}
+92
View File
@@ -0,0 +1,92 @@
package ghrecon
import (
"fmt"
)
type UserResult struct {
Username string
ID string
AvatarURL string
GravatarID string
Name string
Company string
Location string
Email string
Hireable string
Bio string
PublicRepos string
PublicGists string
Followers string
Following string
CreatedAt string
UpdatedAt string
SuspendedAt string
TotalPrivateRepos string
PrivateGists string
DiskUsage string
Collaborators string
Plan string
}
func (r Recon) User(username string) (response UserResult) {
user, resp, err := r.client.Users.Get(r.ctx, username)
if resp.StatusCode == 404 {
r.logger.Fatal("User not found")
}
if err != nil {
r.logger.Fatal("Failed to fetch user's information", "err", err)
}
r.PrintTitle("👤 User informations")
u := UserResult{
Username: user.GetLogin(),
ID: fmt.Sprintf("%d", user.GetID()),
AvatarURL: user.GetAvatarURL(),
GravatarID: user.GetGravatarID(),
Name: user.GetName(),
Company: user.GetCompany(),
Location: user.GetLocation(),
Email: user.GetEmail(),
Hireable: fmt.Sprintf("%t", user.GetHireable()),
Bio: user.GetBio(),
PublicRepos: fmt.Sprintf("%d", user.GetPublicRepos()),
PublicGists: fmt.Sprintf("%d", user.GetPublicGists()),
Followers: fmt.Sprintf("%d", user.GetFollowers()),
Following: fmt.Sprintf("%d", user.GetFollowing()),
CreatedAt: user.GetCreatedAt().String(),
UpdatedAt: user.GetUpdatedAt().String(),
SuspendedAt: user.GetSuspendedAt().String(),
TotalPrivateRepos: fmt.Sprintf("%d", user.GetTotalPrivateRepos()),
PrivateGists: fmt.Sprintf("%d", user.GetPrivateGists()),
DiskUsage: fmt.Sprintf("%d", user.GetDiskUsage()),
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
Plan: user.GetPlan().GetName(),
}
r.PrintInfo("Username", u.Username)
r.PrintInfo("ID", u.ID)
r.PrintInfo("Avatar URL", u.AvatarURL)
r.PrintInfo("Gravatar ID", u.GravatarID)
r.PrintInfo("Name", u.Name)
r.PrintInfo("Company", u.Company)
r.PrintInfo("Location", u.Location)
r.PrintInfo("Email", u.Email)
r.PrintInfo("Hireable", u.Hireable)
r.PrintInfo("Bio", u.Bio)
r.PrintInfo("Public Repos", u.PublicRepos)
r.PrintInfo("Public Gists", u.PublicGists)
r.PrintInfo("Followers", u.Followers)
r.PrintInfo("Following", u.Following)
r.PrintInfo("Created At", u.CreatedAt)
r.PrintInfo("Updated At", u.UpdatedAt)
r.PrintInfo("Suspended At", u.SuspendedAt)
r.PrintInfo("Total Private Repos", u.TotalPrivateRepos)
r.PrintInfo("Private Gists", u.PrivateGists)
r.PrintInfo("Disk Usage", u.DiskUsage)
r.PrintInfo("Collaborators", u.Collaborators)
r.PrintInfo("Plan", u.Plan)
r.PrintNewline()
WaitForRateLimit(resp)
return u
}
+70 -7
View File
@@ -1,9 +1,11 @@
package main
package ghrecon
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
@@ -22,12 +24,17 @@ var (
RedStyle = lipgloss.NewStyle().Foreground(Red)
)
func header() {
func (r Recon) Header() {
if r.silent {
return
}
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
fmt.Println(GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")))
fmt.Println(
GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")),
)
}
func parseUsername(username string) error {
func ParseUsername(username string) error {
if username == "" {
return fmt.Errorf("username is required")
}
@@ -83,11 +90,37 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
return bodyBytes, nil
}
func PrintInfo(key, value string) {
if value == "" || value == "0" || value == "0001-01-01 00:00:00 +0000 UTC" {
func (r Recon) PrintNewline() {
if r.silent {
return
}
fmt.Printf("%s %s\n", GreyStyle.Render(key+":"), GreenStyle.Render(value))
fmt.Println()
}
func (r Recon) PrintTitle(title string) {
if r.silent {
return
}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
fmt.Println(style.Render(title) + "\n")
}
func (r Recon) PrintInfo(key, value string, more ...string) {
if r.silent {
return
}
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
return
}
if strings.HasSuffix(key, "n°") {
fmt.Printf(" %s %s", GreyStyle.Render(key), value)
} else {
fmt.Printf(" %s %s", GreyStyle.Render(key+":"), value)
}
if len(more) > 0 {
fmt.Printf(" %s", GreyStyle.Render("("+strings.Join(more, ", ")+")"))
}
fmt.Println()
}
func WaitForRateLimit(resp *github.Response) {
@@ -98,3 +131,33 @@ func WaitForRateLimit(resp *github.Response) {
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second)
}
}
func SkipResult(name, email string) bool {
if name == "github-actions[bot]" || name == "github-actions" {
return true
}
if email == "github-actions[bot]@users.noreply.github.com" ||
email == "[email protected]" {
return true
}
return false
}
func (r Recon) WriteJson(data any) {
if r.jsonFile == "" {
return
}
file, err := os.Create(r.jsonFile)
if err != nil {
r.logger.Error("Failed to create JSON file", "err", err)
return
}
defer 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)
return
}
r.PrintInfo("INFO", "JSON file created successfully", "file", r.jsonFile)
}
+1
View File
@@ -6,6 +6,7 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.1
github.com/google/go-github/v72 v72.0.0
github.com/spf13/pflag v1.0.6
)
require (
+2
View File
@@ -36,6 +36,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
-140
View File
@@ -1,140 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func keys(client *github.Client, ctx context.Context, username string) {
sshKeys, resp, err := client.Users.ListKeys(ctx, username, nil)
if err != nil {
log.Error("Failed to fetch user's keys", "err", err)
os.Exit(1)
}
if len(sshKeys) == 0 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("No keys found for user "+username),
)
} else {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("+")+
GreyStyle.Render("]"),
GreyStyle.Render("Keys:\n"),
)
}
for _, key := range sshKeys {
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("URL", key.GetURL())
PrintInfo("Title", key.GetTitle())
PrintInfo("Created At", key.GetCreatedAt().String())
PrintInfo("Key", key.GetKey())
PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly()))
PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified()))
PrintInfo("Last Used", key.GetLastUsed().String())
PrintInfo("Added By", key.GetAddedBy())
fmt.Println()
}
WaitForRateLimit(resp)
gpgKeys, resp, err := client.Users.ListGPGKeys(ctx, username, nil)
if err != nil {
log.Error("Failed to fetch user's GPG keys", "err", err)
}
if len(gpgKeys) == 0 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("No GPG keys found\n"),
)
} else {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("+")+
GreyStyle.Render("]"),
GreyStyle.Render("GPG Keys:\n"),
)
}
for _, key := range gpgKeys {
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("Key ID", key.GetKeyID())
PrintInfo("Public Key", key.GetPublicKey())
PrintInfo("Created At", key.GetCreatedAt().String())
PrintInfo("Expires At", key.GetExpiresAt().String())
PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign()))
PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms()))
PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage()))
PrintInfo("Can Certify", fmt.Sprintf("%t", key.GetCanCertify()))
PrintInfo("Primary Key ID", fmt.Sprintf("%d", key.GetPrimaryKeyID()))
PrintInfo("Raw Key", key.GetRawKey())
PrintInfo("Emails", fmt.Sprintf("%d", len(key.Emails)))
for _, email := range key.Emails {
PrintInfo("\tEmail", email.GetEmail())
PrintInfo("\tVerified", fmt.Sprintf("%t", email.GetVerified()))
}
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys)))
for _, subkey := range key.Subkeys {
PrintInfo("\tSubkey ID", fmt.Sprintf("%d", subkey.GetID()))
PrintInfo("\tSubkey Key ID", subkey.GetKeyID())
PrintInfo("\tSubkey Created At", subkey.GetCreatedAt().String())
PrintInfo("\tSubkey Expires At", subkey.GetExpiresAt().String())
PrintInfo("\tSubkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign()))
PrintInfo(
"\tSubkey Can Encrypt Comms",
fmt.Sprintf("%t", subkey.GetCanEncryptComms()),
)
PrintInfo(
"\tSubkey Can Encrypt Storage",
fmt.Sprintf("%t", subkey.GetCanEncryptStorage()),
)
PrintInfo("\tSubkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify()))
PrintInfo("\tSubkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID()))
PrintInfo("\tSubkey Raw Key", subkey.GetRawKey())
PrintInfo("\tSubkey Public Key", subkey.GetPublicKey())
}
fmt.Println()
}
WaitForRateLimit(resp)
signingKeys, resp, err := client.Users.ListSSHSigningKeys(
ctx,
username,
nil,
)
if err != nil {
log.Error("Failed to fetch user's signing keys", "err", err)
}
if len(signingKeys) == 0 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("No signing keys found\n"),
)
} else {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("+")+
GreyStyle.Render("]"),
GreyStyle.Render("Signing Keys:\n"),
)
}
for _, key := range signingKeys {
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("Key", key.GetKey())
PrintInfo("Title", key.GetTitle())
PrintInfo("Created At", key.GetCreatedAt().String())
fmt.Println()
}
WaitForRateLimit(resp)
}
+111 -20
View File
@@ -2,46 +2,137 @@ package main
import (
"context"
"flag"
"fmt"
"os"
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
flag "github.com/spf13/pflag"
)
func main() {
var username string
var token string
flag.StringVar(&username, "username", "", "Target username")
flag.StringVar(&token, "token", "", "Github token")
var onlyCommitsLeak bool
var fromEmail string
var deep bool
var silent bool
var jsonFile string
var excludeRepos string
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",
"d",
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.StringVar(
&excludeRepos,
"exclude-repo",
"",
"Exclude repos from deep scan (comma-separated list)",
)
flag.Parse()
if username == "" {
fmt.Println("Please provide a username with the --username flag")
os.Exit(1)
}
err := parseUsername(username)
if err != nil {
log.Error("Invalid username", "err", err)
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)
if username == "" && fromEmail == "" {
logger.Error(
"Please provide a username with the --username (-u) flag or an email with the --email (-e) flag",
)
os.Exit(1)
} else if username != "" {
if err := ghrecon.ParseUsername(username); err != nil {
logger.Error("Invalid username", "err", err)
os.Exit(1)
}
}
client := github.NewClient(nil)
if token == "" {
log.Info(
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
)
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.",
)
}
} else {
client = client.WithAuthToken(token)
}
ctx := context.Background()
header()
userInfo(client, ctx, username)
orgs(client, ctx, username)
keys(client, ctx, username)
socials(client, username)
commits(client, ctx, username)
r := ghrecon.NewRecon(
client,
logger,
ctx,
silent,
jsonFile,
)
r.Header()
if fromEmail != "" {
emailsInfo := r.Email(fromEmail)
r.WriteJson(
map[string]any{
"Authors": emailsInfo,
},
)
return
}
if onlyCommitsLeak {
commitsInfo := r.Commits(username)
r.WriteJson(
map[string]any{
"Authors": commitsInfo,
},
)
return
}
userInfo := r.User(username)
orgsInfo := r.Orgs(username)
sshKeysInfo := r.SshKeys(username)
gpgKeysInfo := r.GpgKeys(username)
sshSigningKeysInfo := r.SshSigningKeys(username)
socialsInfo := r.Socials(username)
closeFriendsInfo := r.CloseFriends(username)
commitsInfo := r.Commits(username)
results := map[string]any{
"User": userInfo,
"Orgs": orgsInfo,
"SSHKeys": sshKeysInfo,
"GPGKeys": gpgKeysInfo,
"SSHSigningKeys": sshSigningKeysInfo,
"Socials": socialsInfo,
"Commits": commitsInfo,
"CloseFriends": closeFriendsInfo,
}
if deep {
results["Deep"] = r.Deep(username, excludeRepos)
}
r.WriteJson(results)
}
-47
View File
@@ -1,47 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func orgs(client *github.Client, ctx context.Context, username string) {
orgs, resp, err := client.Organizations.List(ctx, username, nil)
if err != nil {
log.Error("Failed to fetch user's organizations", "err", err)
}
if len(orgs) == 0 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("No organizations found for user "+username),
)
} else {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("+")+
GreyStyle.Render("]"),
GreyStyle.Render("Organizations:\n"),
)
}
for _, org := range orgs {
PrintInfo("Login", org.GetLogin())
PrintInfo("ID", fmt.Sprintf("%d", org.GetID()))
PrintInfo("Node ID", org.GetNodeID())
PrintInfo("URL", org.GetURL())
PrintInfo("Repos URL", org.GetReposURL())
PrintInfo("Events URL", org.GetEventsURL())
PrintInfo("Hooks URL", org.GetHooksURL())
PrintInfo("Issues URL", org.GetIssuesURL())
PrintInfo("Members URL", org.GetMembersURL())
PrintInfo("Public Members URL", org.GetPublicMembersURL())
PrintInfo("Avatar URL", org.GetAvatarURL())
PrintInfo("Description", org.GetDescription())
fmt.Println()
}
WaitForRateLimit(resp)
}
-48
View File
@@ -1,48 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func socials(client *github.Client, username string) {
resp, err := FetchGitHubAPI(client, "", "/users/"+username+"/social_accounts")
if err != nil {
log.Error("Failed to fetch user's social media accounts", "err", err)
}
type SocialAccount struct {
Provider string `json:"provider"`
URL string `json:"url"`
}
type SocialAccounts []SocialAccount
var socialAccounts SocialAccounts
err = json.Unmarshal(resp, &socialAccounts)
if err != nil {
log.Error("Failed to unmarshal social media accounts", "err", err)
}
if len(socialAccounts) == 0 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("No social media accounts found\n"),
)
} else {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("+")+
GreyStyle.Render("]"),
GreyStyle.Render("Social Media Accounts:\n"),
)
}
for _, account := range socialAccounts {
PrintInfo("Provider", account.Provider)
PrintInfo("URL", account.URL)
fmt.Println()
}
}
-64
View File
@@ -1,64 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func userInfo(client *github.Client, ctx context.Context, username string) {
fmt.Println(
GreyStyle.Render("[")+
GreenStyle.Render("-")+
GreyStyle.Render("]"),
GreyStyle.Render("Fetching user info...\n"),
)
user, resp, err := client.Users.Get(ctx, username)
if resp.StatusCode == 404 {
fmt.Println(
GreyStyle.Render("[")+
RedStyle.Render("x")+
GreyStyle.Render("]"),
GreyStyle.Render("Error:"),
RedStyle.Render("User not found"),
)
os.Exit(1)
}
if err != nil {
log.Error("Failed to fetch user's information", "err", err)
os.Exit(1)
}
PrintInfo("Username", user.GetLogin())
PrintInfo("ID", fmt.Sprintf("%d", user.GetID()))
PrintInfo("Avatar URL", user.GetAvatarURL())
PrintInfo("Gravatar ID", user.GetGravatarID())
PrintInfo("Name", user.GetName())
PrintInfo("Company", user.GetCompany())
PrintInfo("Location", user.GetLocation())
PrintInfo("Email", user.GetEmail())
PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable()))
PrintInfo("Bio", user.GetBio())
PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos()))
PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists()))
PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers()))
PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing()))
PrintInfo("Created At", user.GetCreatedAt().String())
PrintInfo("Updated At", user.GetUpdatedAt().String())
PrintInfo("Suspended At", user.GetSuspendedAt().String())
PrintInfo("Type", user.GetType())
PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin()))
PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos()))
PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos()))
PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists()))
PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage()))
PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators()))
PrintInfo("Plan", user.GetPlan().GetName())
fmt.Println()
WaitForRateLimit(resp)
}