Compare commits

...
13 Commits
Author SHA1 Message Date
Hadi e572d1326a early return if user not found
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:39:32 +02:00
Hadi 0017d649d3 return nil instead of error
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:31:02 +02:00
Hadi c163fb4ecd Fatal only in cmd
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:26:54 +02:00
Hadi 325397dfef Check repositories length
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:10:35 +02:00
Hadi dab0ba2b38 now retrying to get commits
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:07:16 +02:00
Hadi 98769561c3 Add author.email in levenshtein distance for authors
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:47:12 +02:00
Hadi c76953882c Nil when no result & better error handling
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:39:53 +02:00
Hadi 94fd83ecce fix print with private interface
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:26:31 +02:00
Hadi 2d01c1639f Fix isDir condition
Signed-off-by: Hadi <[email protected]>
2025-09-17 19:47:50 +02:00
Hadi 66030c1c16 add details to close friends
Signed-off-by: Hadi <[email protected]>
2025-09-17 19:31:54 +02:00
Hadi bb8f34055d fix nixos installation step
Signed-off-by: Hadi <[email protected]>
2025-09-16 19:35:38 +02:00
Hadi 26a1cfdd37 Change the way token is retrieve
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:54:51 +02:00
Hadi 3064cdbf7a Change/get defaults
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:40:55 +02:00
11 changed files with 116 additions and 61 deletions
+2 -2
View File
@@ -98,7 +98,7 @@ nix run github:anotherhadi/github-recon -- [--flags value] target_username_or_em
# then add it to your packages # then add it to your packages
environment.systemPackages = with pkgs; [ # or home.packages environment.systemPackages = with pkgs; [ # or home.packages
github-recon inputs.github-recon.defaultPackage.${pkgs.system}
]; ];
``` ```
@@ -123,7 +123,7 @@ github-recon [--flags value] target_username_or_email
--trufflehog Run trufflehog on cloned repositories (only for deep scan) (default true) --trufflehog Run trufflehog on cloned repositories (only for deep scan) (default true)
-S, --silent Suppress all non-essential output -S, --silent Suppress all non-essential output
--spoof-email Spoof email (only for email mode) (default true) --spoof-email Spoof email (only for email mode) (default true)
-a, --hide-avatar Hide the avatar in the output -a, --print-avatar Show the avatar in the output
-j, --json string Write results to specified JSON file -j, --json string Write results to specified JSON file
``` ```
+10 -2
View File
@@ -1,6 +1,8 @@
package main package main
import ( import (
"log"
recon_email "github.com/anotherhadi/github-recon/github-recon/email" recon_email "github.com/anotherhadi/github-recon/github-recon/email"
recon_username "github.com/anotherhadi/github-recon/github-recon/username" recon_username "github.com/anotherhadi/github-recon/github-recon/username"
github_recon_settings "github.com/anotherhadi/github-recon/settings" github_recon_settings "github.com/anotherhadi/github-recon/settings"
@@ -8,7 +10,10 @@ import (
) )
func main() { func main() {
settings := github_recon_settings.GetSettings() settings, err := github_recon_settings.GetSettings()
if err != nil {
log.Fatal(err)
}
if !settings.Silent { if !settings.Silent {
utils.Header() utils.Header()
@@ -23,7 +28,10 @@ func main() {
} }
if settings.TargetType == github_recon_settings.TargetUsername { if settings.TargetType == github_recon_settings.TargetUsername {
result := recon_username.Username(settings) result, err := recon_username.Username(settings)
if err != nil {
log.Fatal(err)
}
writeJson(settings, result) writeJson(settings, result)
} else { } else {
result := recon_email.Email(settings) result := recon_email.Email(settings)
+2 -2
View File
@@ -13,7 +13,7 @@ type EmailResult struct {
TargetType github_recon_settings.TargetType TargetType github_recon_settings.TargetType
Commits CommitsResult Commits CommitsResult
Spoofing SpoofingResult Spoofing *SpoofingResult
} }
func Email(settings github_recon_settings.Settings) EmailResult { func Email(settings github_recon_settings.Settings) EmailResult {
@@ -33,7 +33,7 @@ func Email(settings github_recon_settings.Settings) EmailResult {
} else { } else {
utils.PrintTitle(settings.Silent, "🎭 Spoofing test") utils.PrintTitle(settings.Silent, "🎭 Spoofing test")
result.Spoofing = Spoofing(settings) result.Spoofing = Spoofing(settings)
if result.Spoofing.AvatarURL != "" { if result.Spoofing != nil && result.Spoofing.AvatarURL != "" {
utils.PrintAvatar(settings, result.Spoofing.AvatarURL) utils.PrintAvatar(settings, result.Spoofing.AvatarURL)
} }
utils.PrintStruct(settings, result.Spoofing, 0) utils.PrintStruct(settings, result.Spoofing, 0)
+20 -10
View File
@@ -2,6 +2,7 @@ package recon
import ( import (
"math/rand" "math/rand"
"time"
github_recon_settings "github.com/anotherhadi/github-recon/settings" github_recon_settings "github.com/anotherhadi/github-recon/settings"
"github.com/anotherhadi/github-recon/utils" "github.com/anotherhadi/github-recon/utils"
@@ -26,7 +27,8 @@ func RandomString(n int) string {
return string(b) return string(b)
} }
func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) { func Spoofing(s github_recon_settings.Settings) (response *SpoofingResult) {
response = &SpoofingResult{}
name := "gh-recon-spoofing-" + RandomString(8) name := "gh-recon-spoofing-" + RandomString(8)
private := true private := true
autoInit := true autoInit := true
@@ -41,6 +43,13 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
defer func() {
_, err = s.Client.Repositories.Delete(s.Ctx, repo.Owner.GetLogin(), name)
if err != nil {
s.Logger.Error("Error while deleting repo", "err", err)
}
}()
branch := repo.GetDefaultBranch() branch := repo.GetDefaultBranch()
if branch == "" { if branch == "" {
branch = "main" branch = "main"
@@ -57,7 +66,6 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
ref, resp, err := s.Client.Git.GetRef(s.Ctx, repo.Owner.GetLogin(), name, refName) ref, resp, err := s.Client.Git.GetRef(s.Ctx, repo.Owner.GetLogin(), name, refName)
if err != nil { if err != nil {
s.Logger.Error("Error while getting ref", "err", err) s.Logger.Error("Error while getting ref", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return return
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
@@ -65,7 +73,6 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
parentCommit, resp, err := s.Client.Git.GetCommit(s.Ctx, repo.Owner.GetLogin(), name, ref.GetObject().GetSHA()) parentCommit, resp, err := s.Client.Git.GetCommit(s.Ctx, repo.Owner.GetLogin(), name, ref.GetObject().GetSHA())
if err != nil { if err != nil {
s.Logger.Error("Error while getting parent commit", "err", err) s.Logger.Error("Error while getting parent commit", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return return
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
@@ -81,7 +88,6 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
newCommit, resp, err := s.Client.Git.CreateCommit(s.Ctx, repo.Owner.GetLogin(), name, commit, nil) newCommit, resp, err := s.Client.Git.CreateCommit(s.Ctx, repo.Owner.GetLogin(), name, commit, nil)
if err != nil { if err != nil {
s.Logger.Error("Error while creating spoofed empty commit", "err", err) s.Logger.Error("Error while creating spoofed empty commit", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return return
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
@@ -90,31 +96,35 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
_, resp, err = s.Client.Git.UpdateRef(s.Ctx, repo.Owner.GetLogin(), name, ref, false) _, resp, err = s.Client.Git.UpdateRef(s.Ctx, repo.Owner.GetLogin(), name, ref, false)
if err != nil { if err != nil {
s.Logger.Error("Error while updating ref to spoofed commit", "err", err) s.Logger.Error("Error while updating ref to spoofed commit", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return return
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
const maxRetries = 5
const retryDelay = 2 * time.Second
for i := 0; i < maxRetries; i++ {
commits, _, err := s.Client.Repositories.ListCommits(s.Ctx, repo.Owner.GetLogin(), name, nil) commits, _, err := s.Client.Repositories.ListCommits(s.Ctx, repo.Owner.GetLogin(), name, nil)
if err != nil { if err != nil {
s.Logger.Error("Error while listing commits", "err", err) s.Logger.Error("Error while listing commits", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return return
} }
if len(commits) > 0 { if len(commits) > 1 {
last := commits[0] last := commits[0]
response.Username = last.GetAuthor().GetLogin() response.Username = last.GetAuthor().GetLogin()
response.Name = last.GetAuthor().GetName() response.Name = last.GetAuthor().GetName()
response.Email = last.GetAuthor().GetEmail() response.Email = last.GetAuthor().GetEmail()
response.Url = last.GetAuthor().GetHTMLURL() response.Url = last.GetAuthor().GetHTMLURL()
response.AvatarURL = last.GetAuthor().GetAvatarURL() response.AvatarURL = last.GetAuthor().GetAvatarURL()
break
} }
_, err = s.Client.Repositories.Delete(s.Ctx, repo.Owner.GetLogin(), name) s.Logger.Info("Only one commit found, retrying...", "attempt", i+1)
if err != nil { time.Sleep(retryDelay)
s.Logger.Error("Error while deleting repo", "err", err)
} }
if response.Username == "" && response.Name == "" && response.Email == "" {
return nil
}
return return
} }
+17
View File
@@ -13,7 +13,11 @@ import (
type CloseFriendsResult []CloseFriendResult type CloseFriendsResult []CloseFriendResult
type CloseFriendResult struct { type CloseFriendResult struct {
Name string
Username string Username string
Orgs []string
Company string
Location string
Score int Score int
} }
@@ -96,8 +100,12 @@ func CloseFriends(s github_recon_settings.Settings) (results CloseFriendsResult)
// Add candidate if they matched at least one condition // Add candidate if they matched at least one condition
if score > 0 { if score > 0 {
results = append(results, CloseFriendResult{ results = append(results, CloseFriendResult{
Name: candidateDetails.GetName(),
Username: candidateLogin, Username: candidateLogin,
Orgs: candidateOrgsToNames(candidateOrgs),
Score: score, Score: score,
Location: candidateDetails.GetLocation(),
Company: candidateDetails.GetCompany(),
}) })
} }
} }
@@ -130,6 +138,15 @@ func checkIfUserFollows(s github_recon_settings.Settings, sourceUser, targetUser
} }
return isFollowing, nil return isFollowing, nil
} }
func candidateOrgsToNames(orgs []*github.Organization) []string {
var orgNames []string
for _, org := range orgs {
if org.GetLogin() != "" {
orgNames = append(orgNames, org.GetLogin())
}
}
return orgNames
}
func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) { func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) {
orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil) orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil)
+15 -3
View File
@@ -139,6 +139,11 @@ func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
} }
s.Logger.Info("Cloned all repositories", "path", tmp_folder) s.Logger.Info("Cloned all repositories", "path", tmp_folder)
if len(repositories) == 0 {
s.Logger.Info("No repositories found for the user, skipping deep scan.")
return
}
authorOccurrences := Authors{} authorOccurrences := Authors{}
mapAuthorToIndex := make(map[string]int) mapAuthorToIndex := make(map[string]int)
for _, repo := range repositories { for _, repo := range repositories {
@@ -202,7 +207,7 @@ func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
Name: authorName, Name: authorName,
Email: authorEmail, Email: authorEmail,
FoundIn: []string{repoIdentifier}, FoundIn: []string{repoIdentifier},
Levenshtein: utils.LevenshteinDistance(s.Target, authorName), Levenshtein: levenshteinDistanceAuthor(s.Target, authorName, authorEmail),
}) })
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1 mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
} }
@@ -334,13 +339,13 @@ func findEmailsAndOccurrencesInDir(rootPath string, username string) (Emails, er
if err != nil { if err != nil {
return err return err
} }
if !d.IsDir() { if d.Type().IsRegular() {
if strings.Contains(path, ".git/logs/") { if strings.Contains(path, ".git/logs/") {
return nil return nil
} }
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return nil
} }
currentFileEmails := emailRegex.FindAllString(string(content), -1) currentFileEmails := emailRegex.FindAllString(string(content), -1)
@@ -380,3 +385,10 @@ func findEmailsAndOccurrencesInDir(rootPath string, username string) (Emails, er
return results, nil return results, nil
} }
func levenshteinDistanceAuthor(target, name, email string) int {
if strings.Contains(email, "@") {
email = strings.SplitN(email, "@", 2)[0]
}
return slices.Min([]int{utils.LevenshteinDistance(target, name), utils.LevenshteinDistance(target, email)})
}
+10 -5
View File
@@ -27,16 +27,21 @@ type UsernameResult struct {
DeepScan DeepScanResult DeepScan DeepScanResult
} }
func Username(settings github_recon_settings.Settings) UsernameResult { func Username(settings github_recon_settings.Settings) (result UsernameResult, err error) {
result = UsernameResult{
result := UsernameResult{
Target: settings.Target, Target: settings.Target,
TargetType: settings.TargetType, TargetType: settings.TargetType,
DateTime: time.Now().String(), DateTime: time.Now().String(),
} }
utils.PrintTitle(settings.Silent, "👤 User informations") utils.PrintTitle(settings.Silent, "👤 User informations")
result.User = User(settings) result.User, err = User(settings)
if err != nil {
return
}
if result.User == (UserResult{}) {
return
}
utils.PrintAvatar(settings, result.User.AvatarURL) utils.PrintAvatar(settings, result.User.AvatarURL)
utils.PrintStruct(settings, result.User, 0) utils.PrintStruct(settings, result.User, 0)
@@ -74,5 +79,5 @@ func Username(settings github_recon_settings.Settings) UsernameResult {
utils.PrintStruct(settings, result.DeepScan, 0) utils.PrintStruct(settings, result.DeepScan, 0)
} }
return result return
} }
+4 -4
View File
@@ -31,13 +31,13 @@ type UserResult struct {
Plan string Plan string
} }
func User(s github_recon_settings.Settings) (response UserResult) { func User(s github_recon_settings.Settings) (response UserResult, err error) {
user, resp, err := s.Client.Users.Get(s.Ctx, s.Target) user, resp, err := s.Client.Users.Get(s.Ctx, s.Target)
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
s.Logger.Fatal("User not found with username") return UserResult{}, nil
} }
if err != nil { if err != nil {
s.Logger.Fatal("Failed to fetch user's information", "err", err) return UserResult{}, fmt.Errorf("failed to fetch user's information")
} }
u := UserResult{ u := UserResult{
@@ -65,5 +65,5 @@ func User(s github_recon_settings.Settings) (response UserResult) {
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
return u return u, nil
} }
+17 -17
View File
@@ -32,7 +32,7 @@ type Settings struct {
Silent bool Silent bool
DeepScan bool DeepScan bool
MaxDistance int MaxDistance int
HideAvatar bool PrintAvatar bool
SpoofEmail bool SpoofEmail bool
Trufflehog bool Trufflehog bool
@@ -55,7 +55,7 @@ func GetDefaultSettings() Settings {
Silent: false, Silent: false,
DeepScan: false, DeepScan: false,
MaxDistance: 20, MaxDistance: 20,
HideAvatar: false, PrintAvatar: true,
SpoofEmail: true, SpoofEmail: true,
Trufflehog: true, Trufflehog: true,
@@ -68,7 +68,7 @@ func GetDefaultSettings() Settings {
} }
} }
func GetSettings() (settings Settings) { func GetSettings() (settings Settings, err error) {
settings = GetDefaultSettings() settings = GetDefaultSettings()
//// Flag settings //// Flag settings
flag.Usage = func() { flag.Usage = func() {
@@ -83,55 +83,55 @@ func GetSettings() (settings Settings) {
flag.CommandLine.SortFlags = false flag.CommandLine.SortFlags = false
//// Flags //// Flags
flag.StringVarP(&settings.Token, "token", "t", "null", "Github personal access token (e.g. ghp_aaa...). Can also be set via GITHUB_RECON_TOKEN environment variable. You also need to set the token in $HOME/.config/github-recon/env file if you want to use this tool without passing the token every time.") flag.StringVarP(&settings.Token, "token", "t", settings.Token, "Github personal access token (e.g. ghp_aaa...). Can also be set via GITHUB_RECON_TOKEN environment variable. You also need to set the token in $HOME/.config/github-recon/env file if you want to use this tool without passing the token every time.")
// DeepScan // DeepScan
flag.BoolVarP(&settings.DeepScan, "deepscan", "d", false, "Enable deep scan (clone repos, regex search, analyse licenses, etc.)") flag.BoolVarP(&settings.DeepScan, "deepscan", "d", settings.DeepScan, "Enable deep scan (clone repos, regex search, analyse licenses, etc.)")
flag.IntVar( flag.IntVar(
&settings.MaxRepoSize, &settings.MaxRepoSize,
"max-size", "max-size",
150, settings.MaxRepoSize,
"Limit the size of repositories to scan (in MB) (only for deep scan)", "Limit the size of repositories to scan (in MB) (only for deep scan)",
) )
flag.StringSliceVarP( flag.StringSliceVarP(
&settings.ExcludedRepos, &settings.ExcludedRepos,
"exclude-repo", "exclude-repo",
"e", "e",
[]string{}, settings.ExcludedRepos,
"Exclude repos from deep scan (comma-separated list, only for deep scan)", "Exclude repos from deep scan (comma-separated list, only for deep scan)",
) )
flag.BoolVarP( flag.BoolVarP(
&settings.Refresh, &settings.Refresh,
"refresh", "refresh",
"r", "r",
false, settings.Refresh,
"Refresh the cache (only for deep scan)", "Refresh the cache (only for deep scan)",
) )
flag.BoolVarP( flag.BoolVarP(
&settings.ShowSource, &settings.ShowSource,
"show-source", "show-source",
"s", "s",
false, settings.ShowSource,
"Show where the information (authors, emails, etc) were found (only for deep scan)", "Show where the information (authors, emails, etc) were found (only for deep scan)",
) )
flag.IntVarP( flag.IntVarP(
&settings.MaxDistance, &settings.MaxDistance,
"max-distance", "max-distance",
"m", "m",
20, settings.MaxDistance,
"Maximum Levenshtein distance for matching usernames & emails (only for deep scan)", "Maximum Levenshtein distance for matching usernames & emails (only for deep scan)",
) )
flag.BoolVar( flag.BoolVar(
&settings.Trufflehog, &settings.Trufflehog,
"trufflehog", "trufflehog",
true, settings.Trufflehog,
"Run trufflehog on cloned repositories (only for deep scan)", "Run trufflehog on cloned repositories (only for deep scan)",
) )
flag.BoolVarP(&settings.Silent, "silent", "S", false, "Suppress all non-essential output") flag.BoolVarP(&settings.Silent, "silent", "S", settings.Silent, "Suppress all non-essential output")
flag.BoolVarP(&settings.SpoofEmail, "spoof-email", "", true, "Spoof email (only for email mode)") flag.BoolVarP(&settings.SpoofEmail, "spoof-email", "", settings.SpoofEmail, "Spoof email (only for email mode)")
flag.BoolVarP(&settings.HideAvatar, "hide-avatar", "a", false, "Hide the avatar in the output") flag.BoolVarP(&settings.PrintAvatar, "print-avatar", "a", settings.PrintAvatar, "Show the avatar in the output")
flag.StringVarP(&settings.JsonOutput, "json", "j", "", "Write results to specified JSON file") flag.StringVarP(&settings.JsonOutput, "json", "j", settings.JsonOutput, "Write results to specified JSON file")
//// Parse //// Parse
flag.Parse() flag.Parse()
@@ -152,7 +152,7 @@ func GetSettings() (settings Settings) {
settings.Target = strings.TrimPrefix(settings.Target, "@") // Remove the @ of the username settings.Target = strings.TrimPrefix(settings.Target, "@") // Remove the @ of the username
if strings.Contains(settings.Target, " ") { if strings.Contains(settings.Target, " ") {
settings.Logger.Fatal("Target cannot contain spaces") err = fmt.Errorf("target cannot contain spaces")
} }
if strings.Contains(settings.Target, "@") { if strings.Contains(settings.Target, "@") {
@@ -163,7 +163,7 @@ func GetSettings() (settings Settings) {
// If token is not set via flag, get it from env // If token is not set via flag, get it from env
if settings.Token == "null" || settings.Token == "" { if settings.Token == "null" || settings.Token == "" {
settings.Token = getToken() settings.Token = GetToken()
} }
if settings.Token == "null" || settings.Token == "" { if settings.Token == "null" || settings.Token == "" {
+2 -1
View File
@@ -9,7 +9,8 @@ import (
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )
func getToken() string { // GetToken retrieves the GitHub token from the environment variable or config file
func GetToken() string {
token := os.Getenv("GITHUB_RECON_TOKEN") token := os.Getenv("GITHUB_RECON_TOKEN")
if token != "" { if token != "" {
return token return token
+4 -2
View File
@@ -57,7 +57,9 @@ func PrintStruct(settings github_recon_settings.Settings, s any, indent int) {
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
field := t.Field(i).Name field := t.Field(i).Name
value := v.Field(i) value := v.Field(i)
if !value.CanInterface() {
continue
}
if !value.IsValid() || (value.Kind() == reflect.String && value.String() == "") { if !value.IsValid() || (value.Kind() == reflect.String && value.String() == "") {
continue continue
} }
@@ -143,7 +145,7 @@ func PrintTitle(silent bool, title string) {
} }
func PrintAvatar(settings github_recon_settings.Settings, url string) { func PrintAvatar(settings github_recon_settings.Settings, url string) {
if settings.HideAvatar || url == "" || settings.Silent { if !settings.PrintAvatar || url == "" || settings.Silent {
return return
} }