Compare commits

...
18 Commits
Author SHA1 Message Date
Hadi 53dd8ea2d5 Merge pull request #15 from Dylouwu/patch-1 2026-04-01 22:23:35 +02:00
Purin 832e996708 feat: hash update 2026-04-01 22:20:30 +02:00
Hadi 8528fe5732 Merge pull request #14 from anotherhadi/dependabot/go_modules/golang.org/x/image-0.38.0 2026-04-01 21:08:44 +02:00
dependabot[bot] 85ee3a6333 Bump golang.org/x/image from 0.28.0 to 0.38.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.28.0 to 0.38.0.
- [Commits](https://github.com/golang/image/compare/v0.28.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-30 16:27:18 +00:00
Hadi cb694d896d update flake
Signed-off-by: Hadi <[email protected]>
2025-09-24 17:10:25 +02:00
Hadi 1dfa0c1620 remove useless repetition
Signed-off-by: Hadi <[email protected]>
2025-09-24 17:09:39 +02:00
Hadi ef0240fd65 match version with release
Signed-off-by: Hadi <[email protected]>
2025-09-24 17:09:16 +02:00
Hadi d88bc6ae1e Add "email alias"
Signed-off-by: Hadi <[email protected]>
2025-09-22 19:01:02 +02:00
Hadi c460e5c24f API Url to HTML Url
Signed-off-by: Hadi <[email protected]>
2025-09-22 18:59:04 +02:00
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
14 changed files with 94 additions and 56 deletions
+3 -2
View File
@@ -195,8 +195,9 @@ privacy and security:
information.
- **Manage email exposure**: Use GitHub's settings to control which email
addresses are visible on your profile and in commit history. You can also use
a no-reply email address for commits. Delete/modify any sensitive information
in your commit history.
a no-reply email address for commits, and an
[alias email](https://proton.me/support/addresses-and-aliases) for your
account. Delete/modify any sensitive information in your commit history.
- **Be Mindful of Repository Content**: Avoid including sensitive information in
your repositories, such as API keys, passwords, emails or personal data. Use
`.gitignore` to exclude files that contain sensitive information.
+10 -2
View File
@@ -1,6 +1,8 @@
package main
import (
"log"
recon_email "github.com/anotherhadi/github-recon/github-recon/email"
recon_username "github.com/anotherhadi/github-recon/github-recon/username"
github_recon_settings "github.com/anotherhadi/github-recon/settings"
@@ -8,7 +10,10 @@ import (
)
func main() {
settings := github_recon_settings.GetSettings()
settings, err := github_recon_settings.GetSettings()
if err != nil {
log.Fatal(err)
}
if !settings.Silent {
utils.Header()
@@ -23,7 +28,10 @@ func main() {
}
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)
} else {
result := recon_email.Email(settings)
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1756787288,
"narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=",
"lastModified": 1758427187,
"narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1",
"rev": "554be6495561ff07b6c724047bdd7e0716aa7b46",
"type": "github"
},
"original": {
+2 -2
View File
@@ -14,7 +14,7 @@
(system: f system (import nixpkgs {inherit system;}));
pname = "github-recon";
version = "2.1.0";
version = "1.5.3";
ldflags = ["-s" "-w"];
in {
@@ -30,7 +30,7 @@
cp $GOPATH/bin/cmd $out/bin/github-recon
'';
vendorHash = "sha256-AD0h0k2n8gPqSBz5qqb0ZON/jWiSEWpeO97xR7cYSy8=";
vendorHash = "sha256-16mRhQyoyY3M98cWBURsEvvwVT6aR3JWk8YfAFNfm/A=";
meta = with pkgs.lib; {
description = "Retrieves and aggregates public OSINT data about a Github user using Go and the Github API. Finds hidden emails in commit history, previous usernames, friends, other Github accounts, and more.";
+2 -2
View File
@@ -13,7 +13,7 @@ type EmailResult struct {
TargetType github_recon_settings.TargetType
Commits CommitsResult
Spoofing SpoofingResult
Spoofing *SpoofingResult
}
func Email(settings github_recon_settings.Settings) EmailResult {
@@ -33,7 +33,7 @@ func Email(settings github_recon_settings.Settings) EmailResult {
} else {
utils.PrintTitle(settings.Silent, "🎭 Spoofing test")
result.Spoofing = Spoofing(settings)
if result.Spoofing.AvatarURL != "" {
if result.Spoofing != nil && result.Spoofing.AvatarURL != "" {
utils.PrintAvatar(settings, result.Spoofing.AvatarURL)
}
utils.PrintStruct(settings, result.Spoofing, 0)
+33 -23
View File
@@ -2,6 +2,7 @@ package recon
import (
"math/rand"
"time"
github_recon_settings "github.com/anotherhadi/github-recon/settings"
"github.com/anotherhadi/github-recon/utils"
@@ -26,7 +27,8 @@ func RandomString(n int) string {
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)
private := true
autoInit := true
@@ -41,6 +43,13 @@ func Spoofing(s github_recon_settings.Settings) (response SpoofingResult) {
}
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()
if branch == "" {
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)
if err != nil {
s.Logger.Error("Error while getting ref", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return
}
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())
if err != nil {
s.Logger.Error("Error while getting parent commit", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return
}
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)
if err != nil {
s.Logger.Error("Error while creating spoofed empty commit", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return
}
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)
if err != nil {
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
}
utils.WaitForRateLimit(s, resp)
commits, _, err := s.Client.Repositories.ListCommits(s.Ctx, repo.Owner.GetLogin(), name, nil)
if err != nil {
s.Logger.Error("Error while listing commits", "err", err)
s.Logger.Warn("The temp repo was left undeleted", "repo", repo.GetHTMLURL())
return
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)
if err != nil {
s.Logger.Error("Error while listing commits", "err", err)
return
}
if len(commits) > 1 {
last := commits[0]
response.Username = last.GetAuthor().GetLogin()
response.Name = last.GetAuthor().GetName()
response.Email = last.GetAuthor().GetEmail()
response.Url = last.GetAuthor().GetHTMLURL()
response.AvatarURL = last.GetAuthor().GetAvatarURL()
break
}
s.Logger.Info("Only one commit found, retrying...", "attempt", i+1)
time.Sleep(retryDelay)
}
if len(commits) > 0 {
last := commits[0]
response.Username = last.GetAuthor().GetLogin()
response.Name = last.GetAuthor().GetName()
response.Email = last.GetAuthor().GetEmail()
response.Url = last.GetAuthor().GetHTMLURL()
response.AvatarURL = last.GetAuthor().GetAvatarURL()
if response.Username == "" && response.Name == "" && response.Email == "" {
return nil
}
_, err = s.Client.Repositories.Delete(s.Ctx, repo.Owner.GetLogin(), name)
if err != nil {
s.Logger.Error("Error while deleting repo", "err", err)
}
return
}
+16 -4
View File
@@ -82,7 +82,7 @@ func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
}
repositories = append(repositories, Repositorie{
Repository: repo.GetCloneURL(),
Repository: repo.GetHTMLURL(),
Owner: repo.GetOwner().GetLogin(),
Name: repo.GetName(),
Size: repo.GetSize(),
@@ -139,6 +139,11 @@ func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
}
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{}
mapAuthorToIndex := make(map[string]int)
for _, repo := range repositories {
@@ -202,7 +207,7 @@ func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
Name: authorName,
Email: authorEmail,
FoundIn: []string{repoIdentifier},
Levenshtein: utils.LevenshteinDistance(s.Target, authorName),
Levenshtein: levenshteinDistanceAuthor(s.Target, authorName, authorEmail),
})
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
}
@@ -334,13 +339,13 @@ func findEmailsAndOccurrencesInDir(rootPath string, username string) (Emails, er
if err != nil {
return err
}
if !d.IsDir() {
if d.Type().IsRegular() {
if strings.Contains(path, ".git/logs/") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return err
return nil
}
currentFileEmails := emailRegex.FindAllString(string(content), -1)
@@ -380,3 +385,10 @@ func findEmailsAndOccurrencesInDir(rootPath string, username string) (Emails, er
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
}
func Username(settings github_recon_settings.Settings) UsernameResult {
result := UsernameResult{
func Username(settings github_recon_settings.Settings) (result UsernameResult, err error) {
result = UsernameResult{
Target: settings.Target,
TargetType: settings.TargetType,
DateTime: time.Now().String(),
}
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.PrintStruct(settings, result.User, 0)
@@ -74,5 +79,5 @@ func Username(settings github_recon_settings.Settings) UsernameResult {
utils.PrintStruct(settings, result.DeepScan, 0)
}
return result
return
}
+1 -1
View File
@@ -23,7 +23,7 @@ func Orgs(s github_recon_settings.Settings) (response OrgsResult) {
for _, org := range orgs {
o := OrgResult{
Name: org.GetLogin(),
URL: org.GetURL(),
URL: org.GetHTMLURL(),
Description: org.GetDescription(),
}
response = append(response, o)
+4 -4
View File
@@ -31,13 +31,13 @@ type UserResult struct {
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)
if resp.StatusCode == 404 {
s.Logger.Fatal("User not found with username")
return UserResult{}, 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{
@@ -65,5 +65,5 @@ func User(s github_recon_settings.Settings) (response UserResult) {
}
utils.WaitForRateLimit(s, resp)
return u
return u, nil
}
+2 -2
View File
@@ -1,6 +1,6 @@
module github.com/anotherhadi/github-recon
go 1.24.5
go 1.25.0
require (
github.com/charmbracelet/lipgloss v1.1.0
@@ -26,6 +26,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/image v0.38.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)
+2 -2
View File
@@ -48,8 +48,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+3 -3
View File
@@ -68,11 +68,11 @@ func GetDefaultSettings() Settings {
}
}
func GetSettings() (settings Settings) {
func GetSettings() (settings Settings, err error) {
settings = GetDefaultSettings()
//// Flag settings
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Usage:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "github-recon [flags] <target username or email>\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
@@ -152,7 +152,7 @@ func GetSettings() (settings Settings) {
settings.Target = strings.TrimPrefix(settings.Target, "@") // Remove the @ of the username
if strings.Contains(settings.Target, " ") {
settings.Logger.Fatal("Target cannot contain spaces")
err = fmt.Errorf("target cannot contain spaces")
}
if strings.Contains(settings.Target, "@") {
+3 -1
View File
@@ -57,7 +57,9 @@ func PrintStruct(settings github_recon_settings.Settings, s any, indent int) {
for i := 0; i < v.NumField(); i++ {
field := t.Field(i).Name
value := v.Field(i)
if !value.CanInterface() {
continue
}
if !value.IsValid() || (value.Kind() == reflect.String && value.String() == "") {
continue
}