Compare commits

...
2 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
15 changed files with 846 additions and 217 deletions
+28 -5
View File
@@ -17,6 +17,10 @@ Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and th
- Fetch SSH and GPG keys - Fetch SSH and GPG keys
- Enumerate social accounts - Enumerate social accounts
- Extract unique commit authors (name + email) in both chronological orders - 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 ## Disclaimer
@@ -25,7 +29,7 @@ This tool is intended for educational purposes only. Use responsibly and ensure
## Prerequisites ## Prerequisites
- Go 1.18+ - 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 ## Installation
@@ -67,18 +71,37 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
### Flags ### Flags
- `--username`: GitHub username to inspect (required)
- `--token`: Personal Access Token (optional but recommended) - `--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 ## Example
```bash ```bash
gh-recon --username anotherhadi --token ghp_ABC123... gh-recon --username anotherhadi --token ghp_ABC123...
gh-recon --email [email protected] --token ghp_ABC123...
gh-recon --username anotherhadi --json output.json --deep
``` ```
## Todo ## Todo
Feel free to contribute! Here are some ideas: Feel free to contribute!
- Fetch names in License files **Todo:**
- Fetch emails in README files/comments
- Find and parse licenses
+1 -1
View File
@@ -13,7 +13,7 @@
(system: f system (import nixpkgs { inherit system; })); (system: f system (import nixpkgs { inherit system; }));
pname = "gh-recon"; pname = "gh-recon";
version = "0.1.0"; version = "0.2.0";
ldflags = [ "-s" "-w" ]; 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
}
+60 -39
View File
@@ -6,66 +6,87 @@ import (
"github.com/google/go-github/v72/github" "github.com/google/go-github/v72/github"
) )
func (r Recon) Commits(username string) { type CommitsResult struct {
PrintTitle("🐙 Commits") Name string
Email string
Occurences int
FirstFoundIn string
}
seenNames := make(map[string]struct{}) func (r Recon) Commits(username string) (response []CommitsResult) {
seenEmails := make(map[string]struct{}) r.PrintTitle("🐙 Commits")
var names, emails []string
collect := func(order string) error { results := make(map[string]CommitsResult)
opts := &github.SearchOptions{
Sort: "author-date", collect := func(date string) error {
Order: order,
ListOptions: github.ListOptions{PerPage: 100},
}
for page := 1; page <= 10; page++ { for page := 1; page <= 10; page++ {
opts.ListOptions.Page = page
result, resp, err := r.client.Search.Commits( result, resp, err := r.client.Search.Commits(
r.ctx, r.ctx,
fmt.Sprintf("author:%s", username), fmt.Sprintf("author:%s author-date:%s", username, date),
opts, &github.SearchOptions{
Sort: "author-date",
Order: "desc",
ListOptions: github.ListOptions{PerPage: 100, Page: page},
},
) )
if err != nil { if err != nil {
return fmt.Errorf("fetch page %d (%s): %w", page, order, err) return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
} }
WaitForRateLimit(resp) WaitForRateLimit(resp)
if len(result.Commits) == 0 { if len(result.Commits) == 0 {
break break
} }
for _, item := range result.Commits { for _, item := range result.Commits {
a := item.Commit.GetAuthor() name := item.Commit.GetAuthor().GetName()
name := a.GetName() email := item.Commit.GetAuthor().GetEmail()
email := a.GetEmail() if SkipResult(name, email) {
if _, seen := seenNames[name]; !seen { // continue
seenNames[name] = struct{}{}
names = append(names, name)
PrintInfo(
"Name "+fmt.Sprint(len(names)),
name,
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
GetName(),
)
} }
if _, seen := seenEmails[email]; !seen { if _, seen := results[name+" - "+email]; !seen {
seenEmails[email] = struct{}{} author := CommitsResult{
emails = append(emails, email) Name: name,
PrintInfo( Email: email,
"Email "+fmt.Sprint(len(emails)), Occurences: 1,
email, FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
GetName(), GetName(),
) }
results[name+" - "+email] = author
} else {
result := results[name+" - "+email]
result.Occurences++
results[name+" - "+email] = result
} }
} }
} }
return nil return nil
} }
if err := collect("desc"); err != nil { // Range of dates to bypass the limit of 1000 results
r.logger.Error("Failed to fetch commits", "err", err, "order", "desc") 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)
} }
if err := collect("asc"); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "order", "asc")
} }
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
}
+139 -67
View File
@@ -4,88 +4,151 @@ import (
"fmt" "fmt"
) )
func (r Recon) SshKeys(username string) { 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) sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil)
if err != 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 { } else if len(sshKeys) == 0 {
PrintTitle("🔑 SSH Keys") r.PrintTitle("🔑 SSH Keys")
r.logger.Info("No SSH Keys found\n") r.PrintInfo("INFO", "No SSH Keys found")
} else { } else {
PrintTitle("🔑 SSH Keys") r.PrintTitle("🔑 SSH Keys")
for i, key := range sshKeys { for i, key := range sshKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i)) k := SSHKeyResult{
PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) ID: fmt.Sprintf("%d", key.GetID()),
PrintInfo("URL", key.GetURL()) Url: key.GetURL(),
PrintInfo("Title", key.GetTitle()) Title: key.GetTitle(),
PrintInfo("Created At", key.GetCreatedAt().String()) CreatedAt: key.GetCreatedAt().String(),
PrintInfo("Key", key.GetKey()) Key: key.GetKey(),
PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly())) ReadOnly: fmt.Sprintf("%t", key.GetReadOnly()),
PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified())) Verified: fmt.Sprintf("%t", key.GetVerified()),
PrintInfo("Last Used", key.GetLastUsed().String()) LastUsed: key.GetLastUsed().String(),
PrintInfo("Added By", key.GetAddedBy()) AddedBy: key.GetAddedBy(),
fmt.Println() }
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) WaitForRateLimit(resp)
return
} }
func (r Recon) GpgKeys(username string) { 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) gpgKeys, resp, err := r.client.Users.ListGPGKeys(r.ctx, username, nil)
if err != 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 { } else if len(gpgKeys) == 0 {
PrintTitle("🗝️ GPG Keys") r.PrintTitle("🗝️ GPG Keys")
r.logger.Info("No GPG Keys found\n") r.PrintInfo("INFO", "No GPG Keys found")
} else { } else {
PrintTitle("🗝️ GPG Keys") r.PrintTitle("🗝️ GPG Keys")
for i, key := range gpgKeys { for i, key := range gpgKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i)) k := GPGKeyResult{
PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) ID: fmt.Sprintf("%d", key.GetID()),
PrintInfo("Key ID", key.GetKeyID()) KeyID: key.GetKeyID(),
PrintInfo("Public Key", key.GetPublicKey()) PublicKey: key.GetPublicKey(),
PrintInfo("Created At", key.GetCreatedAt().String()) CreatedAt: key.GetCreatedAt().String(),
PrintInfo("Expires At", key.GetExpiresAt().String()) PrimaryKeyID: fmt.Sprintf("%d", key.GetPrimaryKeyID()),
PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign())) RawKey: key.GetRawKey(),
PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms())) Emails: []GPGKeyEmail{},
PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage())) Subkeys: []GPGKeyResult{},
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 j, email := range key.Emails {
PrintInfo(" Email n°", fmt.Sprintf("%d", j))
PrintInfo(" Email", email.GetEmail())
PrintInfo(" Verified", fmt.Sprintf("%t", email.GetVerified()))
} }
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys))) for _, email := range key.Emails {
for j, subkey := range key.Subkeys { email := GPGKeyEmail{
PrintInfo(" Subkey n°", fmt.Sprintf("%d", j)) Email: email.GetEmail(),
PrintInfo(" Subkey ID", fmt.Sprintf("%d", subkey.GetID())) Verified: fmt.Sprintf("%t", email.GetVerified()),
PrintInfo(" Subkey Key ID", subkey.GetKeyID())
PrintInfo(" Subkey Created At", subkey.GetCreatedAt().String())
PrintInfo(" Subkey Expires At", subkey.GetExpiresAt().String())
PrintInfo(" Subkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign()))
PrintInfo(
" Subkey Can Encrypt Comms",
fmt.Sprintf("%t", subkey.GetCanEncryptComms()),
)
PrintInfo(
" Subkey Can Encrypt Storage",
fmt.Sprintf("%t", subkey.GetCanEncryptStorage()),
)
PrintInfo(" Subkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify()))
PrintInfo(" Subkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID()))
PrintInfo(" Subkey Raw Key", subkey.GetRawKey())
PrintInfo(" Subkey Public Key", subkey.GetPublicKey())
} }
fmt.Println() 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) WaitForRateLimit(resp)
return
} }
func (r Recon) SshSigningKeys(username string) { 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( signingKeys, resp, err := r.client.Users.ListSSHSigningKeys(
r.ctx, r.ctx,
username, username,
@@ -94,18 +157,27 @@ func (r Recon) SshSigningKeys(username string) {
if err != 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 { } else if len(signingKeys) == 0 {
PrintTitle("📝 SSH Signing Keys") r.PrintTitle("📝 SSH Signing Keys")
r.logger.Info("No SSH Signing Keys found\n") r.PrintInfo("INFO", "No SSH Signing Keys found")
} else { } else {
PrintTitle("📝 SSH Signing Keys") r.PrintTitle("📝 SSH Signing Keys")
for i, key := range signingKeys { for i, key := range signingKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i)) k := SSHSigningKeyResult{
PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) ID: fmt.Sprintf("%d", key.GetID()),
PrintInfo("Key", key.GetKey()) Title: key.GetTitle(),
PrintInfo("Title", key.GetTitle()) CreatedAt: key.GetCreatedAt().String(),
PrintInfo("Created At", key.GetCreatedAt().String()) Key: key.GetKey(),
fmt.Println() }
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) WaitForRateLimit(resp)
r.PrintNewline()
return response
} }
+11 -1
View File
@@ -11,12 +11,22 @@ type Recon struct {
client *github.Client client *github.Client
logger *log.Logger logger *log.Logger
ctx context.Context ctx context.Context
silent bool
jsonFile string
} }
func NewRecon(client *github.Client, logger *log.Logger, ctx context.Context) *Recon { func NewRecon(
client *github.Client,
logger *log.Logger,
ctx context.Context,
silent bool,
jsonFile string,
) *Recon {
return &Recon{ return &Recon{
client: client, client: client,
logger: logger, logger: logger,
ctx: ctx, ctx: ctx,
silent: silent,
jsonFile: jsonFile,
} }
} }
+26 -18
View File
@@ -4,31 +4,39 @@ import (
"fmt" "fmt"
) )
func (r Recon) Orgs(username string) { 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) orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil)
if err != 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 { } else if len(orgs) == 0 {
PrintTitle("🏢 Organizations") r.PrintTitle("🏢 Organizations")
r.logger.Info("No Organizations found\n") r.PrintInfo("INFO", "No Organizations found")
} else { } else {
PrintTitle("🏢 Organizations") r.PrintTitle("🏢 Organizations")
for i, org := range orgs { for i, org := range orgs {
PrintInfo("Orgs n°", fmt.Sprintf("%d", i)) o := OrgResult{
PrintInfo("Login", org.GetLogin()) Login: org.GetLogin(),
PrintInfo("ID", fmt.Sprintf("%d", org.GetID())) ID: fmt.Sprintf("%d", org.GetID()),
PrintInfo("Node ID", org.GetNodeID()) URL: org.GetURL(),
PrintInfo("URL", org.GetURL()) Description: org.GetDescription(),
PrintInfo("Repos URL", org.GetReposURL()) }
PrintInfo("Events URL", org.GetEventsURL()) r.PrintInfo("Organization n°", fmt.Sprintf("%d", i))
PrintInfo("Hooks URL", org.GetHooksURL()) r.PrintInfo("Login", o.Login)
PrintInfo("Issues URL", org.GetIssuesURL()) r.PrintInfo("ID", o.ID)
PrintInfo("Members URL", org.GetMembersURL()) r.PrintInfo("URL", o.URL)
PrintInfo("Public Members URL", org.GetPublicMembersURL()) r.PrintInfo("Description", o.Description)
PrintInfo("Avatar URL", org.GetAvatarURL()) r.PrintNewline()
PrintInfo("Description", org.GetDescription()) response = append(response, o)
fmt.Println()
} }
} }
r.PrintNewline()
WaitForRateLimit(resp) WaitForRateLimit(resp)
return
} }
+16 -15
View File
@@ -5,20 +5,19 @@ import (
"fmt" "fmt"
) )
func (r Recon) Socials(username string) { 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") resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
if err != nil { if err != nil {
r.logger.Error("Failed to fetch socials", "err", err) r.logger.Error("Failed to fetch socials", "err", err)
return return
} }
type SocialAccount struct { var socialAccounts []SocialResult
Provider string `json:"provider"`
URL string `json:"url"`
}
type SocialAccounts []SocialAccount
var socialAccounts SocialAccounts
err = json.Unmarshal(resp, &socialAccounts) err = json.Unmarshal(resp, &socialAccounts)
if err != nil { if err != nil {
r.logger.Error("Failed to unmarshal socials", "err", err) r.logger.Error("Failed to unmarshal socials", "err", err)
@@ -26,15 +25,17 @@ func (r Recon) Socials(username string) {
} }
if len(socialAccounts) == 0 { if len(socialAccounts) == 0 {
PrintTitle("🐥 Socials") r.PrintTitle("🐥 Socials")
PrintTitle("No Socials found\n") r.PrintInfo("INFO", "No commits found")
} else { } else {
PrintTitle("🐥 Socials") r.PrintTitle("🐥 Socials")
for i, account := range socialAccounts { for i, account := range socialAccounts {
PrintInfo("Social n°", fmt.Sprintf("%d", i)) r.PrintInfo("Social n°", fmt.Sprintf("%d", i))
PrintInfo("Provider", account.Provider) r.PrintInfo("Provider", account.Provider)
PrintInfo("URL", account.URL) r.PrintInfo("URL", account.URL)
fmt.Println()
} }
} }
r.PrintNewline()
return socialAccounts
} }
+75 -28
View File
@@ -4,7 +4,32 @@ import (
"fmt" "fmt"
) )
func (r Recon) User(username string) { 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) user, resp, err := r.client.Users.Get(r.ctx, username)
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
r.logger.Fatal("User not found") r.logger.Fatal("User not found")
@@ -13,33 +38,55 @@ func (r Recon) User(username string) {
r.logger.Fatal("Failed to fetch user's information", "err", err) r.logger.Fatal("Failed to fetch user's information", "err", err)
} }
PrintTitle("👤 User informations") r.PrintTitle("👤 User informations")
PrintInfo("Username", user.GetLogin()) u := UserResult{
PrintInfo("ID", fmt.Sprintf("%d", user.GetID())) Username: user.GetLogin(),
PrintInfo("Avatar URL", user.GetAvatarURL()) ID: fmt.Sprintf("%d", user.GetID()),
PrintInfo("Gravatar ID", user.GetGravatarID()) AvatarURL: user.GetAvatarURL(),
PrintInfo("Name", user.GetName()) GravatarID: user.GetGravatarID(),
PrintInfo("Company", user.GetCompany()) Name: user.GetName(),
PrintInfo("Location", user.GetLocation()) Company: user.GetCompany(),
PrintInfo("Email", user.GetEmail()) Location: user.GetLocation(),
PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable())) Email: user.GetEmail(),
PrintInfo("Bio", user.GetBio()) Hireable: fmt.Sprintf("%t", user.GetHireable()),
PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos())) Bio: user.GetBio(),
PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists())) PublicRepos: fmt.Sprintf("%d", user.GetPublicRepos()),
PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers())) PublicGists: fmt.Sprintf("%d", user.GetPublicGists()),
PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing())) Followers: fmt.Sprintf("%d", user.GetFollowers()),
PrintInfo("Created At", user.GetCreatedAt().String()) Following: fmt.Sprintf("%d", user.GetFollowing()),
PrintInfo("Updated At", user.GetUpdatedAt().String()) CreatedAt: user.GetCreatedAt().String(),
PrintInfo("Suspended At", user.GetSuspendedAt().String()) UpdatedAt: user.GetUpdatedAt().String(),
PrintInfo("Type", user.GetType()) SuspendedAt: user.GetSuspendedAt().String(),
PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin())) TotalPrivateRepos: fmt.Sprintf("%d", user.GetTotalPrivateRepos()),
PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos())) PrivateGists: fmt.Sprintf("%d", user.GetPrivateGists()),
PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos())) DiskUsage: fmt.Sprintf("%d", user.GetDiskUsage()),
PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists())) Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage())) Plan: user.GetPlan().GetName(),
PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators())) }
PrintInfo("Plan", user.GetPlan().GetName()) r.PrintInfo("Username", u.Username)
fmt.Println() 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) WaitForRateLimit(resp)
return u
} }
+54 -4
View File
@@ -1,9 +1,11 @@
package ghrecon package ghrecon
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
@@ -22,9 +24,14 @@ var (
RedStyle = lipgloss.NewStyle().Foreground(Red) RedStyle = lipgloss.NewStyle().Foreground(Red)
) )
func Header() { func (r Recon) Header() {
if r.silent {
return
}
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ " 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 {
@@ -83,12 +90,25 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
return bodyBytes, nil return bodyBytes, nil
} }
func PrintTitle(title string) { func (r Recon) PrintNewline() {
if r.silent {
return
}
fmt.Println()
}
func (r Recon) PrintTitle(title string) {
if r.silent {
return
}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd")) style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
fmt.Println(style.Render(title) + "\n") fmt.Println(style.Render(title) + "\n")
} }
func PrintInfo(key, value string, more ...string) { func (r Recon) PrintInfo(key, value string, more ...string) {
if r.silent {
return
}
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" { if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
return return
} }
@@ -111,3 +131,33 @@ func WaitForRateLimit(resp *github.Response) {
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second) 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/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.1 github.com/charmbracelet/log v0.4.1
github.com/google/go-github/v72 v72.0.0 github.com/google/go-github/v72 v72.0.0
github.com/spf13/pflag v1.0.6
) )
require ( 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+107 -33
View File
@@ -2,42 +2,49 @@ package main
import ( import (
"context" "context"
"flag"
"fmt"
"os" "os"
ghrecon "github.com/anotherhadi/gh-recon/gh-recon" ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/google/go-github/v72/github" "github.com/google/go-github/v72/github"
flag "github.com/spf13/pflag"
) )
func main() { func main() {
var username string var username string
var token string var token string
flag.StringVar(&username, "username", "", "Target username") var onlyCommitsLeak bool
flag.StringVar(&token, "token", "", "Github token") var fromEmail string
flag.Parse() var deep bool
var silent bool
if username == "" { var jsonFile string
fmt.Println("Please provide a username with the --username flag") var excludeRepos string
os.Exit(1) flag.StringVarP(&username, "username", "u", "", "GitHub username to analyze")
} flag.StringVarP(&token, "token", "t", "", "GitHub personal access token (e.g. ghp_...)")
err := ghrecon.ParseUsername(username) flag.StringVarP(&fromEmail, "email", "e", "", "Search accounts by email address")
if err != nil { flag.BoolVarP(
log.Error("Invalid username", "err", err) &onlyCommitsLeak,
os.Exit(1) "only-commits",
} "c",
false,
client := github.NewClient(nil) "Display only commits with author info",
if token == "" {
log.Info(
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
) )
} else { flag.BoolVarP(
client = client.WithAuthToken(token) &deep,
} "deep",
"d",
ctx := context.Background() 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()
styles := log.DefaultStyles() styles := log.DefaultStyles()
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey) styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
@@ -47,18 +54,85 @@ func main() {
}) })
logger.SetStyles(styles) 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 == "" {
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()
r := ghrecon.NewRecon( r := ghrecon.NewRecon(
client, client,
logger, logger,
ctx, ctx,
silent,
jsonFile,
) )
ghrecon.Header() r.Header()
r.User(username)
r.Orgs(username)
r.SshKeys(username) if fromEmail != "" {
r.GpgKeys(username) emailsInfo := r.Email(fromEmail)
r.SshSigningKeys(username) r.WriteJson(
r.Socials(username) map[string]any{
r.Commits(username) "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)
} }